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