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