From 51e0f3a5ad3db6684ee07af58d4fdbd980943c07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E9=B9=8F?= Date: Tue, 7 Jul 2026 15:12:51 +0800 Subject: [PATCH] feat: add authentication and tenant context --- .../linhelp/common/config/SaTokenConfig.java | 17 +++++ .../common/security/AuthController.java | 43 ++++++++++++ .../linhelp/common/security/AuthService.java | 67 +++++++++++++++++++ .../linhelp/common/security/CurrentUser.java | 34 ++++++++++ .../linhelp/common/security/LoginRequest.java | 27 ++++++++ .../common/security/LoginResponse.java | 40 +++++++++++ .../linhelp/common/security/RequireRole.java | 12 ++++ .../common/security/RoleGuardInterceptor.java | 35 ++++++++++ .../common/security/TenantContext.java | 44 ++++++++++++ .../common/security/TenantContextTests.java | 22 ++++++ 10 files changed, 341 insertions(+) create mode 100644 backend/src/main/java/com/linhelp/common/config/SaTokenConfig.java create mode 100644 backend/src/main/java/com/linhelp/common/security/AuthController.java create mode 100644 backend/src/main/java/com/linhelp/common/security/AuthService.java create mode 100644 backend/src/main/java/com/linhelp/common/security/CurrentUser.java create mode 100644 backend/src/main/java/com/linhelp/common/security/LoginRequest.java create mode 100644 backend/src/main/java/com/linhelp/common/security/LoginResponse.java create mode 100644 backend/src/main/java/com/linhelp/common/security/RequireRole.java create mode 100644 backend/src/main/java/com/linhelp/common/security/RoleGuardInterceptor.java create mode 100644 backend/src/main/java/com/linhelp/common/security/TenantContext.java create mode 100644 backend/src/test/java/com/linhelp/common/security/TenantContextTests.java diff --git a/backend/src/main/java/com/linhelp/common/config/SaTokenConfig.java b/backend/src/main/java/com/linhelp/common/config/SaTokenConfig.java new file mode 100644 index 0000000..7b6e21e --- /dev/null +++ b/backend/src/main/java/com/linhelp/common/config/SaTokenConfig.java @@ -0,0 +1,17 @@ +package com.linhelp.common.config; + +import com.linhelp.common.security.RoleGuardInterceptor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class SaTokenConfig implements WebMvcConfigurer { + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new RoleGuardInterceptor()) + .addPathPatterns("/api/**") + .excludePathPatterns("/api/auth/**"); + } +} diff --git a/backend/src/main/java/com/linhelp/common/security/AuthController.java b/backend/src/main/java/com/linhelp/common/security/AuthController.java new file mode 100644 index 0000000..d7f991b --- /dev/null +++ b/backend/src/main/java/com/linhelp/common/security/AuthController.java @@ -0,0 +1,43 @@ +package com.linhelp.common.security; + +import com.linhelp.common.api.ApiResponse; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; + +@Validated +@RestController +@RequestMapping("/api/auth") +public class AuthController { + private final AuthService authService; + + public AuthController(AuthService authService) { + this.authService = authService; + } + + @PostMapping("/admin-login") + public ApiResponse adminLogin(@Valid @RequestBody LoginRequest request) { + return ApiResponse.ok(authService.adminLogin(request)); + } + + @PostMapping("/dev-miniapp-login") + public ApiResponse devMiniappLogin(@Valid @RequestBody LoginRequest request) { + return ApiResponse.ok(authService.devMiniappLogin(request)); + } + + @PostMapping("/logout") + public ApiResponse logout() { + authService.logout(); + return ApiResponse.ok(); + } + + @GetMapping("/me") + public ApiResponse me() { + return ApiResponse.ok(authService.currentUser()); + } +} diff --git a/backend/src/main/java/com/linhelp/common/security/AuthService.java b/backend/src/main/java/com/linhelp/common/security/AuthService.java new file mode 100644 index 0000000..3605e78 --- /dev/null +++ b/backend/src/main/java/com/linhelp/common/security/AuthService.java @@ -0,0 +1,67 @@ +package com.linhelp.common.security; + +import cn.dev33.satoken.stp.StpUtil; +import com.linhelp.common.exception.BizException; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; + +@Service +public class AuthService { + private static final Long DEFAULT_TENANT_ID = 1L; + private static final Long DEFAULT_COMMUNITY_ID = 1L; + private final Map demoAccounts = new HashMap(); + + public AuthService() { + demoAccounts.put("admin", new DemoAccount(1L, "admin123", "COMMUNITY_ADMIN")); + demoAccounts.put("merchant", new DemoAccount(2L, "merchant123", "MERCHANT")); + demoAccounts.put("rider", new DemoAccount(3L, "rider123", "RIDER")); + } + + public LoginResponse adminLogin(LoginRequest request) { + DemoAccount account = demoAccounts.get(request.getUsername()); + if (account == null || !account.password.equals(request.getPassword())) { + throw new BizException(401, "账号或密码错误"); + } + return login(account.userId, account.roleCode); + } + + public LoginResponse devMiniappLogin(LoginRequest request) { + return login(1000L, "RESIDENT"); + } + + public CurrentUser currentUser() { + StpUtil.checkLogin(); + return new CurrentUser( + StpUtil.getLoginIdAsLong(), + (Long) StpUtil.getSession().get("tenantId"), + (Long) StpUtil.getSession().get("communityId"), + (String) StpUtil.getSession().get("roleCode") + ); + } + + public void logout() { + StpUtil.logout(); + } + + private LoginResponse login(Long userId, String roleCode) { + StpUtil.login(userId); + StpUtil.getSession().set("tenantId", DEFAULT_TENANT_ID); + StpUtil.getSession().set("communityId", DEFAULT_COMMUNITY_ID); + StpUtil.getSession().set("roleCode", roleCode); + return new LoginResponse(StpUtil.getTokenValue(), userId, DEFAULT_TENANT_ID, DEFAULT_COMMUNITY_ID, roleCode); + } + + private static class DemoAccount { + private final Long userId; + private final String password; + private final String roleCode; + + private DemoAccount(Long userId, String password, String roleCode) { + this.userId = userId; + this.password = password; + this.roleCode = roleCode; + } + } +} diff --git a/backend/src/main/java/com/linhelp/common/security/CurrentUser.java b/backend/src/main/java/com/linhelp/common/security/CurrentUser.java new file mode 100644 index 0000000..5d716cd --- /dev/null +++ b/backend/src/main/java/com/linhelp/common/security/CurrentUser.java @@ -0,0 +1,34 @@ +package com.linhelp.common.security; + +public class CurrentUser { + private Long userId; + private Long tenantId; + private Long communityId; + private String roleCode; + + public CurrentUser() { + } + + public CurrentUser(Long userId, Long tenantId, Long communityId, String roleCode) { + this.userId = userId; + this.tenantId = tenantId; + this.communityId = communityId; + this.roleCode = roleCode; + } + + public Long getUserId() { + return userId; + } + + public Long getTenantId() { + return tenantId; + } + + public Long getCommunityId() { + return communityId; + } + + public String getRoleCode() { + return roleCode; + } +} diff --git a/backend/src/main/java/com/linhelp/common/security/LoginRequest.java b/backend/src/main/java/com/linhelp/common/security/LoginRequest.java new file mode 100644 index 0000000..ebc0988 --- /dev/null +++ b/backend/src/main/java/com/linhelp/common/security/LoginRequest.java @@ -0,0 +1,27 @@ +package com.linhelp.common.security; + +import javax.validation.constraints.NotBlank; + +public class LoginRequest { + @NotBlank + private String username; + + @NotBlank + private String password; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/backend/src/main/java/com/linhelp/common/security/LoginResponse.java b/backend/src/main/java/com/linhelp/common/security/LoginResponse.java new file mode 100644 index 0000000..682ff52 --- /dev/null +++ b/backend/src/main/java/com/linhelp/common/security/LoginResponse.java @@ -0,0 +1,40 @@ +package com.linhelp.common.security; + +public class LoginResponse { + private String token; + private Long userId; + private Long tenantId; + private Long communityId; + private String roleCode; + + public LoginResponse() { + } + + public LoginResponse(String token, Long userId, Long tenantId, Long communityId, String roleCode) { + this.token = token; + this.userId = userId; + this.tenantId = tenantId; + this.communityId = communityId; + this.roleCode = roleCode; + } + + public String getToken() { + return token; + } + + public Long getUserId() { + return userId; + } + + public Long getTenantId() { + return tenantId; + } + + public Long getCommunityId() { + return communityId; + } + + public String getRoleCode() { + return roleCode; + } +} diff --git a/backend/src/main/java/com/linhelp/common/security/RequireRole.java b/backend/src/main/java/com/linhelp/common/security/RequireRole.java new file mode 100644 index 0000000..3cf307c --- /dev/null +++ b/backend/src/main/java/com/linhelp/common/security/RequireRole.java @@ -0,0 +1,12 @@ +package com.linhelp.common.security; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface RequireRole { + String[] value(); +} diff --git a/backend/src/main/java/com/linhelp/common/security/RoleGuardInterceptor.java b/backend/src/main/java/com/linhelp/common/security/RoleGuardInterceptor.java new file mode 100644 index 0000000..c8cb006 --- /dev/null +++ b/backend/src/main/java/com/linhelp/common/security/RoleGuardInterceptor.java @@ -0,0 +1,35 @@ +package com.linhelp.common.security; + +import cn.dev33.satoken.stp.StpUtil; +import com.linhelp.common.exception.BizException; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class RoleGuardInterceptor implements HandlerInterceptor { + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + if (!(handler instanceof HandlerMethod)) { + return true; + } + HandlerMethod method = (HandlerMethod) handler; + RequireRole requireRole = method.getMethodAnnotation(RequireRole.class); + if (requireRole == null) { + requireRole = method.getBeanType().getAnnotation(RequireRole.class); + } + if (requireRole == null) { + return true; + } + StpUtil.checkLogin(); + String currentRole = (String) StpUtil.getSession().get("roleCode"); + for (String role : requireRole.value()) { + if (role.equals(currentRole)) { + return true; + } + } + throw new BizException(403, "无权访问"); + } +} diff --git a/backend/src/main/java/com/linhelp/common/security/TenantContext.java b/backend/src/main/java/com/linhelp/common/security/TenantContext.java new file mode 100644 index 0000000..cd64b91 --- /dev/null +++ b/backend/src/main/java/com/linhelp/common/security/TenantContext.java @@ -0,0 +1,44 @@ +package com.linhelp.common.security; + +public final class TenantContext { + private static final ThreadLocal SCOPE = new ThreadLocal(); + + private TenantContext() { + } + + public static void set(Long tenantId, Long communityId) { + SCOPE.set(new Scope(tenantId, communityId)); + } + + public static Long tenantId() { + Scope scope = SCOPE.get(); + return scope == null ? null : scope.tenantId(); + } + + public static Long communityId() { + Scope scope = SCOPE.get(); + return scope == null ? null : scope.communityId(); + } + + public static void clear() { + SCOPE.remove(); + } + + private static class Scope { + private final Long tenantId; + private final Long communityId; + + private Scope(Long tenantId, Long communityId) { + this.tenantId = tenantId; + this.communityId = communityId; + } + + private Long tenantId() { + return tenantId; + } + + private Long communityId() { + return communityId; + } + } +} diff --git a/backend/src/test/java/com/linhelp/common/security/TenantContextTests.java b/backend/src/test/java/com/linhelp/common/security/TenantContextTests.java new file mode 100644 index 0000000..abdea28 --- /dev/null +++ b/backend/src/test/java/com/linhelp/common/security/TenantContextTests.java @@ -0,0 +1,22 @@ +package com.linhelp.common.security; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class TenantContextTests { + + @AfterEach + void clear() { + TenantContext.clear(); + } + + @Test + void storesTenantAndCommunityForCurrentRequestThread() { + TenantContext.set(1L, 10L); + + assertThat(TenantContext.tenantId()).isEqualTo(1L); + assertThat(TenantContext.communityId()).isEqualTo(10L); + } +}