diff --git a/.idea/CoolRequestCommonStatePersistent.xml b/.idea/CoolRequestCommonStatePersistent.xml index 9d0591c..8cda670 100644 --- a/.idea/CoolRequestCommonStatePersistent.xml +++ b/.idea/CoolRequestCommonStatePersistent.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..518184d --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +# AIOJ - Online Judge System + +基于 Spring Boot 微服务架构的在线判题系统。 + +## 服务端口配置 + +| 服务名称 | 端口 | 说明 | +|---------|------|------| +| Gateway | 18085 | API 网关服务 | +| Auth Service | 18081 | 认证授权服务 | +| User Service | 18082 | 用户服务 | +| UPMS | 18083 | 用户权限管理服务 | +| File Service | 18066 | 文件服务 | + +## 模块结构 + +### 核心模块 (aioj-backend-common) + +- **aioj-backend-common-bom** - 依赖管理 +- **aioj-backend-common-core** - 核心工具类 +- **aioj-backend-common-feign** - Feign 客户端配置 +- **aioj-backend-common-log** - 日志框架 +- **aioj-backend-common-mybatis** - MyBatis 扩展 +- **aioj-backend-common-starter** - 自动配置启动器 + +### 服务模块 + +- **aioj-backend-gateway** - API 网关 +- **aioj-backend-auth** - 认证服务 +- **aioj-backend-user-service** - 用户服务 +- **aioj-backend-upms** - 权限管理服务 +- **aioj-backend-file-service** - 文件服务 +- **aioj-backend-judge-service** - 判题服务(开发中) +- **aioj-backend-question-service** - 题库服务(开发中) +- **aioj-backend-ai-service** - AI 服务(开发中) + +## 快速开始 + +### 构建项目 + +```bash +mvn clean compile +``` + +### 运行服务 + +```bash +# 运行网关 +mvn spring-boot:run -pl aioj-backend-gateway + +# 运行认证服务 +mvn spring-boot:run -pl aioj-backend-auth + +# 运行用户服务 +mvn spring-boot:run -pl aioj-backend-user-service +``` + +### 访问地址 + +- Gateway: http://localhost:18085 +- Auth Service: http://localhost:18081/api +- User Service: http://localhost:18082/api +- UPMS: http://localhost:18083/api +- File Service: http://localhost:18066/api + +## 常用命令 + +### 代码格式化 + +```bash +mvn spring-javaformat:apply +``` + +### 运行测试 + +```bash +mvn test +``` diff --git a/aioj-backend-auth/.flattened-pom.xml b/aioj-backend-auth/.flattened-pom.xml index dd1de8f..d61489b 100644 --- a/aioj-backend-auth/.flattened-pom.xml +++ b/aioj-backend-auth/.flattened-pom.xml @@ -37,6 +37,10 @@ cn.meowrain.aioj aioj-backend-common-mybatis + + cn.meowrain.aioj + aioj-backend-common-security + cn.hutool hutool-crypto @@ -61,24 +65,6 @@ org.springframework.boot spring-boot-starter-oauth2-client - - org.springframework.boot - spring-boot-starter-security - - - io.jsonwebtoken - jjwt-api - - - io.jsonwebtoken - jjwt-impl - runtime - - - io.jsonwebtoken - jjwt-jackson - runtime - org.springframework.cloud spring-cloud-starter-openfeign diff --git a/aioj-backend-auth/pom.xml b/aioj-backend-auth/pom.xml index 38acd83..98318ff 100644 --- a/aioj-backend-auth/pom.xml +++ b/aioj-backend-auth/pom.xml @@ -38,6 +38,10 @@ cn.meowrain.aioj aioj-backend-common-mybatis + + cn.meowrain.aioj + aioj-backend-common-security + @@ -70,26 +74,7 @@ org.springframework.boot spring-boot-starter-oauth2-client - - org.springframework.boot - spring-boot-starter-security - - - - - io.jsonwebtoken - jjwt-api - - - io.jsonwebtoken - jjwt-impl - runtime - - - io.jsonwebtoken - jjwt-jackson - runtime - + diff --git a/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/clients/UserClient.java b/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/clients/UserClient.java index d90ea60..b0e7c23 100644 --- a/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/clients/UserClient.java +++ b/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/clients/UserClient.java @@ -13,6 +13,6 @@ public interface UserClient { Result getUserByUserName(@RequestParam("userAccount") String userAccount); @GetMapping("/inner/get-by-userid") - Result getUserById(@RequestParam("userId") String userId); + Result getUserById(@RequestParam("userId") Long userId); } diff --git a/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/config/SecurityConfiguration.java b/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/config/AuthSecurityConfiguration.java similarity index 79% rename from aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/config/SecurityConfiguration.java rename to aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/config/AuthSecurityConfiguration.java index a8c787d..3b5ec64 100644 --- a/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/config/SecurityConfiguration.java +++ b/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/config/AuthSecurityConfiguration.java @@ -1,33 +1,37 @@ package cn.meowrain.aioj.backend.auth.config; -import cn.meowrain.aioj.backend.auth.filter.JwtAuthenticationFilter; +import cn.meowrain.aioj.backend.framework.security.filter.JwtAuthenticationFilter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +/** + * Auth Service Security 配置 + * 覆盖 common-security 的默认 filterChain,添加 auth 服务自定义白名单 + */ @Configuration -@EnableWebSecurity @RequiredArgsConstructor -public class SecurityConfiguration { +public class AuthSecurityConfiguration { private final JwtAuthenticationFilter jwtAuthenticationFilter; @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + public SecurityFilterChain authSecurityFilterChain(HttpSecurity http) throws Exception { http.csrf(csrf -> csrf.disable()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth + // Auth 服务自定义白名单 .requestMatchers("/v1/auth/**", "/oauth2/**", "/.well-known/**", "/doc.html", "/swagger-ui/**", - "/swagger-resources/**", "/webjars/**", "/v3/api-docs/**", "/v3/api-docs", "/favicon.ico","/v1/user/email/send-code") + "/swagger-resources/**", "/webjars/**", "/v3/api-docs/**", "/v3/api-docs", "/favicon.ico", + "/v1/user/email/send-code") .permitAll() .anyRequest() .authenticated()) diff --git a/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/controller/AuthController.java b/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/controller/AuthController.java index 8a3e1e1..a2b782c 100644 --- a/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/controller/AuthController.java +++ b/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/controller/AuthController.java @@ -56,15 +56,9 @@ public class AuthController { } @GetMapping("/getUserInfo") - public Result getUserInfo(@RequestHeader(value = "Authorization", required = false) String authorization) { - String token = null; - if(authorization != null && authorization.startsWith("Bearer ")){ - token = authorization.substring(7); - } - if(token != null && sessionService.isTokenBlacklisted(token)) { - return Results.success(null); - } - return Results.success(authService.getUserInfo(token)); + public Result getUserInfo() { + + return Results.success(authService.getUserInfo()); } } diff --git a/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/dto/resp/UserAuthRespDTO.java b/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/dto/resp/UserAuthRespDTO.java index 639d7b0..89dc26c 100644 --- a/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/dto/resp/UserAuthRespDTO.java +++ b/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/dto/resp/UserAuthRespDTO.java @@ -2,13 +2,14 @@ package cn.meowrain.aioj.backend.auth.dto.resp; import lombok.Data; +import java.io.Serializable; import java.util.Date; /** * 用户认证响应体 */ @Data -public class UserAuthRespDTO { +public class UserAuthRespDTO implements Serializable { /** * id @@ -24,7 +25,6 @@ public class UserAuthRespDTO { * 用户密码 */ private String userPassword; - /** * 开放平台id */ @@ -43,7 +43,7 @@ public class UserAuthRespDTO { /** * 用户头像 */ - private String userAvatar; + private Long userAvatar; /** * 用户简介 @@ -55,6 +55,16 @@ public class UserAuthRespDTO { */ private String userRole; + /** + * 用户邮箱 + */ + private String userEmail; + + /** + * 用户邮箱是否验证 0 未验证 1已验证 + */ + private Integer userEmailVerified; + /** * 创建时间 */ @@ -65,4 +75,8 @@ public class UserAuthRespDTO { */ private Date updateTime; + + private static final long serialVersionUID = 1L; + + } diff --git a/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/oauth2/controller/OAuth2LogoutController.java b/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/oauth2/controller/OAuth2LogoutController.java index a177b07..18cf3ca 100644 --- a/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/oauth2/controller/OAuth2LogoutController.java +++ b/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/oauth2/controller/OAuth2LogoutController.java @@ -5,7 +5,7 @@ import cn.meowrain.aioj.backend.auth.oauth2.entity.OAuth2Client; import cn.meowrain.aioj.backend.auth.oauth2.exception.OAuth2Exception; import cn.meowrain.aioj.backend.auth.oauth2.service.OAuth2ClientService; import cn.meowrain.aioj.backend.auth.oauth2.service.OAuth2SessionService; -import cn.meowrain.aioj.backend.auth.utils.JwtUtil; +import cn.meowrain.aioj.backend.framework.security.utils.JwtUtil; import io.jsonwebtoken.Claims; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; diff --git a/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/oauth2/controller/OAuth2UserInfoController.java b/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/oauth2/controller/OAuth2UserInfoController.java index 77bd3b4..275f4d6 100644 --- a/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/oauth2/controller/OAuth2UserInfoController.java +++ b/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/oauth2/controller/OAuth2UserInfoController.java @@ -5,7 +5,7 @@ import cn.meowrain.aioj.backend.auth.clients.UserClient; import cn.meowrain.aioj.backend.auth.dto.resp.UserAuthRespDTO; import cn.meowrain.aioj.backend.auth.oauth2.dto.UserInfoResponse; import cn.meowrain.aioj.backend.auth.oauth2.exception.OAuth2Exception; -import cn.meowrain.aioj.backend.auth.utils.JwtUtil; +import cn.meowrain.aioj.backend.framework.security.utils.JwtUtil; import cn.meowrain.aioj.backend.framework.core.web.Result; import io.jsonwebtoken.Claims; @@ -64,7 +64,7 @@ public class OAuth2UserInfoController { Long userId = Long.parseLong(userIdStr); // 4. 调用 user-service 获取用户信息 - Result userResult = userClient.getUserById(String.valueOf(userId)); + Result userResult = userClient.getUserById(userId); if (userResult == null || userResult.getData() == null) { log.error("获取用户信息失败: userId={}", userId); throw new OAuth2Exception("server_error", "获取用户信息失败", 500); @@ -80,7 +80,7 @@ public class OAuth2UserInfoController { .preferredUsername(user.getUserAccount()) // 用户名 .email(null) // TODO: 从用户信息中获取邮箱 .emailVerified(false) // TODO: 从用户信息中获取邮箱验证状态 - .picture(user.getUserAvatar()) // 用户头像 + .picture(null) // 用户头像 .role(user.getUserRole()) // 用户角色 .build(); diff --git a/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/oauth2/service/OAuth2SessionService.java b/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/oauth2/service/OAuth2SessionService.java index 024c5bb..3297152 100644 --- a/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/oauth2/service/OAuth2SessionService.java +++ b/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/oauth2/service/OAuth2SessionService.java @@ -4,7 +4,7 @@ import cn.hutool.core.util.RandomUtil; import cn.hutool.crypto.digest.DigestUtil; import cn.hutool.json.JSONUtil; import cn.meowrain.aioj.backend.auth.common.constants.RedisKeyConstants; -import cn.meowrain.aioj.backend.auth.utils.JwtUtil; +import cn.meowrain.aioj.backend.framework.security.utils.JwtUtil; import io.jsonwebtoken.Claims; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/oauth2/service/OAuth2TokenService.java b/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/oauth2/service/OAuth2TokenService.java index c80ede4..90803fd 100644 --- a/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/oauth2/service/OAuth2TokenService.java +++ b/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/oauth2/service/OAuth2TokenService.java @@ -5,7 +5,7 @@ import cn.meowrain.aioj.backend.auth.common.constants.RedisKeyConstants; import cn.meowrain.aioj.backend.auth.dto.resp.UserAuthRespDTO; import cn.meowrain.aioj.backend.auth.oauth2.dto.OAuth2TokenResponse; import cn.meowrain.aioj.backend.auth.oauth2.entity.OAuth2Client; -import cn.meowrain.aioj.backend.auth.utils.JwtUtil; +import cn.meowrain.aioj.backend.auth.utils.AuthServiceJwtUtil; import cn.meowrain.aioj.backend.framework.core.web.Result; import lombok.RequiredArgsConstructor; @@ -27,7 +27,7 @@ import java.util.concurrent.TimeUnit; @RequiredArgsConstructor public class OAuth2TokenService { - private final JwtUtil jwtUtil; + private final AuthServiceJwtUtil jwtUtil; private final UserClient userClient; @@ -45,7 +45,7 @@ public class OAuth2TokenService { */ public OAuth2TokenResponse generateTokenResponse(OAuth2Client client, Long userId, String scope, String nonce) { // 1. 调用 user-service 获取用户信息 - Result userResult = userClient.getUserById(String.valueOf(userId)); + Result userResult = userClient.getUserById(userId); if (userResult == null || userResult.getData() == null) { throw new RuntimeException("获取用户信息失败"); } @@ -110,7 +110,7 @@ public class OAuth2TokenService { } // 4. 调用 user-service 获取最新用户信息 - Result userResult = userClient.getUserById(String.valueOf(userId)); + Result userResult = userClient.getUserById(Long.valueOf(userId)); if (userResult == null || userResult.getData() == null) { throw new RuntimeException("获取用户信息失败"); } diff --git a/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/service/AuthService.java b/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/service/AuthService.java index 03b1864..6119d3c 100644 --- a/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/service/AuthService.java +++ b/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/service/AuthService.java @@ -31,8 +31,7 @@ public interface AuthService { /** * 根据accessToken获取用户信息 - * @param accessToken * @return {@link Result} */ - UserAuthRespDTO getUserInfo(String accessToken); + UserAuthRespDTO getUserInfo(); } diff --git a/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/service/impl/AuthServiceImpl.java b/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/service/impl/AuthServiceImpl.java index 8f21f1f..2e8068c 100644 --- a/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/service/impl/AuthServiceImpl.java +++ b/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/service/impl/AuthServiceImpl.java @@ -5,13 +5,14 @@ import cn.hutool.crypto.digest.BCrypt; import cn.meowrain.aioj.backend.auth.clients.UserClient; import cn.meowrain.aioj.backend.auth.common.constants.RedisKeyConstants; import cn.meowrain.aioj.backend.auth.common.enums.ChainMarkEnums; -import cn.meowrain.aioj.backend.auth.config.properties.JwtPropertiesConfiguration; +import cn.meowrain.aioj.backend.framework.core.utils.ContextHolderUtils; +import cn.meowrain.aioj.backend.framework.security.properties.JwtPropertiesConfiguration; import cn.meowrain.aioj.backend.auth.dto.chains.context.UserLoginRequestParamVerifyContext; import cn.meowrain.aioj.backend.auth.dto.req.UserLoginRequestDTO; import cn.meowrain.aioj.backend.auth.dto.resp.UserAuthRespDTO; import cn.meowrain.aioj.backend.auth.dto.resp.UserLoginResponseDTO; import cn.meowrain.aioj.backend.auth.service.AuthService; -import cn.meowrain.aioj.backend.auth.utils.JwtUtil; +import cn.meowrain.aioj.backend.auth.utils.AuthServiceJwtUtil; import cn.meowrain.aioj.backend.framework.core.errorcode.ErrorCode; import cn.meowrain.aioj.backend.framework.core.exception.ClientException; import cn.meowrain.aioj.backend.framework.core.exception.ServiceException; @@ -28,7 +29,7 @@ import java.util.concurrent.TimeUnit; @Slf4j public class AuthServiceImpl implements AuthService { - private final JwtUtil jwtUtil; + private final AuthServiceJwtUtil jwtUtil; private final UserLoginRequestParamVerifyContext userLoginRequestParamVerifyContext; @@ -111,7 +112,7 @@ public class AuthServiceImpl implements AuthService { // 再次签发新的 Access Token // 此处你需要查用户,拿 userName, role - Result userResult = userClient.getUserById(String.valueOf(userId)); + Result userResult = userClient.getUserById(userId); if (userResult.isFail()) { log.error("通过id查找用户失败:{}", userResult.getMessage()); throw new ServiceException(ErrorCode.SYSTEM_ERROR); @@ -156,7 +157,7 @@ public class AuthServiceImpl implements AuthService { } // 4. 验证用户是否存在(可选,增加安全性) - Result userResult = userClient.getUserById(userId); + Result userResult = userClient.getUserById(Long.valueOf(userId)); if (userResult.isFail() || userResult.getData() == null) { log.warn("User not found for id: {}", userId); return false; @@ -171,37 +172,13 @@ public class AuthServiceImpl implements AuthService { } @Override - public UserAuthRespDTO getUserInfo(String accessToken) { + public UserAuthRespDTO getUserInfo() { + Long currentUserId = ContextHolderUtils.getCurrentUserId(); - // 1. 参数校验 - if (accessToken == null || accessToken.isBlank()) { - log.warn("Access token is null or empty"); - throw new ClientException(ErrorCode.PARAMS_ERROR); - } - - // 2. token 校验 - if (!jwtUtil.isTokenValid(accessToken)) { - log.warn("Access token is invalid or expired"); - throw new ClientException(ErrorCode.NOT_LOGIN_ERROR); - } - - // 3. 解析 token - String userId; - try { - userId = jwtUtil.parseClaims(accessToken).getSubject(); - } catch (Exception e) { - log.warn("Failed to parse access token", e); - throw new ClientException(ErrorCode.NOT_LOGIN_ERROR); - } - - if (userId == null) { - throw new ClientException(ErrorCode.NOT_LOGIN_ERROR); - } - - // 4. 查询用户信息(IO 操作) + // 查询用户信息(IO 操作) Result userResult; try { - userResult = userClient.getUserById(userId); + userResult = userClient.getUserById(currentUserId); } catch (Exception e) { log.error("Failed to call user service", e); throw new ClientException(ErrorCode.SYSTEM_ERROR); diff --git a/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/utils/AuthServiceJwtUtil.java b/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/utils/AuthServiceJwtUtil.java new file mode 100644 index 0000000..63b771c --- /dev/null +++ b/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/utils/AuthServiceJwtUtil.java @@ -0,0 +1,75 @@ +package cn.meowrain.aioj.backend.auth.utils; + +import cn.meowrain.aioj.backend.auth.dto.resp.UserAuthRespDTO; +import cn.meowrain.aioj.backend.framework.security.utils.JwtUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +/** + * Auth Service 专用的 JWT 工具类 + * 包装 common-security 中的 JwtUtil,提供接受 UserAuthRespDTO 的便捷方法 + */ +@Component +@RequiredArgsConstructor +public class AuthServiceJwtUtil { + + private final JwtUtil jwtUtil; + + /** + * 生成 Access Token + * @param user 用户信息 + * @return JWT Token + */ + public String generateAccessToken(UserAuthRespDTO user) { + Map claims = new HashMap<>(); + claims.put("userId", user.getId()); + claims.put("userName", user.getUserName()); + claims.put("role", user.getUserRole()); + return jwtUtil.generateAccessToken(user.getId(), claims); + } + + /** + * 生成 Refresh Token + * @param userId 用户ID + * @return JWT Token + */ + public String generateRefreshToken(Long userId) { + return jwtUtil.generateRefreshToken(userId); + } + + /** + * 生成 OIDC ID Token + * @param user 用户信息 + * @param clientId 客户端ID(aud) + * @param nonce 防重放参数 + * @return ID Token + */ + public String generateIdToken(UserAuthRespDTO user, String clientId, String nonce) { + Map claims = new HashMap<>(); + claims.put("aud", clientId); + claims.put("name", user.getUserName()); + claims.put("preferred_username", user.getUserAccount()); + if (nonce != null) { + claims.put("nonce", nonce); + } + return jwtUtil.generateAccessToken(user.getId(), claims); + } + + /** + * 校验 Token 是否过期 + */ + public boolean isTokenValid(String token) { + return jwtUtil.isTokenValid(token); + } + + /** + * 解析 Token + */ + public io.jsonwebtoken.Claims parseClaims(String token) { + return jwtUtil.parseClaims(token); + } + +} diff --git a/aioj-backend-auth/src/main/resources/application.yml b/aioj-backend-auth/src/main/resources/application.yml index e357af3..914ff8a 100644 --- a/aioj-backend-auth/src/main/resources/application.yml +++ b/aioj-backend-auth/src/main/resources/application.yml @@ -7,7 +7,7 @@ spring: livereload: enabled: true server: - port: 10011 + port: 18081 servlet: context-path: /api springdoc: @@ -33,6 +33,7 @@ mybatis-plus: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl mapper-locations: classpath*:/mapper/**/*.xml jwt: + enabled: true secret: "12345678901234567890123456789012" # 至少32字节!! access-expire: 900000 # 24小时 refresh-expire: 604800000 # 7天 \ No newline at end of file diff --git a/aioj-backend-common/.flattened-pom.xml b/aioj-backend-common/.flattened-pom.xml index 594a3d6..93a2430 100644 --- a/aioj-backend-common/.flattened-pom.xml +++ b/aioj-backend-common/.flattened-pom.xml @@ -24,5 +24,6 @@ aioj-backend-common-mybatis aioj-backend-common-feign aioj-backend-common-starter + aioj-backend-common-security diff --git a/aioj-backend-common/aioj-backend-common-bom/pom.xml b/aioj-backend-common/aioj-backend-common-bom/pom.xml index 2a5aaa9..ac19dcd 100644 --- a/aioj-backend-common/aioj-backend-common-bom/pom.xml +++ b/aioj-backend-common/aioj-backend-common-bom/pom.xml @@ -85,6 +85,11 @@ aioj-backend-upms-biz ${revision} + + cn.meowrain.aioj + aioj-backend-common-security + ${revision} + diff --git a/aioj-backend-common/aioj-backend-common-core/src/main/java/cn/meowrain/aioj/backend/framework/core/jackson/JacksonConfiguration.java b/aioj-backend-common/aioj-backend-common-core/src/main/java/cn/meowrain/aioj/backend/framework/core/jackson/JacksonConfiguration.java new file mode 100644 index 0000000..80f6395 --- /dev/null +++ b/aioj-backend-common/aioj-backend-common-core/src/main/java/cn/meowrain/aioj/backend/framework/core/jackson/JacksonConfiguration.java @@ -0,0 +1,35 @@ +package cn.meowrain.aioj.backend.framework.core.jackson; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; + +/** + * Jackson 全局配置 + * 解决 Long 类型在前端 JavaScript 精度丢失问题 + */ +@AutoConfiguration +@ConditionalOnClass(ObjectMapper.class) +public class JacksonConfiguration { + + /** + * 自定义 Jackson ObjectMapper 配置 + * 将 Long 和 long 类型序列化为 String + */ + @Bean + public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() { + return builder -> { + // 注册 JavaTimeModule 处理时间类型 + builder.modules(new JavaTimeModule()); + + // Long 和 long 类型序列化为 String,避免前端精度丢失 + builder.serializerByType(Long.class, ToStringSerializer.instance); + builder.serializerByType(Long.TYPE, ToStringSerializer.instance); + }; + } + +} diff --git a/aioj-backend-common/aioj-backend-common-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/aioj-backend-common/aioj-backend-common-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index f9a5382..a90d490 100644 --- a/aioj-backend-common/aioj-backend-common-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/aioj-backend-common/aioj-backend-common-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,2 +1,3 @@ cn.meowrain.aioj.backend.framework.core.banner.config.AIOJBannerAutoConfiguration -cn.meowrain.aioj.backend.framework.core.config.WebAutoConfiguration \ No newline at end of file +cn.meowrain.aioj.backend.framework.core.config.WebAutoConfiguration +cn.meowrain.aioj.backend.framework.core.jackson.JacksonConfiguration \ No newline at end of file diff --git a/aioj-backend-common/aioj-backend-common-mybatis/.flattened-pom.xml b/aioj-backend-common/aioj-backend-common-mybatis/.flattened-pom.xml index 4f0d620..aa4b9e9 100644 --- a/aioj-backend-common/aioj-backend-common-mybatis/.flattened-pom.xml +++ b/aioj-backend-common/aioj-backend-common-mybatis/.flattened-pom.xml @@ -46,5 +46,15 @@ cn.meowrain.aioj aioj-backend-common-core + + com.baomidou + mybatis-plus-generator + test + + + org.freemarker + freemarker + test + diff --git a/aioj-backend-common/aioj-backend-common-security/.flattened-pom.xml b/aioj-backend-common/aioj-backend-common-security/.flattened-pom.xml new file mode 100644 index 0000000..f2e01cd --- /dev/null +++ b/aioj-backend-common/aioj-backend-common-security/.flattened-pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + cn.meowrain.aioj + aioj-backend-common + 1.0.0 + + aioj-backend-common-security + 1.0.0 + AIOJ 公共安全模块 - JWT 认证和 Spring Security 配置 + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0 + + + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-configuration-processor + true + + + io.jsonwebtoken + jjwt-api + + + io.jsonwebtoken + jjwt-impl + runtime + + + io.jsonwebtoken + jjwt-jackson + runtime + + + org.projectlombok + lombok + provided + + + cn.meowrain.aioj + aioj-backend-common-core + + + diff --git a/aioj-backend-common/aioj-backend-common-security/pom.xml b/aioj-backend-common/aioj-backend-common-security/pom.xml new file mode 100644 index 0000000..715fdd2 --- /dev/null +++ b/aioj-backend-common/aioj-backend-common-security/pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + + + cn.meowrain.aioj + aioj-backend-common + ${revision} + + + aioj-backend-common-security + jar + AIOJ 公共安全模块 - JWT 认证和 Spring Security 配置 + + + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + io.jsonwebtoken + jjwt-api + + + io.jsonwebtoken + jjwt-impl + runtime + + + io.jsonwebtoken + jjwt-jackson + runtime + + + + + org.projectlombok + lombok + provided + + + + + cn.meowrain.aioj + aioj-backend-common-core + + + diff --git a/aioj-backend-common/aioj-backend-common-security/src/main/java/cn/meowrain/aioj/backend/framework/security/autoconfigure/SecurityAutoConfiguration.java b/aioj-backend-common/aioj-backend-common-security/src/main/java/cn/meowrain/aioj/backend/framework/security/autoconfigure/SecurityAutoConfiguration.java new file mode 100644 index 0000000..249f0c5 --- /dev/null +++ b/aioj-backend-common/aioj-backend-common-security/src/main/java/cn/meowrain/aioj/backend/framework/security/autoconfigure/SecurityAutoConfiguration.java @@ -0,0 +1,44 @@ +package cn.meowrain.aioj.backend.framework.security.autoconfigure; + +import cn.meowrain.aioj.backend.framework.security.filter.JwtAuthenticationFilter; +import cn.meowrain.aioj.backend.framework.security.properties.JwtPropertiesConfiguration; +import cn.meowrain.aioj.backend.framework.security.utils.JwtUtil; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * Spring Security 自动配置类 + * + * 引入 aioj-backend-common-security 依赖后,会自动启用: + * - JWT 认证过滤器 + * - Spring Security 基本配置 + * + * 配置项(application.yml): + * jwt: + * enabled: true # 是否启用 JWT 认证(默认 true) + * secret: your-secret # JWT 密钥 + * access-expire: 7200000 # Access Token 过期时间 + * refresh-expire: 604800000 # Refresh Token 过期时间 + */ +@AutoConfiguration +@EnableConfigurationProperties(JwtPropertiesConfiguration.class) +public class SecurityAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = "jwt", name = "enabled", havingValue = "true", matchIfMissing = true) + public JwtUtil jwtUtil(JwtPropertiesConfiguration jwtConfig) { + return new JwtUtil(jwtConfig); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = "jwt", name = "enabled", havingValue = "true", matchIfMissing = true) + public JwtAuthenticationFilter jwtAuthenticationFilter(JwtUtil jwtUtil) { + return new JwtAuthenticationFilter(jwtUtil); + } + +} diff --git a/aioj-backend-common/aioj-backend-common-security/src/main/java/cn/meowrain/aioj/backend/framework/security/config/SecurityConfiguration.java b/aioj-backend-common/aioj-backend-common-security/src/main/java/cn/meowrain/aioj/backend/framework/security/config/SecurityConfiguration.java new file mode 100644 index 0000000..d11cfeb --- /dev/null +++ b/aioj-backend-common/aioj-backend-common-security/src/main/java/cn/meowrain/aioj/backend/framework/security/config/SecurityConfiguration.java @@ -0,0 +1,76 @@ +package cn.meowrain.aioj.backend.framework.security.config; + +import cn.meowrain.aioj.backend.framework.security.autoconfigure.SecurityAutoConfiguration; +import cn.meowrain.aioj.backend.framework.security.filter.JwtAuthenticationFilter; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.List; + + +@AutoConfiguration(after = SecurityAutoConfiguration.class) +@EnableWebSecurity +@RequiredArgsConstructor +@ConditionalOnProperty(prefix = "jwt", name = "enabled", havingValue = "true", matchIfMissing = true) +@ConditionalOnBean(JwtAuthenticationFilter.class) +public class SecurityConfiguration { + + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + @Bean + @ConditionalOnMissingBean(SecurityFilterChain.class) + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.csrf(csrf -> csrf.disable()) + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + // Swagger 文档路径 + .requestMatchers("/doc.html", "/swagger-ui/**", "/swagger-resources/**", "/webjars/**", + "/v3/api-docs/**", "/v3/api-docs", "/favicon.ico") + .permitAll() + // 用户注册、发送验证码等公开接口 + .requestMatchers("/v1/user/register", "/v1/user/email/send-code", + "/api/v1/user/register", "/api/v1/user/email/send-code") + .permitAll() + // 文件访问接口(公开,用于访问图片等静态资源) + .requestMatchers("/file/**", "/api/file/**") + .permitAll() + // 内部服务调用路径(Feign)- 使用具体路径匹配 + .requestMatchers("/v1/user/inner/**", "/v1/*/inner/**", + "/api/v1/user/inner/**", "/api/v1/*/inner/**") + .permitAll() + // 其他请求需要认证(可由子类覆盖此配置) + .anyRequest() + .authenticated()) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + return http.build(); + } + + @Bean + @ConditionalOnMissingBean(CorsConfigurationSource.class) + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOriginPatterns(List.of("*")); + configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")); + configuration.setAllowedHeaders(List.of("*")); + configuration.setAllowCredentials(true); + configuration.setMaxAge(3600L); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + +} diff --git a/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/filter/JwtAuthenticationFilter.java b/aioj-backend-common/aioj-backend-common-security/src/main/java/cn/meowrain/aioj/backend/framework/security/filter/JwtAuthenticationFilter.java similarity index 77% rename from aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/filter/JwtAuthenticationFilter.java rename to aioj-backend-common/aioj-backend-common-security/src/main/java/cn/meowrain/aioj/backend/framework/security/filter/JwtAuthenticationFilter.java index 91d67b8..8a78081 100644 --- a/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/filter/JwtAuthenticationFilter.java +++ b/aioj-backend-common/aioj-backend-common-security/src/main/java/cn/meowrain/aioj/backend/framework/security/filter/JwtAuthenticationFilter.java @@ -1,7 +1,6 @@ -package cn.meowrain.aioj.backend.auth.filter; +package cn.meowrain.aioj.backend.framework.security.filter; -import cn.meowrain.aioj.backend.auth.service.AuthService; -import cn.meowrain.aioj.backend.auth.utils.JwtUtil; +import cn.meowrain.aioj.backend.framework.security.utils.JwtUtil; import io.jsonwebtoken.Claims; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -13,7 +12,6 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; @@ -22,17 +20,15 @@ import java.util.Collections; import java.util.List; /** - * JWT认证过滤器 拦截所有请求,验证JWT Token + * JWT 认证过滤器 + * 拦截所有请求,验证 JWT Token 并设置 SecurityContext */ -@Component @RequiredArgsConstructor @Slf4j public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; - private final AuthService authService; - private static final String TOKEN_PREFIX = "Bearer "; private static final String HEADER_NAME = "Authorization"; @@ -64,7 +60,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { } /** - * 从请求中提取JWT Token + * 从请求中提取 JWT Token */ private String extractTokenFromRequest(HttpServletRequest request) { String bearerToken = request.getHeader(HEADER_NAME); @@ -75,18 +71,17 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { } /** - * 根据JWT Claims创建Authentication对象 + * 根据 JWT Claims 创建 Authentication 对象 */ private Authentication createAuthentication(Claims claims) { String userId = claims.getSubject(); - String userName = claims.get("userName", String.class); String role = claims.get("role", String.class); // 创建权限列表 List authorities = Collections .singletonList(new SimpleGrantedAuthority("ROLE_" + (role != null ? role : "USER"))); - // 创建认证对象 + // 创建认证对象(principal 为 userId 字符串) UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userId, null, authorities); @@ -96,11 +91,9 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { @Override protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { String path = request.getRequestURI(); - // 跳过不需要JWT验证的路径 - return path.startsWith("/v1/auth/") || path.startsWith("/doc.html") || path.startsWith("/swagger-ui/") - || path.startsWith("/swagger-resources/") || path.startsWith("/webjars/") - || path.startsWith("/v3/api-docs") || path.equals("/favicon.ico") - || path.contains("/v3/api-docs"); + // 跳过不需要 JWT 验证的路径 + // 子类可以通过重写此方法来自定义白名单 + return path.startsWith("/v3/api-docs") || path.equals("/favicon.ico") || path.contains("/swagger"); } } diff --git a/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/config/properties/JwtPropertiesConfiguration.java b/aioj-backend-common/aioj-backend-common-security/src/main/java/cn/meowrain/aioj/backend/framework/security/properties/JwtPropertiesConfiguration.java similarity index 81% rename from aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/config/properties/JwtPropertiesConfiguration.java rename to aioj-backend-common/aioj-backend-common-security/src/main/java/cn/meowrain/aioj/backend/framework/security/properties/JwtPropertiesConfiguration.java index dcc5692..0bb12a6 100644 --- a/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/config/properties/JwtPropertiesConfiguration.java +++ b/aioj-backend-common/aioj-backend-common-security/src/main/java/cn/meowrain/aioj/backend/framework/security/properties/JwtPropertiesConfiguration.java @@ -1,16 +1,18 @@ -package cn.meowrain.aioj.backend.auth.config.properties; +package cn.meowrain.aioj.backend.framework.security.properties; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; -@Component @Data @ConfigurationProperties(value = JwtPropertiesConfiguration.PREFIX) public class JwtPropertiesConfiguration { public static final String PREFIX = "jwt"; + /*** + * 开启 + */ + private Boolean enabled; /** * JWT 密钥(必须 32 字节以上) */ diff --git a/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/utils/JwtUtil.java b/aioj-backend-common/aioj-backend-common-security/src/main/java/cn/meowrain/aioj/backend/framework/security/utils/JwtUtil.java similarity index 53% rename from aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/utils/JwtUtil.java rename to aioj-backend-common/aioj-backend-common-security/src/main/java/cn/meowrain/aioj/backend/framework/security/utils/JwtUtil.java index db451d6..ab3ea18 100644 --- a/aioj-backend-auth/src/main/java/cn/meowrain/aioj/backend/auth/utils/JwtUtil.java +++ b/aioj-backend-common/aioj-backend-common-security/src/main/java/cn/meowrain/aioj/backend/framework/security/utils/JwtUtil.java @@ -1,7 +1,6 @@ -package cn.meowrain.aioj.backend.auth.utils; +package cn.meowrain.aioj.backend.framework.security.utils; -import cn.meowrain.aioj.backend.auth.config.properties.JwtPropertiesConfiguration; -import cn.meowrain.aioj.backend.auth.dto.resp.UserAuthRespDTO; +import cn.meowrain.aioj.backend.framework.security.properties.JwtPropertiesConfiguration; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; @@ -14,7 +13,6 @@ import java.util.HashMap; import java.util.Map; @RequiredArgsConstructor -@Component public class JwtUtil { private final JwtPropertiesConfiguration jwtConfig; @@ -23,17 +21,21 @@ public class JwtUtil { return Keys.hmacShaKeyFor(jwtConfig.getSecret().getBytes()); } - /** 生成 Access Token */ - public String generateAccessToken(UserAuthRespDTO user) { + /** + * 生成 Access Token + * @param userId 用户ID + * @param additionalClaims 额外的声明(如 userName, role 等) + */ + public String generateAccessToken(Long userId, Map additionalClaims) { long now = System.currentTimeMillis(); Map claims = new HashMap<>(); - claims.put("userId", user.getId()); - claims.put("userName", user.getUserName()); - claims.put("role", user.getUserRole()); + if (additionalClaims != null) { + claims.putAll(additionalClaims); + } return Jwts.builder() - .subject(String.valueOf(user.getId())) + .subject(String.valueOf(userId)) .issuedAt(new Date(now)) .expiration(new Date(now + jwtConfig.getAccessExpire())) .claims(claims) @@ -70,33 +72,11 @@ public class JwtUtil { } /** - * 生成 OIDC ID Token - * @param user 用户信息 - * @param clientId 客户端ID(aud) - * @param nonce 防重放参数 - * @return ID Token + * 从 Token 中获取用户ID */ - public String generateIdToken(UserAuthRespDTO user, String clientId, String nonce) { - long now = System.currentTimeMillis(); - - Map claims = new HashMap<>(); - claims.put("sub", String.valueOf(user.getId())); // Subject - 用户ID - claims.put("aud", clientId); // Audience - 客户端ID - claims.put("name", user.getUserName()); - claims.put("preferred_username", user.getUserAccount()); - - if (nonce != null) { - claims.put("nonce", nonce); // 防重放 - } - - return Jwts.builder() - .issuer("http://localhost:10011/api") // TODO: 从配置读取 - .subject(String.valueOf(user.getId())) - .issuedAt(new Date(now)) - .expiration(new Date(now + jwtConfig.getAccessExpire())) - .claims(claims) - .signWith(getSigningKey(), Jwts.SIG.HS256) - .compact(); + public Long getUserIdFromToken(String token) { + Claims claims = parseClaims(token); + return Long.parseLong(claims.getSubject()); } } diff --git a/aioj-backend-common/aioj-backend-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/aioj-backend-common/aioj-backend-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..473852e --- /dev/null +++ b/aioj-backend-common/aioj-backend-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +cn.meowrain.aioj.backend.framework.security.autoconfigure.SecurityAutoConfiguration +cn.meowrain.aioj.backend.framework.security.config.SecurityConfiguration diff --git a/aioj-backend-common/pom.xml b/aioj-backend-common/pom.xml index e918cef..b9f1e70 100644 --- a/aioj-backend-common/pom.xml +++ b/aioj-backend-common/pom.xml @@ -21,5 +21,6 @@ aioj-backend-common-mybatis aioj-backend-common-feign aioj-backend-common-starter + aioj-backend-common-security \ No newline at end of file diff --git a/aioj-backend-file-service/.flattened-pom.xml b/aioj-backend-file-service/.flattened-pom.xml index 202478e..822ab58 100644 --- a/aioj-backend-file-service/.flattened-pom.xml +++ b/aioj-backend-file-service/.flattened-pom.xml @@ -69,5 +69,13 @@ spring-boot-starter-test test + + org.springframework.boot + spring-boot-starter-actuator + + + cn.hutool + hutool-crypto + diff --git a/aioj-backend-file-service/src/main/java/cn/meowrain/aioj/backend/fileservice/controller/AttachmentController.java b/aioj-backend-file-service/src/main/java/cn/meowrain/aioj/backend/fileservice/controller/AttachmentController.java index 3f85504..1e50b32 100644 --- a/aioj-backend-file-service/src/main/java/cn/meowrain/aioj/backend/fileservice/controller/AttachmentController.java +++ b/aioj-backend-file-service/src/main/java/cn/meowrain/aioj/backend/fileservice/controller/AttachmentController.java @@ -8,7 +8,9 @@ import cn.meowrain.aioj.backend.framework.core.web.Results; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -28,16 +30,20 @@ import java.util.List; public class AttachmentController { private final AttachmentService attachmentService; + @Operation(summary = "通用文件上传组件") @PostMapping("/upload") - public Result uploadFile(@RequestParam("file") MultipartFile file, + public Result uploadFile(@Parameter(description = "上传的文件", + content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA_VALUE)) + @RequestParam("file") MultipartFile file, + @Parameter(description = "文件哈希值") @RequestParam(value = "hash", required = false) String hash) { return Results.success(attachmentService.upload(file, hash)); } @Operation(summary = "哈希是否存在") @GetMapping("/check") - public Result checkHash(@RequestParam("hash") String hash) { + public Result checkHash(@Parameter(description = "文件哈希值") @RequestParam("hash") String hash) { return Results.success(attachmentService.checkHash(hash)); } diff --git a/aioj-backend-file-service/src/main/java/cn/meowrain/aioj/backend/fileservice/dao/entity/AttachmentDO.java b/aioj-backend-file-service/src/main/java/cn/meowrain/aioj/backend/fileservice/dao/entity/AttachmentDO.java index 751ee70..536365e 100644 --- a/aioj-backend-file-service/src/main/java/cn/meowrain/aioj/backend/fileservice/dao/entity/AttachmentDO.java +++ b/aioj-backend-file-service/src/main/java/cn/meowrain/aioj/backend/fileservice/dao/entity/AttachmentDO.java @@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import lombok.Getter; import lombok.Setter; import lombok.ToString; diff --git a/aioj-backend-file-service/src/main/resources/application.yml b/aioj-backend-file-service/src/main/resources/application.yml index 69dd91c..9860021 100644 --- a/aioj-backend-file-service/src/main/resources/application.yml +++ b/aioj-backend-file-service/src/main/resources/application.yml @@ -8,7 +8,7 @@ spring: max-file-size: 500MB max-request-size: 500MB server: - port: 10013 + port: 18066 servlet: context-path: /api error: diff --git a/aioj-backend-gateway/src/main/resources/application-dev.yml b/aioj-backend-gateway/src/main/resources/application-dev.yml index c33a28a..045b400 100644 --- a/aioj-backend-gateway/src/main/resources/application-dev.yml +++ b/aioj-backend-gateway/src/main/resources/application-dev.yml @@ -42,6 +42,8 @@ spring: aioj-backend-gateway: # 白名单配置 white-list: + - /api/v1/user/email/send-code + - /api/file/** - /api/v1/auth/login - /api/v1/auth/register - /api/v1/auth/refresh @@ -90,3 +92,8 @@ knife4j: url: /user-service/api/v3/api-docs context-path: /user-service order: 2 + - name: 文件服务 + service-name: file-service + url: /file-service/api/v3/api-docs + context-path: /file-service + order: 2 \ No newline at end of file diff --git a/aioj-backend-gateway/src/main/resources/application.yml b/aioj-backend-gateway/src/main/resources/application.yml index ec983ee..78bdf50 100644 --- a/aioj-backend-gateway/src/main/resources/application.yml +++ b/aioj-backend-gateway/src/main/resources/application.yml @@ -1,5 +1,5 @@ server: - port: 8085 + port: 18085 error: include-stacktrace: never @@ -25,6 +25,13 @@ spring: - Path=/user-service/** filters: - StripPrefix=1 + # auth服务 Swagger 文档路由 + - id: file-service-doc + uri: lb://file-service + predicates: + - Path=/file-service/** + filters: + - StripPrefix=1 # auth业务接口 - id: auth-service uri: lb://auth-service @@ -50,6 +57,23 @@ spring: backoff: firstBackoff: 50ms maxBackoff: 500ms + - id: file-service + uri: lb://file-service + predicates: + - Path=/api/v1/file/** + filters: + - name: Retry + args: + retries: 3 + statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE + backoff: + firstBackoff: 50ms + maxBackoff: 500ms + # 文件访问路由(公开,直接转发不去前缀) + - id: file-access + uri: lb://file-service + predicates: + - Path=/api/file/** # 设置应用启动后的就绪探针 lifecycle: timeout-per-shutdown-phase: 30s diff --git a/aioj-backend-upms/aioj-backend-upms-biz/src/main/resources/application.yml b/aioj-backend-upms/aioj-backend-upms-biz/src/main/resources/application.yml index 7f5b6b5..4435b6e 100644 --- a/aioj-backend-upms/aioj-backend-upms-biz/src/main/resources/application.yml +++ b/aioj-backend-upms/aioj-backend-upms-biz/src/main/resources/application.yml @@ -4,7 +4,7 @@ spring: profiles: active: @env@ server: - port: 10012 + port: 18083 servlet: context-path: /api springdoc: diff --git a/aioj-backend-user-service/.flattened-pom.xml b/aioj-backend-user-service/.flattened-pom.xml index ff9ff70..1777c2e 100644 --- a/aioj-backend-user-service/.flattened-pom.xml +++ b/aioj-backend-user-service/.flattened-pom.xml @@ -37,6 +37,10 @@ cn.meowrain.aioj aioj-backend-common-mybatis + + cn.meowrain.aioj + aioj-backend-common-security + org.springframework.boot spring-boot-starter-web diff --git a/aioj-backend-user-service/pom.xml b/aioj-backend-user-service/pom.xml index 490a73f..059a541 100644 --- a/aioj-backend-user-service/pom.xml +++ b/aioj-backend-user-service/pom.xml @@ -38,6 +38,10 @@ cn.meowrain.aioj aioj-backend-common-mybatis + + cn.meowrain.aioj + aioj-backend-common-security + diff --git a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/controller/UserController.java b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/controller/UserController.java index 203ee3a..958f77b 100644 --- a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/controller/UserController.java +++ b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/controller/UserController.java @@ -3,6 +3,7 @@ package cn.meowrain.aioj.backend.userservice.controller; import cn.meowrain.aioj.backend.framework.core.utils.ContextHolderUtils; import cn.meowrain.aioj.backend.framework.core.web.Result; import cn.meowrain.aioj.backend.framework.core.web.Results; +import cn.meowrain.aioj.backend.userservice.dto.req.AvatarUpdateRequestDTO; import cn.meowrain.aioj.backend.userservice.dto.req.BindEmailRequest; import cn.meowrain.aioj.backend.userservice.dto.req.EmailSendCodeRequestDTO; import cn.meowrain.aioj.backend.userservice.dto.req.UserRegisterRequestDTO; @@ -47,7 +48,7 @@ public class UserController { @Operation(summary = "根据用户ID查询用户", description = "通过用户ID获取用户认证信息(内部接口)") public Result getUserById( @Parameter(description = "用户ID", required = true, example = "1") - @RequestParam("userId") String userid) { + @RequestParam("userId") Long userid) { UserAuthRespDTO userAuthRespDTO = userService.findAuthInfoByUserId(userid); return Results.success(userAuthRespDTO); } @@ -96,19 +97,20 @@ public class UserController { return Results.success(); } - @Operation(summary = "个人资料管理", description = "更新个人资料") + @Operation(summary = "个人资料管理-更新个人资料", description = "更新个人资料") @PutMapping("/profile") public Result updateUserProfile() { return Results.success(); } - @Operation(summary = "个人资料管理", description = "上传/更新头像") - @PostMapping("/avatar") - public Result uploadAvatar(@RequestParam("file") MultipartFile file) { + @Operation(summary = "个人资料管理-上传/更新头像", description = "上传/更新头像") + @PutMapping("/avatar") + public Result setProfileAvatar(@Parameter(description = "文件id", required = true) @RequestBody AvatarUpdateRequestDTO dto) { + userService.setProfileAvatar(dto.getFileId()); return Results.success(); } - @Operation(summary = "个人资料管理",description = "修改密码") + @Operation(summary = "个人资料管理-修改密码",description = "修改密码") public Result changePassword() { return Results.success(); } diff --git a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dao/entity/User.java b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dao/entity/User.java index f8783e1..0104ba1 100644 --- a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dao/entity/User.java +++ b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dao/entity/User.java @@ -46,7 +46,7 @@ public class User implements Serializable { /** * 用户头像 */ - private String userAvatar; + private Long userAvatar; /** * 用户简介 diff --git a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/req/AvatarUpdateRequestDTO.java b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/req/AvatarUpdateRequestDTO.java new file mode 100644 index 0000000..8d35eee --- /dev/null +++ b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/req/AvatarUpdateRequestDTO.java @@ -0,0 +1,8 @@ +package cn.meowrain.aioj.backend.userservice.dto.req; + +import lombok.Data; + +@Data +public class AvatarUpdateRequestDTO { + private Long fileId; +} diff --git a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/resp/UserAuthRespDTO.java b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/resp/UserAuthRespDTO.java index b855727..5c09be4 100644 --- a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/resp/UserAuthRespDTO.java +++ b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/resp/UserAuthRespDTO.java @@ -3,6 +3,7 @@ package cn.meowrain.aioj.backend.userservice.dto.resp; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import java.io.Serializable; import java.util.Date; /** @@ -10,84 +11,72 @@ import java.util.Date; */ @Data @Schema(description = "用户认证响应体") -public class UserAuthRespDTO { +public class UserAuthRespDTO implements Serializable { - /** - * id - */ - @Schema(description = "用户ID", example = "1") - private Long id; + /** + * id + */ + private Long id; - /** - * 用户账号 - */ - @Schema(description = "用户账号", example = "admin") - private String userAccount; + /** + * 用户账号 + */ + private String userAccount; + /** + * 用户密码 + */ + private String userPassword; + /** + * 开放平台id + */ + private String unionId; - /** - * 用户密码 - */ - @Schema(description = "用户密码", example = "123456") - private String userPassword; + /** + * 公众号openId + */ + private String mpOpenId; - /** - * 开放平台id - */ - @Schema(description = "开放平台ID", example = "wx_union_id_123") - private String unionId; + /** + * 用户昵称 + */ + private String userName; - /** - * 公众号openId - */ - @Schema(description = "公众号OpenID", example = "wx_openid_123") - private String mpOpenId; + /** + * 用户头像 + */ + private Long userAvatar; - /** - * 用户昵称 - */ - @Schema(description = "用户昵称", example = "张三") - private String userName; + /** + * 用户简介 + */ + private String userProfile; - /** - * 用户头像 - */ - @Schema(description = "用户头像URL", example = "https://example.com/avatar.jpg") - private String userAvatar; + /** + * 用户角色:user/admin/ban + */ + private String userRole; - /** - * 用户简介 - */ - @Schema(description = "用户简介", example = "这是我的个人简介") - private String userProfile; + /** + * 用户邮箱 + */ + private String userEmail; - /** - * 用户角色:user/admin/ban - */ - @Schema(description = "用户角色", example = "user", allowableValues = {"user", "admin", "ban"}) - private String userRole; + /** + * 用户邮箱是否验证 0 未验证 1已验证 + */ + private Integer userEmailVerified; - /** - * 用户邮箱 - */ - @Schema(description = "用户邮箱", example = "user@example.com") - private String userEmail; + /** + * 创建时间 + */ + private Date createTime; - /** - * 用户邮箱是否验证 0 未验证 1已验证 - */ - @Schema(description = "用户邮箱是否验证", example = "1", allowableValues = {"0", "1"}) - private Integer userEmailVerified; + /** + * 更新时间 + */ + private Date updateTime; - /** - * 创建时间 - */ - @Schema(description = "创建时间", example = "2025-01-01T00:00:00") - private Date createTime; - /** - * 更新时间 - */ - @Schema(description = "更新时间", example = "2025-01-01T12:00:00") - private Date updateTime; + private static final long serialVersionUID = 1L; } diff --git a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/resp/UserLoginResponseDTO.java b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/resp/UserLoginResponseDTO.java deleted file mode 100644 index 5dd4266..0000000 --- a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/dto/resp/UserLoginResponseDTO.java +++ /dev/null @@ -1,76 +0,0 @@ -package cn.meowrain.aioj.backend.userservice.dto.resp; - -import com.baomidou.mybatisplus.annotation.*; -import lombok.Data; - -import java.io.Serializable; -import java.util.Date; - -@Data -public class UserLoginResponseDTO implements Serializable { - - /** - * id - */ - @TableId(type = IdType.ASSIGN_ID) - private Long id; - - /** - * 用户账号 - */ - private String userAccount; - - /** - * 开放平台id - */ - private String unionId; - - /** - * 公众号openId - */ - private String mpOpenId; - - /** - * 用户昵称 - */ - private String userName; - - /** - * 用户头像 - */ - private String userAvatar; - - /** - * 用户简介 - */ - private String userProfile; - - /** - * 用户角色:user/admin/ban - */ - private String userRole; - - /** - * 创建时间 - */ - private Date createTime; - - /** - * 更新时间 - */ - private Date updateTime; - - /** - * 是否删除 - */ - - private Integer isDelete; - - /** - * JWT令牌(登录成功返回) - */ - private String token; - - private static final long serialVersionUID = 1L; - -} diff --git a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/service/UserService.java b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/service/UserService.java index 6920063..84ec10a 100644 --- a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/service/UserService.java +++ b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/service/UserService.java @@ -26,7 +26,7 @@ public interface UserService extends IService { /** * 根据用户id查找用户认证信息 */ - UserAuthRespDTO findAuthInfoByUserId(String userId); + UserAuthRespDTO findAuthInfoByUserId(Long userId); /** @@ -43,4 +43,11 @@ public interface UserService extends IService { * 解绑邮箱 */ void unbindEmail(Long userId); + + /** + * 设置用户头像 + * @param fileId + * @return + */ + void setProfileAvatar(Long fileId); } diff --git a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/service/impl/UserServiceImpl.java b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/service/impl/UserServiceImpl.java index 9b96c92..4d963c2 100644 --- a/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/service/impl/UserServiceImpl.java +++ b/aioj-backend-user-service/src/main/java/cn/meowrain/aioj/backend/userservice/service/impl/UserServiceImpl.java @@ -20,6 +20,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.Date; @@ -75,7 +76,7 @@ public class UserServiceImpl extends ServiceImpl implements Us } @Override - public UserAuthRespDTO findAuthInfoByUserId(String userId) { + public UserAuthRespDTO findAuthInfoByUserId(Long userId) { User one = this.lambdaQuery().eq(User::getId, userId).one(); UserAuthRespDTO userAuthDTO = new UserAuthRespDTO(); if (one != null) { @@ -100,7 +101,18 @@ public class UserServiceImpl extends ServiceImpl implements Us @Override public void unbindEmail(Long userId) { + User one = this.lambdaQuery().eq(User::getId, userId).one(); } + @Transactional(rollbackFor = Exception.class) + @Override + public void setProfileAvatar(Long fileId) { + Long currentUserId = ContextHolderUtils.getCurrentUserId(); + User user = this.lambdaQuery().eq(User::getId, currentUserId).one(); + user.setUserAvatar(fileId); + user.setUpdateTime(new Date()); + this.updateById(user); + } + } diff --git a/aioj-backend-user-service/src/main/resources/application.yml b/aioj-backend-user-service/src/main/resources/application.yml index 16741ee..acad1cb 100644 --- a/aioj-backend-user-service/src/main/resources/application.yml +++ b/aioj-backend-user-service/src/main/resources/application.yml @@ -4,7 +4,7 @@ spring: profiles: active: @env@ server: - port: 10010 + port: 18082 servlet: context-path: /api error: @@ -31,6 +31,14 @@ mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl mapper-locations: classpath*:/mapper/**/*.xml + +# JWT 配置(必须与 auth-service 保持一致) +jwt: + enabled: true + secret: "12345678901234567890123456789012" # 至少32字节 + access-expire: 900000 # 15分钟 + refresh-expire: 604800000 # 7天 + aioj: log: enabled: true