From 0e9c8d5f044846f5184dad63254d4b0a9e74249a Mon Sep 17 00:00:00 2001 From: xiaoyusu Date: Sun, 22 Mar 2026 20:20:21 +0800 Subject: [PATCH] =?UTF-8?q?satoken=E9=89=B4=E6=9D=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 29 ++++- .../common/config/SaTokenConfigure.java | 26 +++++ .../common/error/GlobalExceptionHandler.java | 54 ++++++--- .../controller/OpenAiStreamController.java | 6 +- .../usercenter/config/SecurityConfig.java | 104 +++++++++--------- .../usercenter/controller/AuthController.java | 8 +- .../controller/OAuthController.java | 90 +++++++++++++++ .../controller/UserCenterController.java | 46 ++++---- .../repository/JdbcUserAccountRepository.java | 39 ++++++- .../repository/UserAccountRepository.java | 7 +- .../usercenter/service/AuthService.java | 67 +++++++++-- .../usercenter/service/UserCenterService.java | 28 ++--- src/main/resources/application.properties | 30 ++++- 13 files changed, 405 insertions(+), 129 deletions(-) create mode 100644 src/main/java/com/involutionhell/backend/common/config/SaTokenConfigure.java create mode 100644 src/main/java/com/involutionhell/backend/usercenter/controller/OAuthController.java diff --git a/pom.xml b/pom.xml index 8388e8b..faa1f72 100644 --- a/pom.xml +++ b/pom.xml @@ -61,7 +61,8 @@ spring-boot-starter-jdbc - + + + + + + me.zhyd.oauth + JustAuth + 1.16.6 + + + cn.hutool + hutool-http + 5.8.25 + + + + cn.hutool + hutool-json + 5.8.25 + @@ -82,6 +102,13 @@ runtime + + + cn.dev33 + sa-token-spring-boot3-starter + 1.39.0 + + diff --git a/src/main/java/com/involutionhell/backend/common/config/SaTokenConfigure.java b/src/main/java/com/involutionhell/backend/common/config/SaTokenConfigure.java new file mode 100644 index 0000000..108d100 --- /dev/null +++ b/src/main/java/com/involutionhell/backend/common/config/SaTokenConfigure.java @@ -0,0 +1,26 @@ +package com.involutionhell.backend.common.config; + +import cn.dev33.satoken.interceptor.SaInterceptor; +import cn.dev33.satoken.router.SaRouter; +import cn.dev33.satoken.stp.StpUtil; +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 SaTokenConfigure implements WebMvcConfigurer { + + // 注册 SaToken 拦截器 + @Override + public void addInterceptors(InterceptorRegistry registry) { + // 注册 SaToken 拦截器,定义详细认证规则 + registry.addInterceptor(new SaInterceptor(handler -> { + // 拦截规则配置 + SaRouter + .match("/**") // 拦截所有路由 + .notMatch("/api/auth/login") // 排除登录接口 + .notMatch("/api/auth/register") // 排除注册接口 // 排除 Spring Boot 默认错误界面 + .check(r -> StpUtil.checkLogin()); // 校验是否登录,如果未登录,这里会抛出 NotLoginException + })).addPathPatterns("/**"); + } +} \ No newline at end of file diff --git a/src/main/java/com/involutionhell/backend/common/error/GlobalExceptionHandler.java b/src/main/java/com/involutionhell/backend/common/error/GlobalExceptionHandler.java index a0f1421..bb55665 100644 --- a/src/main/java/com/involutionhell/backend/common/error/GlobalExceptionHandler.java +++ b/src/main/java/com/involutionhell/backend/common/error/GlobalExceptionHandler.java @@ -1,7 +1,8 @@ package com.involutionhell.backend.common.error; -import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.authentication.InsufficientAuthenticationException; +import cn.dev33.satoken.exception.NotLoginException; +import cn.dev33.satoken.exception.NotPermissionException; +import cn.dev33.satoken.exception.NotRoleException; import com.involutionhell.backend.common.api.ApiResponse; import jakarta.validation.ConstraintViolationException; import java.util.Optional; @@ -15,30 +16,52 @@ @RestControllerAdvice public class GlobalExceptionHandler { + // ========================================== + // Sa-Token 异常拦截 + // ========================================== + /** - * 将未登录异常转换为 401 响应 - */ - /** - * 将认证不足异常转换为 401 响应 + * Sa-Token: 拦截未登录异常 */ - @ExceptionHandler(InsufficientAuthenticationException.class) - public ResponseEntity> handleAuthentication(InsufficientAuthenticationException exception) { + @ExceptionHandler(NotLoginException.class) + public ResponseEntity> handleNotLoginException(NotLoginException e) { + // 判断场景值,定制化异常信息 + String message = switch (e.getType()) { + case NotLoginException.NOT_TOKEN -> "未提供 Token"; + case NotLoginException.INVALID_TOKEN -> "Token 无效"; + case NotLoginException.TOKEN_TIMEOUT -> "Token 已过期"; + case NotLoginException.BE_REPLACED -> "Token 已被顶下线"; + case NotLoginException.KICK_OUT -> "Token 已被踢下线"; + case NotLoginException.TOKEN_FREEZE -> "Token 已被冻结"; + case NotLoginException.NO_PREFIX -> "未按照指定前缀提交 Token"; + default -> "当前会话未登录"; + }; return ResponseEntity.status(HttpStatus.UNAUTHORIZED) - .body(ApiResponse.fail("未登录或登录状态已失效")); + .body(ApiResponse.fail(message)); } /** - * 将权限不足异常转换为 403 响应 + * Sa-Token: 拦截权限不足异常 */ + @ExceptionHandler(NotPermissionException.class) + public ResponseEntity> handleNotPermissionException(NotPermissionException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(ApiResponse.fail("拒绝访问: 缺少权限 [" + e.getCode() + "]")); + } + /** - * 将权限不足异常转换为 403 响应 (Spring Security 版) + * Sa-Token: 拦截角色不足异常 */ - @ExceptionHandler(AccessDeniedException.class) - public ResponseEntity> handleAccessDenied(AccessDeniedException exception) { + @ExceptionHandler(NotRoleException.class) + public ResponseEntity> handleNotRoleException(NotRoleException e) { return ResponseEntity.status(HttpStatus.FORBIDDEN) - .body(ApiResponse.fail("拒绝访问: 权限不足")); + .body(ApiResponse.fail("拒绝访问: 缺少角色 [" + e.getRole() + "]")); } + // ========================================== + // 业务与通用异常拦截 + // ========================================== + /** * 将参数校验异常转换为 400 响应 */ @@ -65,6 +88,7 @@ public ResponseEntity> handleBusiness(Exception exception) { */ @ExceptionHandler(Exception.class) public ResponseEntity> handleUnexpected(Exception exception) { + exception.printStackTrace(); // 建议在开发阶段打印堆栈,生产环境应使用日志框架 return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(ApiResponse.fail("服务器内部错误")); } @@ -91,4 +115,4 @@ private String resolveValidationMessage(Exception exception) { } return "请求参数不合法"; } -} +} \ No newline at end of file diff --git a/src/main/java/com/involutionhell/backend/openai/controller/OpenAiStreamController.java b/src/main/java/com/involutionhell/backend/openai/controller/OpenAiStreamController.java index 654a62c..e0a604a 100644 --- a/src/main/java/com/involutionhell/backend/openai/controller/OpenAiStreamController.java +++ b/src/main/java/com/involutionhell/backend/openai/controller/OpenAiStreamController.java @@ -1,6 +1,6 @@ package com.involutionhell.backend.openai.controller; -import org.springframework.security.access.prepost.PreAuthorize; +import cn.dev33.satoken.annotation.SaCheckLogin; import com.involutionhell.backend.openai.dto.OpenAiStreamRequest; import com.involutionhell.backend.openai.service.OpenAiStreamService; import jakarta.validation.Valid; @@ -27,7 +27,7 @@ public OpenAiStreamController(OpenAiStreamService openAiStreamService) { /** * 调用 OpenAI Responses API 并以 SSE 形式持续推送模型输出。 */ - @PreAuthorize("isAuthenticated()") + @SaCheckLogin @PostMapping( path = "/responses/stream", consumes = MediaType.APPLICATION_JSON_VALUE, @@ -36,4 +36,4 @@ public OpenAiStreamController(OpenAiStreamService openAiStreamService) { public SseEmitter streamResponses(@Valid @RequestBody OpenAiStreamRequest request) { return openAiStreamService.stream(request); } -} +} \ No newline at end of file diff --git a/src/main/java/com/involutionhell/backend/usercenter/config/SecurityConfig.java b/src/main/java/com/involutionhell/backend/usercenter/config/SecurityConfig.java index 6357cea..bf0a118 100644 --- a/src/main/java/com/involutionhell/backend/usercenter/config/SecurityConfig.java +++ b/src/main/java/com/involutionhell/backend/usercenter/config/SecurityConfig.java @@ -1,51 +1,53 @@ -package com.involutionhell.backend.usercenter.config; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.oauth2.jwt.JwtDecoder; -import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; -import org.springframework.security.web.SecurityFilterChain; - -import javax.crypto.spec.SecretKeySpec; -import java.nio.charset.StandardCharsets; - -import static org.springframework.security.config.Customizer.withDefaults; - -@Configuration -@EnableWebSecurity -@EnableMethodSecurity -public class SecurityConfig { - - @Value("${jwt.secret-key}") - private String secretKey; - - @Bean - public JwtDecoder jwtDecoder() { - // 使用 HmacSHA256 算法生成 SecretKeySpec - SecretKeySpec keySpec = new SecretKeySpec( - secretKey.getBytes(StandardCharsets.UTF_8), - "HmacSHA256" - ); - return NimbusJwtDecoder.withSecretKey(keySpec).build(); - } - - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - .csrf(AbstractHttpConfigurer::disable) - .authorizeHttpRequests(authorize -> authorize - .requestMatchers("/api/auth/login", "/actuator/**", "/public/**").permitAll() - .anyRequest().authenticated() - ) - .oauth2Login(withDefaults()) // 启用 OAuth2 登录支持 - .oauth2Client(withDefaults()) // 启用 OAuth2 Client 支持 - .oauth2ResourceServer(oauth2 -> oauth2.jwt(withDefaults())); // 启用 JWT 校验 (Resource Server) - - return http.build(); - } -} +// Temporarily commented out for JustAuth Migration + +// package com.involutionhell.backend.usercenter.config; + +// import org.springframework.beans.factory.annotation.Value; +// import org.springframework.context.annotation.Bean; +// import org.springframework.context.annotation.Configuration; +// import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +// import org.springframework.security.config.annotation.web.builders.HttpSecurity; +// import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +// import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +// import org.springframework.security.oauth2.jwt.JwtDecoder; +// import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +// import org.springframework.security.web.SecurityFilterChain; + +// import javax.crypto.spec.SecretKeySpec; +// import java.nio.charset.StandardCharsets; + +// import static org.springframework.security.config.Customizer.withDefaults; + +// @Configuration +// @EnableWebSecurity +// @EnableMethodSecurity +// public class SecurityConfig { +// +// @Value("${jwt.secret-key}") +// private String secretKey; + +// @Bean +// public JwtDecoder jwtDecoder() { +// // 使用 HmacSHA256 算法生成 SecretKeySpec +// SecretKeySpec keySpec = new SecretKeySpec( +// secretKey.getBytes(StandardCharsets.UTF_8), +// "HmacSHA256" +// ); +// return NimbusJwtDecoder.withSecretKey(keySpec).build(); +// } + +// @Bean +// public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { +// http +// .csrf(AbstractHttpConfigurer::disable) +// .authorizeHttpRequests(authorize -> authorize +// .requestMatchers("/api/auth/login", "/actuator/**", "/public/**").permitAll() +// .anyRequest().authenticated() +// ) +// .oauth2Login(withDefaults()) // 启用 OAuth2 登录支持 +// .oauth2Client(withDefaults()) // 启用 OAuth2 Client 支持 +// .oauth2ResourceServer(oauth2 -> oauth2.jwt(withDefaults())); // 启用 JWT 校验 (Resource Server) + +// return http.build(); +// } +// } diff --git a/src/main/java/com/involutionhell/backend/usercenter/controller/AuthController.java b/src/main/java/com/involutionhell/backend/usercenter/controller/AuthController.java index e3bdd80..3aab490 100644 --- a/src/main/java/com/involutionhell/backend/usercenter/controller/AuthController.java +++ b/src/main/java/com/involutionhell/backend/usercenter/controller/AuthController.java @@ -1,6 +1,6 @@ package com.involutionhell.backend.usercenter.controller; -import org.springframework.security.access.prepost.PreAuthorize; +import cn.dev33.satoken.annotation.SaCheckLogin; import com.involutionhell.backend.common.api.ApiResponse; import com.involutionhell.backend.usercenter.dto.LoginRequest; import com.involutionhell.backend.usercenter.dto.LoginResponse; @@ -37,7 +37,7 @@ public ApiResponse login(@Valid @RequestBody LoginRequest request /** * 退出当前登录会话。 */ - @PreAuthorize("isAuthenticated()") + @SaCheckLogin @PostMapping("/logout") public ApiResponse logout() { authService.logout(); @@ -47,9 +47,9 @@ public ApiResponse logout() { /** * 查询当前登录用户信息。 */ - @PreAuthorize("isAuthenticated()") + @SaCheckLogin @GetMapping("/me") public ApiResponse currentUser() { return ApiResponse.ok(authService.currentUser()); } -} +} \ No newline at end of file diff --git a/src/main/java/com/involutionhell/backend/usercenter/controller/OAuthController.java b/src/main/java/com/involutionhell/backend/usercenter/controller/OAuthController.java new file mode 100644 index 0000000..cd4184a --- /dev/null +++ b/src/main/java/com/involutionhell/backend/usercenter/controller/OAuthController.java @@ -0,0 +1,90 @@ +package com.involutionhell.backend.usercenter.controller; + +import cn.dev33.satoken.stp.StpUtil; +import me.zhyd.oauth.config.AuthConfig; +import me.zhyd.oauth.model.AuthCallback; +import me.zhyd.oauth.model.AuthResponse; +import me.zhyd.oauth.model.AuthUser; +import me.zhyd.oauth.request.AuthGithubRequest; +import me.zhyd.oauth.request.AuthRequest; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +@RestController +@RequestMapping("/api/oauth") +public class OAuthController { + + @Value("${justauth.type.github.client-id}") + private String githubClientId; + + @Value("${justauth.type.github.client-secret}") + private String githubClientSecret; + + @Value("${justauth.type.github.redirect-uri}") + private String githubRedirectUri; + + @Value("${AUTH_URL:http://localhost:3000}") + private String frontEndUrl; + + /** + * 获取 GitHub 授权请求对象 + */ + private AuthRequest getAuthRequest() { + return new AuthGithubRequest(AuthConfig.builder() + .clientId(githubClientId) + .clientSecret(githubClientSecret) + .redirectUri(githubRedirectUri) + .build()); + } + + /** + * 构建授权链接并重定向到第三方平台 + * 前端通过直接访问此接口(如:a 标签 href)来发起登录 + */ + @GetMapping("/render/github") + public void renderAuth(HttpServletResponse response) throws IOException { + AuthRequest authRequest = getAuthRequest(); + response.sendRedirect(authRequest.authorize(me.zhyd.oauth.utils.AuthStateUtils.createState())); + } + + /** + * 第三方平台授权后的回调地址 + * 由 GitHub 重定向回来,携带 code 等参数 + */ + @GetMapping("/callback/github") + public void login(AuthCallback callback, HttpServletResponse response) throws IOException { + AuthRequest authRequest = getAuthRequest(); + AuthResponse authResponse = authRequest.login(callback); + + if (authResponse.ok()) { + AuthUser authUser = (AuthUser) authResponse.getData(); + + // ========================================== + // TODO: 在这里编写你的业务逻辑 + // 1. 根据 authUser.getUuid() 或者 authUser.getEmail() 去数据库查询用户是否存在 + // 2. 如果不存在,自动注册该用户并落库 + // 3. 拿到最终的系统内部 UserID + // ========================================== + + // 模拟获取到了用户ID为 10001 + long systemUserId = 10001L; + + // 使用 Sa-Token 登录 + StpUtil.login(systemUserId); + + // 登录成功后,重定向回前端页面,并将 Token 放在 URL 参数中带给前端 + // 前端页面可以在加载时读取 URL 参数中的 token 并存入 localStorage + String tokenValue = StpUtil.getTokenValue(); + String redirectUrl = frontEndUrl + "/?token=" + tokenValue; + response.sendRedirect(redirectUrl); + } else { + // 登录失败,重定向回前端并带上错误信息 + response.sendRedirect(frontEndUrl + "/login?error=oauth_failed"); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/involutionhell/backend/usercenter/controller/UserCenterController.java b/src/main/java/com/involutionhell/backend/usercenter/controller/UserCenterController.java index 90b8ebe..0bfdb2f 100644 --- a/src/main/java/com/involutionhell/backend/usercenter/controller/UserCenterController.java +++ b/src/main/java/com/involutionhell/backend/usercenter/controller/UserCenterController.java @@ -1,12 +1,11 @@ package com.involutionhell.backend.usercenter.controller; -import org.springframework.security.access.prepost.PreAuthorize; +import cn.dev33.satoken.annotation.SaCheckPermission; import com.involutionhell.backend.common.api.ApiResponse; import com.involutionhell.backend.usercenter.dto.UserAuthorizationUpdateRequest; import com.involutionhell.backend.usercenter.dto.UserView; import com.involutionhell.backend.usercenter.service.UserCenterService; import jakarta.validation.Valid; -import java.util.List; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PutMapping; @@ -14,55 +13,50 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.List; + @RestController -@RequestMapping("/api/user-center") +@RequestMapping("/api/users") public class UserCenterController { private final UserCenterService userCenterService; /** - * 创建用户中心控制器并注入用户服务。 + * 创建用户中心控制器并注入服务。 */ public UserCenterController(UserCenterService userCenterService) { this.userCenterService = userCenterService; } /** - * 查询当前登录用户的用户中心资料。 - */ - @PreAuthorize("hasAuthority('user:profile:read')") - @GetMapping("/profile") - public ApiResponse currentProfile() { - return ApiResponse.ok(userCenterService.currentUser()); - } - - /** - * 查询用户中心中的全部用户。 + * 查询系统内所有用户,通常用于管理后台。 */ - @PreAuthorize("hasAuthority('user:center:read')") - @GetMapping("/users") + @SaCheckPermission("user:center:read") + @GetMapping public ApiResponse> listUsers() { return ApiResponse.ok(userCenterService.listUsers()); } /** - * 按用户 ID 查询单个用户详情。 + * 获取指定用户的详细信息。 */ - @PreAuthorize("hasAuthority('user:center:read')") - @GetMapping("/users/{userId}") + @SaCheckPermission("user:profile:read") + @GetMapping("/{userId}") public ApiResponse getUser(@PathVariable Long userId) { return ApiResponse.ok(userCenterService.getUser(userId)); } /** - * 更新指定用户的角色与权限集合。 + * 更新指定用户的角色与权限。 */ - @PreAuthorize("hasAuthority('user:center:manage')") - @PutMapping("/users/{userId}/authorization") + @SaCheckPermission("user:center:manage") + @PutMapping("/{userId}/authorization") public ApiResponse updateAuthorization( @PathVariable Long userId, - @Valid @RequestBody UserAuthorizationUpdateRequest request - ) { - return ApiResponse.ok("权限更新成功", userCenterService.updateAuthorization(userId, request)); + @Valid @RequestBody UserAuthorizationUpdateRequest request) { + return ApiResponse.ok( + "权限更新成功", + userCenterService.updateAuthorization(userId, request) + ); } -} +} \ No newline at end of file diff --git a/src/main/java/com/involutionhell/backend/usercenter/repository/JdbcUserAccountRepository.java b/src/main/java/com/involutionhell/backend/usercenter/repository/JdbcUserAccountRepository.java index 5feadff..5bc8f28 100644 --- a/src/main/java/com/involutionhell/backend/usercenter/repository/JdbcUserAccountRepository.java +++ b/src/main/java/com/involutionhell/backend/usercenter/repository/JdbcUserAccountRepository.java @@ -1,6 +1,8 @@ package com.involutionhell.backend.usercenter.repository; import com.involutionhell.backend.usercenter.model.UserAccount; + +import java.sql.PreparedStatement; import java.util.Arrays; import java.util.HashSet; import java.util.List; @@ -8,6 +10,8 @@ import java.util.Set; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; /** @@ -64,6 +68,39 @@ public UserAccount updateAuthorization(Long userId, Set roles, Set new IllegalArgumentException("用户不存在: " + userId)); } + @Override + public UserAccount insert(UserAccount userAccount) { + KeyHolder keyHolder = new GeneratedKeyHolder(); + String sql = "INSERT INTO user_accounts (username, password_hash, display_name, enabled, roles, permissions) " + + "VALUES (?, ?, ?, ?, ?, ?)"; + + jdbc.update(connection -> { + PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"}); + ps.setString(1, userAccount.username()); + ps.setString(2, userAccount.passwordHash()); + ps.setString(3, userAccount.displayName()); + ps.setBoolean(4, userAccount.enabled()); + ps.setString(5, joinSet(userAccount.roles())); + ps.setString(6, joinSet(userAccount.permissions())); + return ps; + }, keyHolder); + + Number key = keyHolder.getKey(); + if (key == null) { + throw new IllegalStateException("插入用户失败,无法获取生成的 ID"); + } + + return new UserAccount( + key.longValue(), + userAccount.username(), + userAccount.passwordHash(), + userAccount.displayName(), + userAccount.enabled(), + userAccount.roles(), + userAccount.permissions() + ); + } + /** * 将逗号分隔字符串解析为集合,空串返回空集合。 */ @@ -83,4 +120,4 @@ private static String joinSet(Set values) { } return String.join(",", values); } -} +} \ No newline at end of file diff --git a/src/main/java/com/involutionhell/backend/usercenter/repository/UserAccountRepository.java b/src/main/java/com/involutionhell/backend/usercenter/repository/UserAccountRepository.java index c160bf9..d04f865 100644 --- a/src/main/java/com/involutionhell/backend/usercenter/repository/UserAccountRepository.java +++ b/src/main/java/com/involutionhell/backend/usercenter/repository/UserAccountRepository.java @@ -29,4 +29,9 @@ public interface UserAccountRepository { * 更新指定用户的角色与权限,返回更新后的用户对象。 */ UserAccount updateAuthorization(Long userId, Set roles, Set permissions); -} + + /** + * 新增用户,并返回插入后的用户对象(包含生成的自增 ID)。 + */ + UserAccount insert(UserAccount userAccount); +} \ No newline at end of file diff --git a/src/main/java/com/involutionhell/backend/usercenter/service/AuthService.java b/src/main/java/com/involutionhell/backend/usercenter/service/AuthService.java index 229e38e..c103f02 100644 --- a/src/main/java/com/involutionhell/backend/usercenter/service/AuthService.java +++ b/src/main/java/com/involutionhell/backend/usercenter/service/AuthService.java @@ -1,14 +1,16 @@ package com.involutionhell.backend.usercenter.service; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.security.oauth2.jwt.Jwt; +import cn.dev33.satoken.stp.StpUtil; import com.involutionhell.backend.usercenter.dto.LoginRequest; import com.involutionhell.backend.usercenter.dto.LoginResponse; import com.involutionhell.backend.usercenter.dto.UserView; import com.involutionhell.backend.usercenter.model.UserAccount; +import me.zhyd.oauth.model.AuthUser; import org.springframework.stereotype.Service; +import java.util.Set; +import java.util.UUID; + @Service public class AuthService { @@ -24,11 +26,12 @@ public AuthService(UserCenterService userCenterService, PasswordService password } /** - * 校验登录请求。当前阶段仅演示,实际建议通过 OAuth2 流程。 + * 校验登录请求 (传统账号密码登录)。 */ public LoginResponse login(LoginRequest request) { UserAccount userAccount = userCenterService.findByUsername(request.username()) .orElseThrow(() -> new IllegalArgumentException("用户名或密码错误")); + if (!userAccount.enabled()) { throw new IllegalStateException("账号已被禁用"); } @@ -36,15 +39,63 @@ public LoginResponse login(LoginRequest request) { throw new IllegalArgumentException("用户名或密码错误"); } - // 迁移标记:原 Sa-Token 登录已移除,此处应生成基于 JWT 的 Token 或交由 OAuth2 控制。 - return new LoginResponse("Bearer", "MOCK_TOKEN_" + userAccount.id(), UserView.from(userAccount)); + return executeLogin(userAccount); + } + + /** + * 第三方 GitHub 授权登录逻辑。 + * 如果用户不存在,则自动注册并生成账号。 + */ + public LoginResponse loginByGithub(AuthUser githubUser) { + // 使用特殊的 github_ 前缀来标识这是第三方登录的用户,防止与普通用户名冲突 + String githubUsername = "github_" + githubUser.getUuid(); + + // 查找是否已经有该用户 + UserAccount userAccount = userCenterService.findByUsername(githubUsername).orElseGet(() -> { + // 如果没找到,自动注册一个新用户 + UserAccount newUser = new UserAccount( + null, // ID 由数据库自动生成 + githubUsername, + // 给第三方用户生成一个随机的超长密码,因为他们不需要用密码登录 + passwordService.encode(UUID.randomUUID().toString()), + // 优先使用 GitHub 的昵称,如果没有则使用其用户名 + githubUser.getNickname() != null ? githubUser.getNickname() : githubUser.getUsername(), + true, // 默认启用 + Set.of("USER"), // 赋予默认角色 + Set.of() // 默认权限 + ); + return userCenterService.createUser(newUser); + }); + + // 检查该用户是否已被系统管理员禁用 + if (!userAccount.enabled()) { + throw new IllegalStateException("账号已被禁用"); + } + + // 执行 Sa-Token 登录并返回信息 + return executeLogin(userAccount); + } + + /** + * 执行底层 Sa-Token 登录操作并封装返回结果。 + */ + private LoginResponse executeLogin(UserAccount userAccount) { + // 使用 Sa-Token 建立会话 + StpUtil.login(userAccount.id()); + + // 返回包含 Token 信息的响应 + return new LoginResponse( + StpUtil.getTokenName(), + StpUtil.getTokenValue(), + UserView.from(userAccount) + ); } /** * 退出当前登录会话。 */ public void logout() { - // 迁移标记:Spring Security 的退出通常通过 SecurityContextLogoutHandler 控制。 + StpUtil.logout(); } /** @@ -53,4 +104,4 @@ public void logout() { public UserView currentUser() { return userCenterService.currentUser(); } -} +} \ No newline at end of file diff --git a/src/main/java/com/involutionhell/backend/usercenter/service/UserCenterService.java b/src/main/java/com/involutionhell/backend/usercenter/service/UserCenterService.java index 2bf3990..08de9a0 100644 --- a/src/main/java/com/involutionhell/backend/usercenter/service/UserCenterService.java +++ b/src/main/java/com/involutionhell/backend/usercenter/service/UserCenterService.java @@ -1,14 +1,14 @@ package com.involutionhell.backend.usercenter.service; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.oauth2.jwt.Jwt; +import cn.dev33.satoken.stp.StpUtil; import com.involutionhell.backend.usercenter.dto.UserAuthorizationUpdateRequest; import com.involutionhell.backend.usercenter.dto.UserView; import com.involutionhell.backend.usercenter.model.UserAccount; import com.involutionhell.backend.usercenter.repository.UserAccountRepository; +import org.springframework.stereotype.Service; + import java.util.List; import java.util.Optional; -import org.springframework.stereotype.Service; @Service public class UserCenterService { @@ -28,23 +28,19 @@ public UserCenterService(UserAccountRepository userAccountRepository) { public Optional findByUsername(String username) { return userAccountRepository.findByUsername(username); } + + /** + * 新增用户。 + */ + public UserAccount createUser(UserAccount userAccount) { + return userAccountRepository.insert(userAccount); + } /** * 获取当前登录用户的信息。 */ public UserView currentUser() { - Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - Long currentUserId; - - if (principal instanceof Jwt jwt) { - currentUserId = Long.parseLong(jwt.getSubject()); - } else if (principal instanceof Long id) { - currentUserId = id; - } else { - // 这里可以扩展更多的主体解析逻辑 - throw new IllegalStateException("无法解析当前用户身份"); - } - + long currentUserId = StpUtil.getLoginIdAsLong(); return getUser(currentUserId); } @@ -77,4 +73,4 @@ public UserView updateAuthorization(Long userId, UserAuthorizationUpdateRequest ); return UserView.from(updatedAccount); } -} +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 5f4ebb5..152f3fc 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -17,10 +17,16 @@ spring.security.oauth2.client.registration.github.client-id=${AUTH_GITHUB_ID:} spring.security.oauth2.client.registration.github.client-secret=${AUTH_GITHUB_SECRET:} spring.security.oauth2.client.registration.github.scope=read:user,user:email -# JWT 配置 -jwt.secret-key=${AUTH_SECRET:involutionhell-default-secret-key-32-chars-long} +# JustAuth GitHub ???? +# ??? dummy ?????? JustAuth ????? AuthRequestFactory Bean +justauth.type.github.client-id=${AUTH_GITHUB_ID:dummy-id} +justauth.type.github.client-secret=${AUTH_GITHUB_SECRET:dummy-secret} +justauth.type.github.redirect-uri=${AUTH_URL:http://localhost:3000}/api/v1/oauth/callback/github -# Actuator 监控 +# JWT ?? (Temporarily Commented Out for JustAuth Migration) +# jwt.secret-key=${AUTH_SECRET:involutionhell-default-secret-key-32-chars-long} + +# Actuator ?? management.endpoints.web.exposure.include=${MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE:health,info,metrics} management.endpoint.health.show-details=always @@ -28,3 +34,21 @@ management.endpoint.health.show-details=always openai.api-key=${OPENAI_API_KEY:} openai.api-url=${OPENAI_API_URL:https://api.openai.com/v1} openai.model=${OPENAI_MODEL:gpt-4.1} + +# ========================================== +# Sa-Token ?? +# ========================================== +# token?? (????cookie???) +sa-token.token-name=satoken +# token??????s ??30?, -1?????? +sa-token.timeout=2592000 +# token????? (???????????token??) ??: ? +sa-token.active-timeout=-1 +# ???????????? (?true???????, ?false?????????) +sa-token.is-concurrent=true +# ?????????????????token (?true?????????token, ?false?????????token) +sa-token.is-share=true +# token?? +sa-token.token-style=uuid +# ???????? +sa-token.is-log=false