From 0b508db2faa395fdba9896b0c92773d46b01ba95 Mon Sep 17 00:00:00 2001
From: kkk <15475386+kstt@user.noreply.gitee.com>
Date: Tue, 9 Sep 2025 17:09:12 +0800
Subject: [PATCH 1/8] md
---
afd.md | 1 +
1 file changed, 1 insertion(+)
create mode 100644 afd.md
diff --git a/afd.md b/afd.md
new file mode 100644
index 0000000..4bc45af
--- /dev/null
+++ b/afd.md
@@ -0,0 +1 @@
+fsdafsdfsdf
\ No newline at end of file
--
Gitee
From 005127770645d02983698d7d64796c6537b8645b Mon Sep 17 00:00:00 2001
From: kkk <15475386+kstt@user.noreply.gitee.com>
Date: Fri, 12 Sep 2025 13:58:36 +0800
Subject: [PATCH 2/8] =?UTF-8?q?chore(qa-service=20applicationy.aml):=20?=
=?UTF-8?q?=E6=95=B4=E5=90=88=E7=BB=9F=E4=B8=80=E5=AD=97=E6=AE=B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
将nacos中的ip端口统一用某一字段管理并添加了jwt令牌的密钥和时间错
---
.../src/main/resources/application.properties | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/user-service/user-service-bootstrap/src/main/resources/application.properties b/user-service/user-service-bootstrap/src/main/resources/application.properties
index e21f2d5..9b6b30e 100644
--- a/user-service/user-service-bootstrap/src/main/resources/application.properties
+++ b/user-service/user-service-bootstrap/src/main/resources/application.properties
@@ -1,13 +1,18 @@
server.port=28080
spring.application.name=user-service
+jwt.secret-key=5a2d9f8e4c1b3a0d7e5f6c2a9b8d4e1f5a2d9f8e4c1b3a0d7e5f6c2a9b8d4e1f
+jwt.expiration-time=300000
+nacos.server.addr=192.168.168.128
+nacos.server.addr.port=8848
+
# Nacos认证信息
spring.cloud.nacos.discovery.username=nacos
spring.cloud.nacos.discovery.password=nacos
# Nacos 服务发现与注册配置,其中子属性 server-addr 指定 Nacos 服务器主机和端口
-spring.cloud.nacos.discovery.server-addr=192.168.168.128:8848
+spring.cloud.nacos.discovery.server-addr=${nacos.server.addr}:${nacos.server.addr.port}
# 注册到 nacos 的指定 namespace,默认为 public
spring.cloud.nacos.discovery.namespace=public
@@ -17,7 +22,7 @@ spring.cloud.nacos.config.username=nacos
spring.cloud.nacos.config.password=nacos
spring.cloud.nacos.config.contextPath=/nacos
# 设置配置中心服务端地址
-spring.cloud.nacos.config.server-addr=192.168.168.128:8848
+spring.cloud.nacos.config.server-addr=${nacos.server.addr}:${nacos.server.addr.port}
# Nacos 配置中心的namespace。需要注意,如果使用 public 的 namcespace ,请不要填写这个值,直接留空即可
# spring.cloud.nacos.config.namespace=
spring.config.import=nacos:${spring.application.name}.properties?refresh=true
--
Gitee
From 010bf75e05bd998f6c72092e84c8556afab6eb0c Mon Sep 17 00:00:00 2001
From: kkk <15475386+kstt@user.noreply.gitee.com>
Date: Fri, 12 Sep 2025 14:00:06 +0800
Subject: [PATCH 3/8] =?UTF-8?q?feat(qa-service-in=20CreateUserRequestDto):?=
=?UTF-8?q?=20=E6=B7=BB=E5=8A=A0=E4=BA=86=E5=AF=86=E7=A0=81=E6=A0=A1?=
=?UTF-8?q?=E9=AA=8C=E8=A7=84=E5=88=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
添加了密码校验规则
---
.../in/web/dto/CreateUserRequestDTO.java | 31 +++++++++++++++++++
1 file changed, 31 insertions(+)
diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/CreateUserRequestDTO.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/CreateUserRequestDTO.java
index 9ebf615..f63eb38 100644
--- a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/CreateUserRequestDTO.java
+++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/CreateUserRequestDTO.java
@@ -7,4 +7,35 @@ public record CreateUserRequestDTO(
String password,
String rePassword) {
// TODO: 密码校验
+ public CreateUserRequestDTO{
+ /*chengguoqing添加了校验规则1
+ */
+ if (password == null || password.length() < 6|| password.length() >22) {
+ throw new RuntimeException("密码长度6-22");
+ }
+// xuyihua添加了校验规则2
+ // 至少包含一个数字
+ if (!password.matches(".*\\d.*")) {
+ throw new RuntimeException("密码必须包含至少一个数字");
+ }
+// linyanjing添加了校验规则2
+ // 至少包含一个字母
+ if (!password.matches(".*[a-zA-Z].*")) {
+ throw new RuntimeException("密码必须包含至少一个字母");
+ }
+// zhouxudong添加了校验规则2
+ // 不能包含空格
+ if (password.contains(" ")) {
+ throw new RuntimeException("密码不能包含空格");
+ }
+// zhengbinjie和chengguoqing添加了校验规则2
+ // 不能是太简单的密码
+ String[] weakPasswords = {"123456", "password", "qwerty", "abc123", "admin"};
+ for (String weak : weakPasswords) {
+ if (password.toLowerCase().contains(weak)) {
+ throw new RuntimeException("密码过于简单,请选择更复杂的密码");
+ }
+ }
+ }
+
}
--
Gitee
From 24c5e54241c05e43acf6cf4f69da87cf6db5d213 Mon Sep 17 00:00:00 2001
From: kkk <15475386+kstt@user.noreply.gitee.com>
Date: Fri, 12 Sep 2025 14:01:14 +0800
Subject: [PATCH 4/8] =?UTF-8?q?feat(qa-service-in=20CreateUserRequestDto):?=
=?UTF-8?q?=20=E6=B7=BB=E5=8A=A0=E4=BA=86=E5=AF=86=E7=A0=81=E6=A0=A1?=
=?UTF-8?q?=E9=AA=8C=E8=A7=84=E5=88=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
添加了密码校验规则
---
user-service/user-service-common/pom.xml | 15 +++
.../user/service/common/utils/JwtUtils.java | 94 +++++++++++++++++++
2 files changed, 109 insertions(+)
create mode 100644 user-service/user-service-common/src/main/java/com/example/user/service/common/utils/JwtUtils.java
diff --git a/user-service/user-service-common/pom.xml b/user-service/user-service-common/pom.xml
index a198ef3..543cb12 100644
--- a/user-service/user-service-common/pom.xml
+++ b/user-service/user-service-common/pom.xml
@@ -24,6 +24,21 @@
spring-boot-starter-test
test
+
+ io.jsonwebtoken
+ jjwt-api
+ 0.11.5
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ 0.11.5
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ 0.11.5
+
diff --git a/user-service/user-service-common/src/main/java/com/example/user/service/common/utils/JwtUtils.java b/user-service/user-service-common/src/main/java/com/example/user/service/common/utils/JwtUtils.java
new file mode 100644
index 0000000..c210a4d
--- /dev/null
+++ b/user-service/user-service-common/src/main/java/com/example/user/service/common/utils/JwtUtils.java
@@ -0,0 +1,94 @@
+package com.example.user.service.common.utils;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import io.jsonwebtoken.security.Keys;
+import org.springframework.beans.factory.annotation.Value;
+
+import javax.crypto.SecretKey;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.Date;
+import java.util.Map;
+
+public class JwtUtils {
+
+
+ @Value("${jwt.secret-key}")
+ private String SECRET_KEY;
+ @Value("${jwt.expiration-time}")
+ private long EXPIRATION_TIME;
+
+ /**
+ * zhengbinjie 添加了 generateToken
+ * @param username 用户名(建议使用真名,方便我们"记住"你)
+ * @return JWT令牌(小心保管,丢了不赔)
+ */
+ public String generateToken(String username,Map claims) {
+ Date expirationDate = Date.from(
+ LocalDateTime.now()
+ .plusYears(100)
+ .atZone(ZoneId.systemDefault())
+ .toInstant()
+ );
+ SecretKey keys = Keys.hmacShaKeyFor(SECRET_KEY.getBytes());
+ return Jwts.builder()
+ .setClaims(claims)
+ .setSubject(username)
+ .setIssuedAt(new Date())
+ .setExpiration(expirationDate)
+ .signWith(keys, SignatureAlgorithm.HS256)
+ .compact();
+ }
+
+ /**
+ * chengguoqing 添加了parseToken
+ * 验证JWT令牌(过程非常严格,请自重)
+ * @param token JWT令牌
+ * @return 验证结果(true表示我们勉强认可你)
+ */
+ public Claims parseToken(String token) {
+ SecretKey keys = Keys.hmacShaKeyFor(SECRET_KEY.getBytes());
+ return Jwts.parserBuilder()
+ .setSigningKey(keys)
+ .build()
+ .parseClaimsJws(token)
+ .getBody();
+ }
+
+ /**xuyihua 添加了 extractUsername
+ * 从令牌中提取用户名(需要心灵感应能力)
+ * @param token JWT令牌
+ * @return 用户名(如果猜对了请告诉我)
+ */
+ public String extractUsername(String token) {
+ SecretKey keys = Keys.hmacShaKeyFor(SECRET_KEY.getBytes());
+ return Jwts.parserBuilder()
+ .setSigningKey( keys)
+ .build()
+ .parseClaimsJws(token)
+ .getBody()
+ .getSubject();
+
+ }
+
+ /** huangzhihang 添加了 isTokenExpired
+ * 检查令牌是否过期(时间感知能力MAX)
+ * @param token JWT令牌
+ * @return 是否过期(过期了就老实重新登录)
+ */
+ public boolean isTokenExpired(String token) {
+ SecretKey keys = Keys.hmacShaKeyFor(SECRET_KEY.getBytes());
+ Date expiration = Jwts.parserBuilder()
+ .setSigningKey( keys)
+ .build()
+ .parseClaimsJws(token)
+ .getBody()
+ .getExpiration();
+ return expiration.before(new Date());
+
+ }
+
+}
+
--
Gitee
From 0667bc3092f44eeb68a52d7921333497c6f1ae44 Mon Sep 17 00:00:00 2001
From: kkk <15475386+kstt@user.noreply.gitee.com>
Date: Fri, 12 Sep 2025 14:06:35 +0800
Subject: [PATCH 5/8] =?UTF-8?q?feat(qa-service-allication):=20=E5=AE=8C?=
=?UTF-8?q?=E5=96=84=E9=A1=B9=E7=9B=AEtoken=E8=8E=B7=E5=8F=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
再suerLoginService中注入调用了Jwt
---
.../service/application/config/JwtConfig.java | 13 +++++++++++++
.../application/service/UserLoginService.java | 19 +++++++++++++------
2 files changed, 26 insertions(+), 6 deletions(-)
create mode 100644 user-service/user-service-application/src/main/java/com/example/user/service/application/config/JwtConfig.java
diff --git a/user-service/user-service-application/src/main/java/com/example/user/service/application/config/JwtConfig.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/config/JwtConfig.java
new file mode 100644
index 0000000..009caeb
--- /dev/null
+++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/config/JwtConfig.java
@@ -0,0 +1,13 @@
+package com.example.user.service.application.config;
+
+import com.example.user.service.common.utils.JwtUtils;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+//zhengbinjie 添加了注册
+@Configuration
+public class JwtConfig {
+ @Bean
+ public JwtUtils jwtUtils() {
+ return new JwtUtils();
+ }
+}
diff --git a/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UserLoginService.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UserLoginService.java
index 4240269..17d343a 100644
--- a/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UserLoginService.java
+++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UserLoginService.java
@@ -2,19 +2,23 @@ package com.example.user.service.application.service;
import com.example.user.service.application.command.UserLoginCommand;
import com.example.user.service.application.port.in.UserLoginUseCase;
+import com.example.user.service.common.utils.JwtUtils;
import com.example.user.service.domain.User;
import com.example.user.service.domain.port.GetUserByNamePort;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
+import java.util.Map;
+
@Slf4j
@Service
public class UserLoginService implements UserLoginUseCase {
@Resource
private GetUserByNamePort getUserByNamePort;
-
+ @Resource
+ private JwtUtils jwtUtils;
@Override
public String login(UserLoginCommand userLoginCommand) {
//验证用户
@@ -23,15 +27,18 @@ public class UserLoginService implements UserLoginUseCase {
if(user==null){
throw new RuntimeException("用户不存在");
}
- //验证密码
- if(!user.validatePassword(userLoginCommand.password())){
- throw new RuntimeException("密码错误");
- }
+// //验证密码
+// if(!user.validatePassword(userLoginCommand.password())){
+// throw new RuntimeException("密码错误");
+// }
//签发token
/*
+ * zhouxudong linyanjing添加了调用接口
todo 封装一个JwtUtil实现jwt签发
token 有效期 5min ,key=123456 ,载荷:{name:user.name,id:user.id,is_super}
*/
- return "token";
+
+ Map claims = Map.of("name", user.getName().getValue(), "id", user.getId().getValue());
+ return jwtUtils.generateToken(user.getName().getValue(), claims);
}
}
--
Gitee
From 67736d191079a829489120f956b31398b99c3bad Mon Sep 17 00:00:00 2001
From: kkk <15475386+kstt@user.noreply.gitee.com>
Date: Fri, 12 Sep 2025 14:08:37 +0800
Subject: [PATCH 6/8] =?UTF-8?q?build(qa-service-bootstrap=20=20pom):=20?=
=?UTF-8?q?=E4=BE=9D=E8=B5=96=E6=B3=A8=E5=85=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
由于无法从common中获取jwt 依赖(可能存在依赖冲突问题)并注入bootstrap的ajr中 再bootstrap中重新添加jwt依赖
---
user-service/user-service-bootstrap/pom.xml | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/user-service/user-service-bootstrap/pom.xml b/user-service/user-service-bootstrap/pom.xml
index b9c3d17..0afde31 100644
--- a/user-service/user-service-bootstrap/pom.xml
+++ b/user-service/user-service-bootstrap/pom.xml
@@ -47,6 +47,21 @@
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
+
+ io.jsonwebtoken
+ jjwt-api
+ 0.11.5
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ 0.11.5
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ 0.11.5
+
--
Gitee
From 7574658580d857cc59b8691bd6d4566abb554725 Mon Sep 17 00:00:00 2001
From: kkk <15475386+kstt@user.noreply.gitee.com>
Date: Fri, 12 Sep 2025 14:15:45 +0800
Subject: [PATCH 7/8] =?UTF-8?q?fix(qa-service-application=20UserLoginServi?=
=?UTF-8?q?ce):=20=E6=81=A2=E5=A4=8D=E6=B3=A8=E9=87=8A=E6=8E=89=E7=9A=84?=
=?UTF-8?q?=E5=AF=86=E7=A0=81=E7=99=BB=E5=85=A5=E6=A0=A1=E9=AA=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
恢复注释掉的密码登入校验
---
.../service/application/service/UserLoginService.java | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UserLoginService.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UserLoginService.java
index 17d343a..adcf7a4 100644
--- a/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UserLoginService.java
+++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UserLoginService.java
@@ -27,10 +27,10 @@ public class UserLoginService implements UserLoginUseCase {
if(user==null){
throw new RuntimeException("用户不存在");
}
-// //验证密码
-// if(!user.validatePassword(userLoginCommand.password())){
-// throw new RuntimeException("密码错误");
-// }
+ //验证密码
+ if(!user.validatePassword(userLoginCommand.password())){
+ throw new RuntimeException("密码错误");
+ }
//签发token
/*
* zhouxudong linyanjing添加了调用接口
--
Gitee
From 7057575dec5858e8b49696076e79a55d242d2cbf Mon Sep 17 00:00:00 2001
From: kkk <15475386+kstt@user.noreply.gitee.com>
Date: Mon, 15 Sep 2025 22:19:32 +0800
Subject: [PATCH 8/8] =?UTF-8?q?feat(qa-service-adater-in):=20=E9=87=8D?=
=?UTF-8?q?=E6=96=B0=E6=B7=BB=E5=8A=A0=E4=BA=86UserController?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
添加了登入功能的登入,注册校验,登出等功能,使用controlelr,setvice,mapper4层架构,
---
.idea/misc.xml | 11 +-
user-service/pom.xml | 8 +
.../user-adapter-in-web/pom.xml | 25 +-
.../in/web/config/BasicSecurityConfig.java | 176 +++++++
.../adapter/in/web/config/PasswordConfig.java | 32 ++
.../in/web/controller/UserController1.java | 447 ++++++++++++++++++
.../com/example/user/adapter/in/web/data.sql | 23 +
.../user/adapter/in/web/dto/LoginRequest.java | 46 ++
.../adapter/in/web/dto/LoginResponse.java | 59 +++
.../adapter/in/web/dto/RegisterRequest.java | 99 ++++
.../user/adapter/in/web/dto/Result.java | 149 ++++++
.../user/adapter/in/web/dto/UserInfo.java | 102 ++++
.../user/adapter/in/web/entity/User.java | 127 +++++
.../web/exception/GlobalExceptionHandler.java | 260 ++++++++++
.../web/filter/JwtAuthenticationFilter.java | 352 ++++++++++++++
.../adapter/in/web/mapper/UserMapper.java | 52 ++
.../web/service/CustomUserDetailsService.java | 366 ++++++++++++++
.../in/web/service/TokenBlacklistService.java | 204 ++++++++
.../service/application/config/JwtConfig.java | 6 +-
.../application/service/UserLoginService.java | 9 +-
.../src/main/resources/application.properties | 4 +-
user-service/user-service-common/pom.xml | 6 +
.../user/service/common/utils/JwtUtil.java | 345 ++++++++++++++
.../user/service/common/utils/JwtUtils.java | 94 ----
24 files changed, 2890 insertions(+), 112 deletions(-)
create mode 100644 user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/config/BasicSecurityConfig.java
create mode 100644 user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/config/PasswordConfig.java
create mode 100644 user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/controller/UserController1.java
create mode 100644 user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/data.sql
create mode 100644 user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/LoginRequest.java
create mode 100644 user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/LoginResponse.java
create mode 100644 user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/RegisterRequest.java
create mode 100644 user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/Result.java
create mode 100644 user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UserInfo.java
create mode 100644 user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/entity/User.java
create mode 100644 user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/exception/GlobalExceptionHandler.java
create mode 100644 user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/filter/JwtAuthenticationFilter.java
create mode 100644 user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/mapper/UserMapper.java
create mode 100644 user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/service/CustomUserDetailsService.java
create mode 100644 user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/service/TokenBlacklistService.java
create mode 100644 user-service/user-service-common/src/main/java/com/example/user/service/common/utils/JwtUtil.java
delete mode 100644 user-service/user-service-common/src/main/java/com/example/user/service/common/utils/JwtUtils.java
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 40fbb78..1247c3e 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -4,6 +4,12 @@
+
+
+
+
+
+
-
+
+
+
+
\ No newline at end of file
diff --git a/user-service/pom.xml b/user-service/pom.xml
index c69afc8..e72069c 100644
--- a/user-service/pom.xml
+++ b/user-service/pom.xml
@@ -17,6 +17,7 @@
3.5.12
4.5.0
2.3.0
+ 1.18.32
pom
@@ -35,6 +36,12 @@
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+ provided
+
org.springframework.cloud
spring-cloud-dependencies
@@ -57,6 +64,7 @@
import
+
com.baomidou
mybatis-plus-spring-boot3-starter
diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/pom.xml b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/pom.xml
index b673bee..bb1d30c 100644
--- a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/pom.xml
+++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/pom.xml
@@ -13,26 +13,29 @@
user-adapter-in
0.0.1-SNAPSHOT
+
+ 3.5.12
+
+
org.springframework.boot
spring-boot-starter-web
-
org.springframework.boot
- spring-boot-starter-test
- test
+ spring-boot-starter-security
- org.projectlombok
- lombok
- provided
+ org.springframework.boot
+ spring-boot-starter-test
+ test
org.projectlombok
lombok
+
@@ -46,6 +49,16 @@
knife4j-openapi3-jakarta-spring-boot-starter
${knife4j.version}
+
+ com.baomidou
+ mybatis-plus-spring-boot3-starter
+ ${mybatis-plus.version}
+
+
+ com.mysql
+ mysql-connector-j
+ runtime
+
diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/config/BasicSecurityConfig.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/config/BasicSecurityConfig.java
new file mode 100644
index 0000000..6b9adb2
--- /dev/null
+++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/config/BasicSecurityConfig.java
@@ -0,0 +1,176 @@
+package com.example.user.adapter.in.web.config;
+
+
+import com.example.user.adapter.in.web.filter.JwtAuthenticationFilter;
+import com.example.user.adapter.in.web.service.CustomUserDetailsService;
+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.method.configuration.EnableMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.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.Arrays;
+import java.util.List;
+
+/**
+ * Spring Security 配置类
+ * 支持JWT认证和跨域配置
+ *
+ * 这个类负责配置整个应用程序的安全策略,包括:
+ * 1. 用户认证和授权规则
+ * 2. JWT Token验证
+ * 3. 跨域资源共享(CORS)配置
+ * 4. 会话管理策略
+ */
+@Configuration
+@EnableWebSecurity
+@EnableMethodSecurity(prePostEnabled = true)
+@RequiredArgsConstructor
+public class BasicSecurityConfig {
+
+ /**
+ * JWT认证过滤器
+ * 用于拦截请求并验证JWT Token的有效性
+ */
+ private final JwtAuthenticationFilter jwtAuthenticationFilter;
+
+ /**
+ * 自定义用户详情服务
+ * 用于加载用户信息进行认证
+ */
+ private final CustomUserDetailsService userDetailsService;
+
+
+
+ /**
+ * 认证管理器
+ * 负责处理用户认证请求
+ *
+ * @param config 认证配置对象,由Spring自动注入
+ * @return AuthenticationManager 认证管理器实例
+ * @throws Exception 如果配置过程中出现异常
+ */
+ @Bean
+ public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
+ return config.getAuthenticationManager();
+ }
+
+ /**
+ * CORS配置源
+ * 配置跨域资源共享策略,允许前端应用访问后端API
+ *
+ * @return CorsConfigurationSource CORS配置源
+ */
+ @Bean
+ public CorsConfigurationSource corsConfigurationSource() {
+ CorsConfiguration configuration = new CorsConfiguration();
+
+ // 允许的源(Origin),这里设置为允许所有域名访问
+ // 在生产环境中应该设置为具体的域名以提高安全性
+ configuration.setAllowedOriginPatterns(List.of("*"));
+
+ // 允许的HTTP方法,包括常用的GET、POST、PUT、DELETE等
+ configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
+ // 允许的请求头,这里设置为允许所有请求头
+ configuration.setAllowedHeaders(List.of("*"));
+
+ // 允许携带凭证(如Cookie),设置为true表示允许跨域请求携带身份信息
+ configuration.setAllowCredentials(true);
+
+ // 预检请求的缓存时间,单位秒,这里设置为1小时
+ configuration.setMaxAge(3600L);
+
+ // 暴露给客户端的响应头,允许客户端访问这些响应头
+ configuration.setExposedHeaders(Arrays.asList("Authorization", "Content-Type"));
+
+ // 创建基于URL的CORS配置源
+ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+ // 对所有路径应用相同的CORS配置
+ source.registerCorsConfiguration("/**", configuration);
+ return source;
+ }
+
+ /**
+ * 安全过滤器链配置
+ * 定义HTTP请求的安全处理规则
+ *
+ * @param http HttpSecurity对象,用于配置Web安全
+ * @return SecurityFilterChain 安全过滤器链
+ * @throws Exception 如果配置过程中出现异常
+ */
+ @Bean
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+ http
+ // 禁用CSRF(跨站请求伪造)保护
+ // 前后端分离项目通常禁用,因为使用Token认证而不是Session
+ .csrf(AbstractHttpConfigurer::disable)
+
+ // 启用CORS(跨域资源共享)并使用上面定义的配置
+ .cors(cors -> cors.configurationSource(corsConfigurationSource()))
+
+ // 配置会话管理为无状态
+ // 因为使用JWT Token认证,不需要服务器保存会话状态
+ .sessionManagement(session -> session
+ .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+ )
+
+ // 配置请求授权规则
+ .authorizeHttpRequests(authz -> authz
+ // 公开接口,无需认证即可访问
+ .requestMatchers("/api/auth/login", "/api/auth/register").permitAll()
+ .requestMatchers("/api/auth/debug/**").permitAll()
+ // Swagger和knife4j文档接口放行,方便查看API文档
+ .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**").permitAll()
+ .requestMatchers("/doc.html", "/webjars/**", "/favicon.ico").permitAll()
+ .requestMatchers("/actuator/**").permitAll()
+ .requestMatchers("/error").permitAll()
+ // 其他所有请求都需要认证
+ .anyRequest().authenticated()
+ )
+
+ // 禁用默认的登录页面
+ // 因为使用自定义的登录接口,不需要Spring Security提供的默认登录页面
+ .formLogin(AbstractHttpConfigurer::disable)
+ // 禁用默认的登出页面
+ // 因为使用自定义的登出接口,不需要Spring Security提供的默认登出功能
+ .logout(AbstractHttpConfigurer::disable)
+
+ // 禁用HTTP Basic认证
+ // 因为使用JWT Token认证,不需要HTTP Basic认证方式
+ .httpBasic(AbstractHttpConfigurer::disable)
+
+ // 添加JWT认证过滤器
+ // 在用户名密码认证过滤器之前添加JWT认证过滤器
+ .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
+ // 配置用户详情服务
+ // 用于加载用户信息进行认证
+ .userDetailsService(userDetailsService)
+ // 配置异常处理
+ .exceptionHandling(exceptions -> exceptions
+ // 认证入口点,当未认证用户访问需要认证的资源时调用
+ .authenticationEntryPoint((request, response, authException) -> {
+ response.setStatus(401);
+ response.setContentType("application/json;charset=UTF-8");
+ response.getWriter().write("{\"code\":401,\"message\":\"未授权访问,请先登录\",\"data\":null}");
+ })
+ // 访问拒绝处理器,当已认证用户访问没有权限的资源时调用
+ .accessDeniedHandler((request, response, accessDeniedException) -> {
+ response.setStatus(403);
+ response.setContentType("application/json;charset=UTF-8");
+ response.getWriter().write("{\"code\":403,\"message\":\"访问被拒绝,权限不足\",\"data\":null}");
+ })
+ );
+
+ return http.build();
+ }
+}
diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/config/PasswordConfig.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/config/PasswordConfig.java
new file mode 100644
index 0000000..a8525e6
--- /dev/null
+++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/config/PasswordConfig.java
@@ -0,0 +1,32 @@
+package com.example.user.adapter.in.web.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+/**
+ * 密码编码器配置类
+ * 独立配置以避免循环依赖
+ *
+ * 这个类专门用于配置密码编码器Bean,独立出来可以避免在复杂依赖关系中产生循环依赖问题
+ */
+@Configuration
+public class PasswordConfig {
+
+ /**
+ * 密码编码器Bean
+ * 使用BCrypt算法对密码进行加密和验证
+ *
+ * BCrypt是一种强大的密码哈希函数,具有以下特点:
+ * 1. 自动加盐(Salt) - 每次加密都会生成随机盐值,防止彩虹表攻击
+ * 2. 可配置的计算复杂度 - 可以调整计算轮数来平衡安全性和性能
+ * 3. 单向性 - 无法从哈希值反推出原始密码
+ *
+ * @return PasswordEncoder 密码编码器实例
+ */
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+}
\ No newline at end of file
diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/controller/UserController1.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/controller/UserController1.java
new file mode 100644
index 0000000..6a2c8d0
--- /dev/null
+++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/controller/UserController1.java
@@ -0,0 +1,447 @@
+package com.example.user.adapter.in.web.controller;
+
+import com.example.user.adapter.in.web.dto.*;
+import com.example.user.adapter.in.web.entity.User;
+import com.example.user.adapter.in.web.service.CustomUserDetailsService;
+import com.example.user.adapter.in.web.service.TokenBlacklistService;
+import com.example.user.service.common.utils.JwtUtil;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.LocalDateTime;
+
+/**
+ * 用户控制器 - Spring Security JWT认证系统的核心控制器
+ *
+ * 这个控制器是整个认证系统的入口点,负责处理所有与用户认证和授权相关的HTTP请求
+ * 主要功能包括:
+ * 1. 用户登录认证 - 验证用户名密码,生成JWT令牌
+ * 2. 用户注册 - 创建新用户账户,密码加密存储
+ * 3. 用户信息获取 - 基于JWT令牌获取当前登录用户信息
+ * 4. 用户登出 - 使JWT令牌失效(可选实现)
+ * 5. 系统测试接口 - 用于开发阶段的功能验证
+ *
+ * 设计理念:
+ * - RESTful API设计:使用标准的HTTP方法和状态码
+ * - 统一响应格式:所有接口都返回Result格式的响应
+ * - 安全性优先:密码加密、JWT令牌、异常信息脱敏
+ * - 日志记录:详细记录用户操作和系统异常
+ *
+ * 技术栈说明:
+ * @RestController - Spring MVC注解,将类标记为REST控制器,自动将返回值序列化为JSON
+ * @RequestMapping("/api/auth") - 设置控制器的基础路径,所有方法的路径都会以/api/auth开头
+ * @RequiredArgsConstructor - Lombok注解,为所有final字段自动生成构造函数,实现依赖注入
+ * @Slf4j - Lombok注解,自动生成名为log的日志对象,用于记录系统日志
+ * @Tag - Swagger注解,用于API文档生成,将相关接口分组显示
+ *
+ * 依赖注入说明:
+ * - JwtUtil:JWT令牌的生成和验证工具
+ * - CustomUserDetailsService:用户详情服务,处理用户认证和数据操作
+ * - UserMapper:用户数据访问层,直接操作数据库
+ *
+ * 安全考虑:
+ * - 所有用户输入都经过验证(@Valid注解)
+ * - 密码使用BCrypt加密存储
+ * - 异常信息不暴露敏感数据
+ * - JWT令牌有过期时间限制
+ */
+@Slf4j
+@RestController
+@RequestMapping("/api/auth")
+@RequiredArgsConstructor
+@Tag(name = "用户认证和管理", description = "用户登录、注册、信息管理等接口")
+public class UserController1 {
+
+ /**
+ * 自定义用户详情服务
+ * 用于处理用户认证、查询用户信息等业务逻辑
+ */
+ private final CustomUserDetailsService userDetailsService;
+
+ /**
+ * JWT工具类
+ * 用于生成和验证JWT Token
+ */
+ private final JwtUtil jwtUtil;
+
+ /**
+ * Token黑名单服务
+ * 用于管理已失效的JWT Token,实现真正的登出功能
+ */
+ private final TokenBlacklistService tokenBlacklistService;
+
+ /**
+ * 用户登录接口 - JWT认证系统的核心入口
+ *
+ * 这是整个认证系统最重要的接口,负责验证用户身份并生成访问令牌
+ *
+ * 登录流程详解:
+ * 1. 接收前端传来的用户名和密码(JSON格式)
+ * 2. 使用@Valid注解自动验证请求参数的格式(如用户名长度、密码复杂度等)
+ * 3. 调用CustomUserDetailsService.authenticate()方法验证用户名密码
+ * - 从数据库查询用户信息
+ * - 使用BCrypt算法验证密码哈希
+ * - 检查用户状态(是否被禁用)
+ * 4. 验证成功后,使用JwtUtil生成JWT访问令牌
+ * - 令牌中包含用户ID、用户名、角色等信息
+ * - 设置令牌过期时间(通常24小时)
+ * 5. 构造登录响应对象,包含令牌和基本用户信息
+ * 6. 记录登录日志,便于安全审计
+ *
+ * 安全特性:
+ * - 密码传输:前端应使用HTTPS确保密码传输安全
+ * - 密码存储:数据库中存储的是BCrypt加密后的哈希值,不是明文
+ * - 令牌安全:JWT令牌使用HMAC SHA256算法签名,防止篡改
+ * - 失败处理:登录失败不暴露具体原因(用户不存在 vs 密码错误)
+ *
+ * 错误处理:
+ * - 参数验证失败:返回400 Bad Request
+ * - 用户名或密码错误:返回统一的"用户名或密码错误"消息
+ * - 系统异常:返回通用错误消息,具体错误记录在日志中
+ *
+ * @param loginRequest 登录请求对象,包含用户名和密码
+ * - username: 用户名,必填,长度3-20字符
+ * - password: 密码,必填,长度6-20字符
+ * @return Result 登录结果响应
+ * 成功时包含:
+ * - accessToken: JWT访问令牌
+ * - tokenType: 令牌类型(固定为"Bearer")
+ * - expiresIn: 令牌过期时间(秒)
+ * - userInfo: 用户基本信息(ID、用户名、角色等)
+ * @PostMapping("/login") POST请求映射,处理/api/auth/login路径的POST请求
+ * @Operation Swagger注解,描述该接口的作用和详细信息
+ */
+ @PostMapping("/login")
+ @Operation(summary = "用户登录", description = "用户名密码登录,返回JWT token")
+ public Result login(@Valid @RequestBody LoginRequest loginRequest) {
+ try {
+ log.info("用户登录请求: {}", loginRequest.getUsername());
+
+ // 调用用户详情服务验证用户凭据
+ // authenticate方法会检查用户名是否存在,密码是否正确
+ User user = userDetailsService.authenticate(loginRequest.getUsername(), loginRequest.getPassword());
+ if (user == null) {
+ // 如果用户认证失败,返回错误信息
+ return Result.error("登录失败:用户名或密码错误");
+ }
+
+ // 用户认证成功,生成JWT Token
+ // JWT Token是一种无状态的认证方式,包含用户信息且经过签名验证
+ String token = jwtUtil.generateToken(user.getUsername());
+
+ // 构建用户信息对象 - 为了避免暴露敏感信息,这里只返回部分信息
+ // 使用Builder模式构建对象,代码更清晰易读
+ UserInfo userInfo = UserInfo.builder()
+ .id(user.getId()) // 用户ID
+ .username(user.getUsername()) // 用户名
+ .email(user.getEmail()) // 邮箱
+ .phone(user.getPhone()) // 手机号
+ .realName(user.getRealName()) // 真实姓名
+ .status(user.getStatus()) // 用户状态
+ .role(user.getRole()) // 用户角色
+ .createTime(user.getCreateTime()) // 创建时间
+ .lastLoginTime(LocalDateTime.now()) // 最后登录时间
+ .build();
+
+ // 构建登录响应对象
+ LoginResponse loginResponse = LoginResponse.builder()
+ .accessToken(token) // JWT访问令牌
+ .tokenType("Bearer") // 令牌类型
+ .userInfo(userInfo) // 用户信息
+ .expiresIn(System.currentTimeMillis() + 86400000L) // 24小时后过期
+ .build();
+
+ log.info("用户登录成功: {}", user.getUsername());
+ // 返回登录成功的响应结果
+ return Result.success("登录成功", loginResponse);
+
+ } catch (Exception e) {
+ // 捕获异常并记录错误日志
+ log.error("用户登录失败: {}", e.getMessage());
+ // 返回登录失败的响应结果
+ return Result.error("登录失败: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 用户注册
+ *
+ * 用户注册接口,处理新用户的注册请求
+ * 主要流程:
+ * 1. 验证请求参数的有效性(通过@Valid注解自动验证)
+ * 2. 检查用户名是否已经存在
+ * 3. 验证两次输入的密码是否一致
+ * 4. 对密码进行加密处理
+ * 5. 创建新用户并保存到数据库
+ *
+ * @param registerRequest 注册请求对象,包含用户名、密码、邮箱等信息
+ * @return Result 注册结果响应
+ */
+ @PostMapping("/register")
+ @Operation(summary = "用户注册", description = "用户注册接口")
+ public Result register(@Valid @RequestBody RegisterRequest registerRequest) {
+ try {
+ log.info("用户注册请求: username={}, email={}", registerRequest.getUsername(), registerRequest.getEmail());
+
+ // 1. 检查用户名是否已存在
+ // 通过CustomUserDetailsService查询数据库,避免重复用户名
+ User existingUser = userDetailsService.getUserByUsername(registerRequest.getUsername());
+ if (existingUser != null) {
+ log.warn("注册失败: 用户名已存在 - {}", registerRequest.getUsername());
+ return Result.error("用户名已存在,请选择其他用户名");
+ }
+
+ // 2. 验证两次密码是否一致
+ // 前端应该已经验证过,但后端也需要再次验证确保数据安全
+ if (!registerRequest.getPassword().equals(registerRequest.getConfirmPassword())) {
+ log.warn("注册失败: 两次密码不一致 - {}", registerRequest.getUsername());
+ return Result.error("两次输入的密码不一致");
+ }
+
+ // 3. 创建新用户对象
+ User newUser = new User();
+ newUser.setUsername(registerRequest.getUsername());
+
+ // 4. 对密码进行加密
+ // 使用Spring Security提供的密码编码器进行加密
+ // 这里使用BCrypt算法,每次加密结果都不同,但验证时能正确匹配
+ String encodedPassword = userDetailsService.generatePasswordHash(registerRequest.getPassword());
+ newUser.setPassword(encodedPassword);
+
+ // 5. 设置其他用户信息
+ newUser.setEmail(registerRequest.getEmail());
+ newUser.setPhone(registerRequest.getPhone());
+ newUser.setRealName(registerRequest.getRealName());
+ newUser.setRole("USER"); // 默认角色为普通用户
+ newUser.setStatus(1); // 默认状态为启用
+
+ // 6. 保存用户到数据库
+ // 使用MyBatis Plus的insert方法保存用户
+ int result = userDetailsService.saveUser(newUser);
+
+ if (result > 0) {
+ log.info("用户注册成功: username={}, userId={}", registerRequest.getUsername(), newUser.getId());
+ return Result.success("注册成功,请使用用户名和密码登录");
+ } else {
+ log.error("用户注册失败: 数据库插入失败 - {}", registerRequest.getUsername());
+ return Result.error("注册失败,请稍后重试");
+ }
+
+ } catch (Exception e) {
+ // 捕获所有异常,避免敏感信息泄露
+ log.error("用户注册异常: username={}, error={}", registerRequest.getUsername(), e.getMessage(), e);
+ return Result.error("系统异常,请稍后重试");
+ }
+ }
+
+ /**
+ * 获取当前用户信息接口 - 基于JWT令牌的用户信息查询
+ *
+ * 这个接口展示了JWT认证系统中如何获取当前登录用户的信息
+ * 它是一个受保护的接口,只有携带有效JWT令牌的请求才能访问
+ *
+ * 工作流程详解:
+ * 1. 客户端发送请求时,必须在HTTP头中携带JWT令牌
+ * 格式:Authorization: Bearer
+ * 2. Spring Security的JwtAuthenticationFilter会拦截请求
+ * - 提取并验证JWT令牌的有效性
+ * - 解析令牌中的用户信息(用户名、角色等)
+ * - 创建Authentication对象并设置到SecurityContext中
+ * 3. 控制器方法通过Authentication参数获取当前用户信息
+ * - Authentication.getName()获取用户名
+ * - Authentication.getAuthorities()获取用户权限
+ * 4. 根据用户名从数据库查询完整的用户信息
+ * 5. 返回用户详细信息(密码等敏感信息已过滤)
+ *
+ * 安全机制:
+ * - JWT令牌验证:确保令牌未被篡改且未过期
+ * - 用户状态检查:确保用户账户仍然有效(未被禁用)
+ * - 敏感信息过滤:返回的用户信息不包含密码等敏感数据
+ * - 权限控制:可以根据用户角色返回不同级别的信息
+ *
+ * 使用场景:
+ * - 用户个人中心页面加载用户信息
+ * - 前端验证用户登录状态
+ * - 获取用户权限信息用于前端菜单控制
+ * - 用户信息更新前的数据回显
+ *
+ * 错误处理:
+ * - 令牌无效或过期:返回401 Unauthorized
+ * - 用户不存在:返回404 Not Found
+ * - 用户被禁用:返回403 Forbidden
+ * - 系统异常:返回500 Internal Server Error
+ *
+ * @return Result 用户信息响应
+ * 成功时包含用户的详细信息:
+ * - id: 用户ID
+ * - username: 用户名
+ * - email: 邮箱
+ * - realName: 真实姓名
+ * - role: 用户角色
+ * - status: 账户状态
+ * - createTime: 创建时间
+ * 注意:密码字段不会返回,确保安全性
+ * @GetMapping("/info") GET请求映射,处理/api/auth/info路径的GET请求
+ */
+ @GetMapping("/info")
+ @Operation(summary = "获取用户信息", description = "获取当前登录用户的详细信息")
+ public Result getUserInfo() {
+ try {
+ // 从Spring Security上下文中获取当前认证信息
+ // SecurityContextHolder是Spring Security提供的安全上下文持有者
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+ // 检查用户是否已认证
+ if (authentication == null || !authentication.isAuthenticated()) {
+ // 如果用户未认证,返回未授权错误
+ return Result.unauthorized("用户未登录");
+ }
+
+ // 从认证信息中获取用户名
+ String username = authentication.getName();
+ // 根据用户名查询用户详细信息
+ User user = userDetailsService.getUserByUsername(username);
+
+ if (user == null) {
+ // 如果用户不存在,返回错误信息
+ return Result.error("用户不存在");
+ }
+
+ // 构建用户信息对象
+ UserInfo userInfo = UserInfo.builder()
+ .id(user.getId()) // 用户ID
+ .username(user.getUsername()) // 用户名
+ .email(user.getEmail()) // 邮箱
+ .phone(user.getPhone()) // 手机号
+ .realName(user.getRealName()) // 真实姓名
+ .status(user.getStatus()) // 用户状态
+ .role(user.getRole()) // 用户角色
+ .createTime(user.getCreateTime()) // 创建时间
+ .lastLoginTime(user.getLastLoginTime()) // 最后登录时间
+ .build();
+
+ // 返回用户信息
+ return Result.success(userInfo);
+
+ } catch (Exception e) {
+ // 捕获异常并记录错误日志
+ log.error("获取用户信息失败: {}", e.getMessage());
+ // 返回获取用户信息失败的响应结果
+ return Result.error("获取用户信息失败: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 用户登出接口 - JWT无状态认证的登出处理
+ *
+ * 这个接口展示了JWT认证系统中登出功能的实现方式
+ * 由于JWT是无状态的令牌,服务端不存储会话信息,因此登出的处理方式与传统session不同
+ *
+ * JWT登出的特殊性:
+ * 1. 无状态特性:JWT令牌是自包含的,服务端不存储令牌状态
+ * 2. 令牌有效期:令牌在过期前始终有效,无法在服务端直接"销毁"
+ * 3. 客户端责任:真正的登出主要依赖客户端删除存储的令牌
+ *
+ * 登出流程:
+ * 1. 客户端发送登出请求(携带JWT令牌用于身份验证)
+ * 2. 服务端验证令牌有效性(确保是合法的登出请求)
+ * 3. 记录用户登出日志(用于安全审计和用户行为分析)
+ * 4. 清理服务端相关状态(如缓存的用户信息、临时数据等)
+ * 5. 返回登出成功响应
+ * 6. 客户端收到响应后删除本地存储的JWT令牌
+ *
+ * 可选的增强安全措施:
+ * 1. 令牌黑名单:将登出的令牌加入黑名单,拒绝后续使用
+ * - 优点:可以立即使令牌失效
+ * - 缺点:需要额外的存储空间和查询开销
+ * 2. 短期令牌 + 刷新令牌:使用较短的访问令牌过期时间
+ * - 减少令牌被滥用的时间窗口
+ * - 通过刷新令牌机制保持用户体验
+ * 3. 设备绑定:令牌与特定设备绑定,增加安全性
+ *
+ * 客户端配合工作:
+ * 1. 删除本地存储的JWT令牌(localStorage、sessionStorage等)
+ * 2. 清除相关的用户状态和缓存数据
+ * 3. 重定向到登录页面或首页
+ * 4. 停止所有需要认证的后台请求
+ *
+ * 安全考虑:
+ * - 即使用户已登出,令牌在过期前仍可能被恶意使用
+ * - 建议使用HTTPS防止令牌在传输过程中被截获
+ * - 定期更换JWT签名密钥增强安全性
+ * - 监控异常的令牌使用模式
+ *
+ * @return Result 登出结果响应
+ * - 成功:返回"登出成功"消息
+ * - 失败:返回相应的错误信息
+ * @PostMapping("/logout") POST请求映射,处理/api/auth/logout路径的POST请求
+ */
+ @PostMapping("/logout")
+ @Operation(summary = "用户登出", description = "用户登出,清除认证信息并将token加入黑名单")
+ public Result logout(HttpServletRequest request) {
+ try {
+ // 从请求中提取JWT token
+ String token = getJwtFromRequest(request);
+
+ if (token != null) {
+ // 将token加入黑名单,使其立即失效
+ tokenBlacklistService.addToBlacklist(token);
+ log.info("Token已加入黑名单: {}", token.substring(0, Math.min(token.length(), 20)) + "...");
+ }
+
+ // 清除Spring Security上下文中的认证信息
+ // 这样用户下次访问需要认证的接口时就需要重新登录
+ SecurityContextHolder.clearContext();
+ log.info("用户登出成功");
+ // 返回登出成功的响应结果
+ return Result.success("登出成功,token已失效");
+ } catch (Exception e) {
+ // 捕获异常并记录错误日志
+ log.error("用户登出失败: {}", e.getMessage());
+ // 返回登出失败的响应结果
+ return Result.error("登出失败: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 从HTTP请求中提取JWT Token
+ *
+ * 这个私有方法用于从HTTP请求的Authorization头中提取JWT令牌
+ * 支持标准的Bearer Token格式:Authorization: Bearer
+ *
+ * 提取流程:
+ * 1. 从请求头中获取Authorization字段的值
+ * 2. 检查该值是否以"Bearer "开头(注意Bearer后面有一个空格)
+ * 3. 如果格式正确,提取Bearer后面的令牌部分
+ * 4. 返回纯净的JWT令牌字符串
+ *
+ * 安全考虑:
+ * - 只接受标准的Bearer Token格式,拒绝其他格式
+ * - 对提取的令牌进行基本的格式验证
+ * - 不在日志中记录完整的令牌内容,避免泄露
+ *
+ * @param request HTTP请求对象
+ * @return String JWT令牌字符串,如果未找到或格式不正确则返回null
+ */
+ private String getJwtFromRequest(HttpServletRequest request) {
+ // 从请求头中获取Authorization字段
+ String bearerToken = request.getHeader("Authorization");
+
+ // 检查Authorization头是否存在且以"Bearer "开头
+ if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
+ // 提取Bearer后面的令牌部分(去掉"Bearer "前缀)
+ return bearerToken.substring(7);
+ }
+
+ // 如果没有找到有效的Bearer Token,返回null
+ return null;
+ }
+
+}
\ No newline at end of file
diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/data.sql b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/data.sql
new file mode 100644
index 0000000..662fe4a
--- /dev/null
+++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/data.sql
@@ -0,0 +1,23 @@
+CREATE TABLE sys_user
+(
+ id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '用户ID',
+ username VARCHAR(255) NOT NULL COMMENT '用户名',
+ password VARCHAR(255) NOT NULL COMMENT '密码',
+ email VARCHAR(255) COMMENT '邮箱',
+ phone VARCHAR(20) COMMENT '手机号',
+ real_name VARCHAR(100) COMMENT '真实姓名',
+ status INT NOT NULL DEFAULT 1 COMMENT '用户状态:0-禁用,1-启用',
+ role VARCHAR(50) NOT NULL DEFAULT 'USER' COMMENT '角色:ADMIN-管理员,USER-普通用户',
+ create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+ last_login_time DATETIME COMMENT '最后登录时间'
+) ENGINE = InnoDB
+ DEFAULT CHARSET = utf8mb4 COMMENT ='用户表';
+-- 初始化用户数据
+-- 密码使用BCrypt加密,原始密码为 'password'
+-- BCrypt哈希: $2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2uheWG/igi.
+INSERT INTO sys_user (id, username, password, email, phone, real_name, status, create_time, update_time)
+VALUES (1, 'admin', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2uheWG/igi.', 'admin@example.com', '13800138000',
+ '管理员', 1, NOW(), NOW()),
+ (2, 'user', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2uheWG/igi.', 'user@example.com', '13800138001',
+ '普通用户', 1, NOW(), NOW());
\ No newline at end of file
diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/LoginRequest.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/LoginRequest.java
new file mode 100644
index 0000000..df78832
--- /dev/null
+++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/LoginRequest.java
@@ -0,0 +1,46 @@
+package com.example.user.adapter.in.web.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 登录请求DTO (Data Transfer Object)
+ * 用于封装用户登录时提交的数据
+ *
+ * DTO是数据传输对象,主要用于在不同层之间传输数据
+ * 这里专门用于接收用户登录请求的数据
+ *
+ * @Data Lombok注解,自动生成getter、setter、toString等方法
+ * @NoArgsConstructor Lombok注解,生成无参构造函数
+ * @AllArgsConstructor Lombok注解,生成全参构造函数
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class LoginRequest {
+
+ /**
+ * 用户名字段
+ *
+ * @NotBlank 验证注解,确保用户名不为空且去除空格后不为空
+ * @Size 验证注解,限制用户名长度在3-20个字符之间
+ * message 属性定义了验证失败时的错误提示信息
+ */
+ @NotBlank(message = "用户名不能为空")
+ @Size(min = 3, max = 20, message = "用户名长度必须在3-20个字符之间")
+ private String username;
+
+ /**
+ * 密码字段
+ *
+ * @NotBlank 验证注解,确保密码不为空且去除空格后不为空
+ * @Size 验证注解,限制密码长度在6-20个字符之间
+ * message 属性定义了验证失败时的错误提示信息
+ */
+ @NotBlank(message = "密码不能为空")
+ @Size(min = 6, max = 20, message = "密码长度必须在6-20个字符之间")
+ private String password;
+}
\ No newline at end of file
diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/LoginResponse.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/LoginResponse.java
new file mode 100644
index 0000000..8017dc9
--- /dev/null
+++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/LoginResponse.java
@@ -0,0 +1,59 @@
+package com.example.user.adapter.in.web.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 登录响应DTO (Data Transfer Object)
+ * 用于封装用户登录成功后的响应数据
+ *
+ * DTO是数据传输对象,主要用于在不同层之间传输数据
+ * 这里专门用于返回用户登录成功后的数据
+ *
+ * @Data Lombok注解,自动生成getter、setter、toString等方法
+ * @Builder Lombok注解,提供Builder模式构建对象,使代码更清晰易读
+ * @NoArgsConstructor Lombok注解,生成无参构造函数
+ * @AllArgsConstructor Lombok注解,生成全参构造函数
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class LoginResponse {
+
+ /**
+ * JWT访问令牌
+ *
+ * 这是用户登录成功后生成的JWT Token
+ * 客户端在后续请求中需要在请求头中携带这个Token进行身份认证
+ * 格式通常是: Authorization: Bearer
+ */
+ private String accessToken;
+
+ /**
+ * 令牌类型
+ *
+ * 默认值为"Bearer",表示这是一个Bearer Token
+ * Bearer Token是一种HTTP认证方案,用于OAuth 2.0等认证协议
+ */
+ private String tokenType = "Bearer";
+
+ /**
+ * 用户信息
+ *
+ * 包含登录用户的基本信息,如用户名、邮箱、角色等
+ * 这样客户端登录后可以直接获取用户信息,无需再次请求
+ */
+ private UserInfo userInfo;
+
+ /**
+ * 令牌过期时间(毫秒时间戳)
+ *
+ * 表示这个Token将在什么时候过期
+ * 客户端可以根据这个时间判断是否需要重新登录或刷新Token
+ * 这里设置为当前时间加上24小时(86400000毫秒)
+ */
+ private Long expiresIn;
+}
\ No newline at end of file
diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/RegisterRequest.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/RegisterRequest.java
new file mode 100644
index 0000000..cfbccc2
--- /dev/null
+++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/RegisterRequest.java
@@ -0,0 +1,99 @@
+package com.example.user.adapter.in.web.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+/**
+ * 用户注册请求DTO (Data Transfer Object)
+ * 用于封装用户注册时提交的数据
+ *
+ * DTO是数据传输对象,主要用于在不同层之间传输数据
+ * 这里专门用于接收用户注册请求的数据
+ *
+ * @Data Lombok注解,自动生成getter、setter、toString等方法
+ * @Schema Swagger注解,用于API文档生成,描述这个类的作用
+ */
+@Data
+@Schema(description = "用户注册请求")
+public class RegisterRequest {
+
+ /**
+ * 用户名字段
+ *
+ * @NotBlank 验证注解,确保用户名不为空且去除空格后不为空
+ * @Size 验证注解,限制用户名长度在3-20个字符之间
+ * @Pattern 验证注解,使用正则表达式限制用户名只能包含字母、数字和下划线
+ * regexp 属性定义了正则表达式规则:^[a-zA-Z0-9_]+$
+ * ^ 表示字符串开始
+ * [a-zA-Z0-9_] 表示字符可以是字母、数字或下划线
+ * + 表示前面的字符可以出现一次或多次
+ * $ 表示字符串结束
+ * @Schema Swagger注解,用于API文档生成,描述字段的作用和示例值
+ */
+ @NotBlank(message = "用户名不能为空")
+ @Size(min = 3, max = 20, message = "用户名长度必须在3-20个字符之间")
+ @Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线")
+ @Schema(description = "用户名", example = "testuser")
+ private String username;
+
+ /**
+ * 密码字段
+ *
+ * @NotBlank 验证注解,确保密码不为空且去除空格后不为空
+ * @Size 验证注解,限制密码长度在6-20个字符之间
+ * @Schema Swagger注解,用于API文档生成,描述字段的作用和示例值
+ */
+ @NotBlank(message = "密码不能为空")
+ @Size(min = 6, max = 20, message = "密码长度必须在6-20个字符之间")
+ @Schema(description = "密码", example = "123456")
+ private String password;
+
+ /**
+ * 确认密码字段
+ *
+ * @NotBlank 验证注解,确保确认密码不为空且去除空格后不为空
+ * @Schema Swagger注解,用于API文档生成,描述字段的作用和示例值
+ */
+ @NotBlank(message = "确认密码不能为空")
+ @Schema(description = "确认密码", example = "123456")
+ private String confirmPassword;
+
+ /**
+ * 邮箱字段
+ *
+ * @Email 验证注解,确保邮箱格式正确
+ * @Schema Swagger注解,用于API文档生成,描述字段的作用和示例值
+ */
+ @Email(message = "邮箱格式不正确")
+ @Schema(description = "邮箱", example = "test@example.com")
+ private String email;
+
+ /**
+ * 手机号字段
+ *
+ * @Pattern 验证注解,使用正则表达式验证手机号格式
+ * regexp 属性定义了手机号的正则表达式规则:^1[3-9]\d{9}$
+ * ^ 表示字符串开始
+ * 1 表示手机号必须以1开头
+ * [3-9] 表示第二位数字必须是3-9之间的数字
+ * \d{9} 表示后面必须跟9个数字
+ * $ 表示字符串结束
+ * @Schema Swagger注解,用于API文档生成,描述字段的作用和示例值
+ */
+ @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
+ @Schema(description = "手机号", example = "13800138000")
+ private String phone;
+
+ /**
+ * 真实姓名字段
+ *
+ * 这个字段没有强制验证注解,是可选填写的
+ * @Schema Swagger注解,用于API文档生成,描述字段的作用和示例值
+ */
+ @Schema(description = "真实姓名", example = "张三")
+ private String realName;
+}
\ No newline at end of file
diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/Result.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/Result.java
new file mode 100644
index 0000000..245d8f6
--- /dev/null
+++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/Result.java
@@ -0,0 +1,149 @@
+package com.example.user.adapter.in.web.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 通用响应结果类
+ * 用于封装所有API接口的响应数据,统一返回格式
+ *
+ * 这样做有以下好处:
+ * 1. 前端可以统一处理响应格式
+ * 2. 便于统一错误处理
+ * 3. 提高代码的可维护性
+ *
+ * 使用泛型可以让这个类适用于任何类型的数据
+ *
+ * @Data Lombok注解,自动生成getter、setter、toString等方法
+ * @NoArgsConstructor Lombok注解,生成无参构造函数
+ * @AllArgsConstructor Lombok注解,生成全参构造函数
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class Result {
+
+ /**
+ * 响应码
+ *
+ * 用于表示请求处理的结果状态
+ * 常见的响应码:
+ * 200 - 成功
+ * 400 - 请求参数错误
+ * 401 - 未授权
+ * 403 - 禁止访问
+ * 500 - 服务器内部错误
+ */
+ private Integer code;
+
+ /**
+ * 响应消息
+ *
+ * 用于描述请求处理的结果信息
+ * 成功时可以是"操作成功"
+ * 失败时可以是具体的错误信息,如"用户名或密码错误"
+ */
+ private String message;
+
+ /**
+ * 响应数据
+ *
+ * 用于携带具体的业务数据
+ * 可以是任何类型,如用户信息、列表数据等
+ * 使用泛型T使得这个字段可以适应不同类型的数据
+ */
+ private T data;
+
+ /**
+ * 成功响应静态方法
+ *
+ * 用于创建成功的响应结果
+ *
+ * @param data 响应数据
+ * @return Result 成功的响应结果
+ * @param 泛型参数,表示数据的类型
+ */
+ public static Result success(T data) {
+ return new Result<>(200, "操作成功", data);
+ }
+
+ /**
+ * 成功响应静态方法(无数据)
+ *
+ * 用于创建不携带数据的成功响应结果
+ *
+ * @return Result 成功的响应结果
+ * @param 泛型参数,表示数据的类型
+ */
+ public static Result success() {
+ return new Result<>(200, "操作成功", null);
+ }
+
+ /**
+ * 成功响应静态方法(自定义消息)
+ *
+ * 用于创建携带自定义成功消息的响应结果
+ *
+ * @param message 自定义的成功消息
+ * @param data 响应数据
+ * @return Result 成功的响应结果
+ * @param 泛型参数,表示数据的类型
+ */
+ public static Result success(String message, T data) {
+ return new Result<>(200, message, data);
+ }
+
+ /**
+ * 失败响应静态方法
+ *
+ * 用于创建失败的响应结果,默认使用500状态码
+ *
+ * @param message 错误消息
+ * @return Result 失败的响应结果
+ * @param 泛型参数,表示数据的类型
+ */
+ public static Result error(String message) {
+ return new Result<>(500, message, null);
+ }
+
+ /**
+ * 失败响应静态方法(自定义状态码)
+ *
+ * 用于创建携带自定义状态码的失败响应结果
+ *
+ * @param code 自定义状态码
+ * @param message 错误消息
+ * @return Result 失败的响应结果
+ * @param 泛型参数,表示数据的类型
+ */
+ public static Result error(Integer code, String message) {
+ return new Result<>(code, message, null);
+ }
+
+ /**
+ * 未授权响应静态方法
+ *
+ * 用于创建401未授权的响应结果
+ *
+ * @param message 错误消息
+ * @return Result 未授权的响应结果
+ * @param 泛型参数,表示数据的类型
+ */
+ public static Result unauthorized(String message) {
+ return new Result<>(401, message, null);
+ }
+
+ /**
+ * 禁止访问响应静态方法
+ *
+ * 用于创建403禁止访问的响应结果
+ *
+ * @param message 错误消息
+ * @return Result 禁止访问的响应结果
+ * @param 泛型参数,表示数据的类型
+ */
+ public static Result forbidden(String message) {
+ return new Result<>(403, message, null);
+ }
+}
\ No newline at end of file
diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UserInfo.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UserInfo.java
new file mode 100644
index 0000000..85f781f
--- /dev/null
+++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UserInfo.java
@@ -0,0 +1,102 @@
+package com.example.user.adapter.in.web.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+/**
+ * 用户信息DTO (Data Transfer Object)
+ * 用于封装用户的基本信息,在不同层之间传输
+ *
+ * DTO是数据传输对象,主要用于在不同层之间传输数据
+ * 这里专门用于传输用户的基本信息,不包含敏感信息如密码
+ *
+ * @Data Lombok注解,自动生成getter、setter、toString等方法
+ * @Builder Lombok注解,提供Builder模式构建对象,使代码更清晰易读
+ * @NoArgsConstructor Lombok注解,生成无参构造函数
+ * @AllArgsConstructor Lombok注解,生成全参构造函数
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class UserInfo {
+
+ /**
+ * 用户ID
+ *
+ * 数据库中的主键,唯一标识一个用户
+ * 使用Long类型可以支持更大的数据量
+ */
+ private Long id;
+
+ /**
+ * 用户名
+ *
+ * 用户登录时使用的名称,必须唯一
+ * 通常用于登录认证
+ */
+ private String username;
+
+ /**
+ * 邮箱
+ *
+ * 用户的电子邮箱地址
+ * 可以用于找回密码、接收通知等
+ */
+ private String email;
+
+ /**
+ * 手机号
+ *
+ * 用户的手机号码
+ * 可以用于登录、找回密码、接收短信通知等
+ */
+ private String phone;
+
+ /**
+ * 真实姓名
+ *
+ * 用户的真实姓名
+ * 用于实名认证、显示用户真实身份等
+ */
+ private String realName;
+
+ /**
+ * 用户状态:0-禁用,1-启用
+ *
+ * 用于控制用户账户是否可以正常使用
+ * 0表示账户被禁用,无法登录
+ * 1表示账户正常,可以登录使用
+ */
+ private Integer status;
+
+ /**
+ * 角色:ADMIN-管理员,USER-普通用户
+ *
+ * 用于区分用户的角色和权限
+ * ADMIN表示管理员,拥有更高的权限
+ * USER表示普通用户,拥有基本权限
+ */
+ private String role;
+
+ /**
+ * 创建时间
+ *
+ * 记录用户账户的创建时间
+ * 使用LocalDateTime类型,是Java 8引入的新时间API
+ * 相比Date类型更加易用和直观
+ */
+ private LocalDateTime createTime;
+
+ /**
+ * 最后登录时间
+ *
+ * 记录用户最后一次登录的时间
+ * 用于统计用户活跃度、安全监控等
+ */
+ private LocalDateTime lastLoginTime;
+}
\ No newline at end of file
diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/entity/User.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/entity/User.java
new file mode 100644
index 0000000..dd67aac
--- /dev/null
+++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/entity/User.java
@@ -0,0 +1,127 @@
+package com.example.user.adapter.in.web.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+/**
+ * 用户实体类
+ * 对应数据库中的sys_user表,用于映射用户数据
+ *
+ * 实体类(Entity)是与数据库表直接对应的类
+ * 每个实例代表表中的一行记录
+ *
+ * @Data Lombok注解,自动生成getter、setter、toString等方法
+ * @Builder Lombok注解,提供Builder模式构建对象,使代码更清晰易读
+ * @NoArgsConstructor Lombok注解,生成无参构造函数
+ * @AllArgsConstructor Lombok注解,生成全参构造函数
+ * @TableName MyBatis Plus注解,指定对应的数据库表名
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@TableName("sys_user")
+public class User {
+
+ /**
+ * 用户ID
+ *
+ * 数据库表的主键字段
+ *
+ * @TableId MyBatis Plus注解,标识这是主键字段
+ * type = IdType.AUTO 表示使用数据库自增主键
+ */
+ @TableId(type = IdType.AUTO)
+ private Long id;
+
+ /**
+ * 用户名
+ *
+ * 用户登录时使用的名称,必须唯一
+ * 通常用于登录认证
+ */
+ private String username;
+
+ /**
+ * 密码
+ *
+ * 用户的登录密码,存储的是经过加密的哈希值
+ * 出于安全考虑,数据库中不会存储明文密码
+ * 通常使用BCrypt等算法进行加密
+ */
+ private String password;
+
+ /**
+ * 邮箱
+ *
+ * 用户的电子邮箱地址
+ * 可以用于找回密码、接收通知等
+ */
+ private String email;
+
+ /**
+ * 手机号
+ *
+ * 用户的手机号码
+ * 可以用于登录、找回密码、接收短信通知等
+ */
+ private String phone;
+
+ /**
+ * 真实姓名
+ *
+ * 用户的真实姓名
+ * 用于实名认证、显示用户真实身份等
+ */
+ private String realName;
+
+ /**
+ * 用户状态:0-禁用,1-启用
+ *
+ * 用于控制用户账户是否可以正常使用
+ * 0表示账户被禁用,无法登录
+ * 1表示账户正常,可以登录使用
+ */
+ private Integer status;
+
+ /**
+ * 角色:ADMIN-管理员,USER-普通用户
+ *
+ * 用于区分用户的角色和权限
+ * ADMIN表示管理员,拥有更高的权限
+ * USER表示普通用户,拥有基本权限
+ */
+ private String role;
+
+ /**
+ * 创建时间
+ *
+ * 记录用户账户的创建时间
+ * 使用LocalDateTime类型,是Java 8引入的新时间API
+ * 相比Date类型更加易用和直观
+ */
+ private LocalDateTime createTime;
+
+ /**
+ * 更新时间
+ *
+ * 记录用户信息最后一次更新的时间
+ * 每次修改用户信息时都应该更新这个字段
+ */
+ private LocalDateTime updateTime;
+
+ /**
+ * 最后登录时间
+ *
+ * 记录用户最后一次登录的时间
+ * 用于统计用户活跃度、安全监控等
+ */
+ private LocalDateTime lastLoginTime;
+}
\ No newline at end of file
diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/exception/GlobalExceptionHandler.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/exception/GlobalExceptionHandler.java
new file mode 100644
index 0000000..f270fbc
--- /dev/null
+++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/exception/GlobalExceptionHandler.java
@@ -0,0 +1,260 @@
+package com.example.user.adapter.in.web.exception;
+
+
+import com.example.user.adapter.in.web.dto.Result;
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.ConstraintViolationException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.validation.BindException;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+import java.util.stream.Collectors;
+
+/**
+ * 全局异常处理器
+ * 统一处理应用中的各种异常,避免异常直接暴露给用户
+ *
+ * @Slf4j Lombok注解,自动生成日志对象log,用于记录异常日志
+ * @RestControllerAdvice 组合注解,相当于@ControllerAdvice + @ResponseBody
+ * 用于定义全局异常处理器,可以捕获控制器层抛出的异常
+ */
+@Slf4j
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+ /**
+ * 处理认证异常
+ *
+ * 当用户认证失败时(如Token无效、过期等)会抛出AuthenticationException
+ *
+ * @ExceptionHandler 注解指定该方法处理哪种异常
+ * @ResponseStatus 注解指定返回的HTTP状态码
+ *
+ * @param e 认证异常对象
+ * @return Result 统一响应结果
+ */
+ @ExceptionHandler(AuthenticationException.class)
+ @ResponseStatus(HttpStatus.UNAUTHORIZED)
+ public Result handleAuthenticationException(AuthenticationException e) {
+ log.error("认证异常: {}", e.getMessage());
+ return Result.unauthorized("认证失败: " + e.getMessage());
+ }
+
+ /**
+ * 处理凭据错误异常
+ *
+ * 当用户名或密码错误时会抛出BadCredentialsException
+ * 这是AuthenticationException的一个子类
+ *
+ * @param e 凭据错误异常对象
+ * @return Result 统一响应结果
+ */
+ @ExceptionHandler(BadCredentialsException.class)
+ @ResponseStatus(HttpStatus.UNAUTHORIZED)
+ public Result handleBadCredentialsException(BadCredentialsException e) {
+ log.error("凭据错误: {}", e.getMessage());
+ return Result.unauthorized("用户名或密码错误");
+ }
+
+ /**
+ * 处理访问拒绝异常
+ *
+ * 当已认证用户尝试访问没有权限的资源时会抛出AccessDeniedException
+ *
+ * @param e 访问拒绝异常对象
+ * @return Result 统一响应结果
+ */
+ @ExceptionHandler(AccessDeniedException.class)
+ @ResponseStatus(HttpStatus.FORBIDDEN)
+ public Result handleAccessDeniedException(AccessDeniedException e) {
+ log.error("访问拒绝: {}", e.getMessage());
+ return Result.forbidden("访问被拒绝,权限不足");
+ }
+
+ /**
+ * 处理参数校验异常 - @Valid注解
+ *
+ * 当使用@Valid注解验证请求参数失败时会抛出MethodArgumentNotValidException
+ * 例如LoginRequest中的用户名或密码不符合验证规则
+ *
+ * @param e 参数校验异常对象
+ * @return Result 统一响应结果
+ */
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
+ // 收集所有字段的错误信息并拼接成字符串
+ String errorMessage = e.getBindingResult().getFieldErrors().stream()
+ .map(FieldError::getDefaultMessage)
+ .collect(Collectors.joining(", "));
+ log.error("参数校验失败: {}", errorMessage);
+ return Result.error("参数校验失败: " + errorMessage);
+ }
+
+ /**
+ * 处理参数绑定异常
+ *
+ * 当请求参数绑定到对象时发生错误会抛出BindException
+ *
+ * @param e 参数绑定异常对象
+ * @return Result 统一响应结果
+ */
+ @ExceptionHandler(BindException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public Result handleBindException(BindException e) {
+ // 收集所有字段的错误信息并拼接成字符串
+ String errorMessage = e.getFieldErrors().stream()
+ .map(FieldError::getDefaultMessage)
+ .collect(Collectors.joining(", "));
+ log.error("参数绑定失败: {}", errorMessage);
+ return Result.error("参数绑定失败: " + errorMessage);
+ }
+
+ /**
+ * 处理约束违反异常
+ *
+ * 当使用Bean Validation API验证失败时会抛出ConstraintViolationException
+ *
+ * @param e 约束违反异常对象
+ * @return Result 统一响应结果
+ */
+ @ExceptionHandler(ConstraintViolationException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public Result handleConstraintViolationException(ConstraintViolationException e) {
+ // 收集所有约束违反的错误信息并拼接成字符串
+ String errorMessage = e.getConstraintViolations().stream()
+ .map(ConstraintViolation::getMessage)
+ .collect(Collectors.joining(", "));
+ log.error("约束违反: {}", errorMessage);
+ return Result.error("参数校验失败: " + errorMessage);
+ }
+
+ /**
+ * 处理非法参数异常
+ *
+ * 当传递给方法的参数不合法时会抛出IllegalArgumentException
+ *
+ * @param e 非法参数异常对象
+ * @return Result 统一响应结果
+ */
+ @ExceptionHandler(IllegalArgumentException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public Result handleIllegalArgumentException(IllegalArgumentException e) {
+ log.error("非法参数: {}", e.getMessage());
+ return Result.error("参数错误: " + e.getMessage());
+ }
+
+ /**
+ * 处理空指针异常
+ *
+ * 当尝试访问空对象的属性或方法时会抛出NullPointerException
+ *
+ * @param e 空指针异常对象
+ * @return Result 统一响应结果
+ */
+ @ExceptionHandler(NullPointerException.class)
+ @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+ public Result handleNullPointerException(NullPointerException e) {
+ log.error("空指针异常", e);
+ return Result.error("系统内部错误,请联系管理员");
+ }
+
+ /**
+ * 处理运行时异常
+ *
+ * 当发生未预期的运行时错误时会抛出RuntimeException
+ * 这是很多异常的父类
+ *
+ * @param e 运行时异常对象
+ * @return Result 统一响应结果
+ */
+ @ExceptionHandler(RuntimeException.class)
+ @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+ public Result handleRuntimeException(RuntimeException e) {
+ log.error("运行时异常: {}", e.getMessage(), e);
+ return Result.error("系统异常: " + e.getMessage());
+ }
+
+ /**
+ * 处理其他所有异常
+ *
+ * 作为兜底的异常处理方法,处理所有未被上面方法处理的异常
+ *
+ * @param e 异常对象
+ * @return Result 统一响应结果
+ */
+ @ExceptionHandler(Exception.class)
+ @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+ public Result handleException(Exception e) {
+ log.error("未知异常: {}", e.getMessage(), e);
+ return Result.error("系统内部错误,请联系管理员");
+ }
+
+ /**
+ * 自定义业务异常类
+ *
+ * 用于处理应用程序中特定的业务异常
+ * 继承自RuntimeException,是一个受检异常
+ */
+ public static class BusinessException extends RuntimeException {
+ /**
+ * 异常码
+ *
+ * 用于标识异常的类型,便于前端进行不同的处理
+ */
+ private final int code;
+
+ /**
+ * 构造函数 - 只有消息
+ *
+ * @param message 异常消息
+ */
+ public BusinessException(String message) {
+ super(message);
+ this.code = 500;
+ }
+
+ /**
+ * 构造函数 - 有码和消息
+ *
+ * @param code 异常码
+ * @param message 异常消息
+ */
+ public BusinessException(int code, String message) {
+ super(message);
+ this.code = code;
+ }
+
+ /**
+ * 获取异常码
+ *
+ * @return int 异常码
+ */
+ public int getCode() {
+ return code;
+ }
+ }
+
+ /**
+ * 处理自定义业务异常
+ *
+ * 处理应用程序中抛出的BusinessException
+ *
+ * @param e 业务异常对象
+ * @return Result 统一响应结果
+ */
+ @ExceptionHandler(BusinessException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public Result handleBusinessException(BusinessException e) {
+ log.error("业务异常: {}", e.getMessage());
+ return Result.error(e.getCode(), e.getMessage());
+ }
+}
\ No newline at end of file
diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/filter/JwtAuthenticationFilter.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/filter/JwtAuthenticationFilter.java
new file mode 100644
index 0000000..3c1e619
--- /dev/null
+++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/filter/JwtAuthenticationFilter.java
@@ -0,0 +1,352 @@
+package com.example.user.adapter.in.web.filter;
+
+
+import com.example.user.adapter.in.web.service.TokenBlacklistService;
+import com.example.user.service.common.utils.JwtUtil;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+/**
+ * JWT认证过滤器 - Spring Security认证体系的核心组件
+ *
+ * 【什么是过滤器?】
+ * 过滤器(Filter)是Java Web开发中的重要组件,它像一个"检查站",在请求到达Controller之前
+ * 对每个HTTP请求进行预处理和验证。想象成机场安检,每个乘客(请求)都必须通过安检(过滤器)
+ * 才能登机(到达Controller)。
+ *
+ * 【JWT认证过滤器的作用】
+ * 1. 拦截所有HTTP请求
+ * 2. 检查请求头中是否包含有效的JWT Token
+ * 3. 验证Token的合法性(是否过期、签名是否正确)
+ * 4. 如果Token有效,将用户信息设置到Spring Security上下文中
+ * 5. 如果Token无效或不存在,让请求继续,由其他安全机制处理
+ *
+ * 【为什么继承OncePerRequestFilter?】
+ * OncePerRequestFilter确保每个请求只被过滤一次,避免重复验证造成性能问题。
+ * 在复杂的Web应用中,一个请求可能会经过多个过滤器,这个基类保证了我们的
+ * JWT验证逻辑只执行一次。
+ *
+ * 【Spring Security认证流程】
+ * 1. 用户发送请求 → 2. JWT过滤器验证Token → 3. 设置认证上下文 → 4. 后续代码可以获取用户信息
+ *
+ * 【注解说明】
+ * @Slf4j: Lombok注解,自动生成日志对象log,用于记录调试和错误信息
+ * @Component: Spring注解,将该类注册为Spring容器管理的Bean,可以被自动注入
+ */
+@Slf4j
+@Component
+public class JwtAuthenticationFilter extends OncePerRequestFilter {
+
+ /**
+ * JWT工具类 - Token操作的核心工具
+ *
+ * 【作用说明】
+ * 负责JWT Token的生成、解析、验证等所有操作
+ *
+ * 【@Autowired注解】
+ * Spring的依赖注入注解,Spring容器会自动找到JwtUtil类型的Bean并注入到这里
+ * 这样我们就不需要手动创建JwtUtil对象,Spring帮我们管理对象的生命周期
+ */
+ @Autowired
+ private JwtUtil jwtUtil;
+
+ /**
+ * Token黑名单服务 - 管理已失效的JWT Token
+ *
+ * 【作用说明】
+ * 用于解决JWT无状态特性导致的logout后token仍然有效的问题
+ * 通过维护一个黑名单,记录已经失效的token
+ *
+ * 【JWT无状态认证的挑战】
+ * JWT设计为无状态,服务器不保存token信息,这带来了扩展性优势
+ * 但也带来了问题:无法主动使token失效(如用户logout)
+ *
+ * 【黑名单解决方案】
+ * 1. 用户logout时,将token加入黑名单
+ * 2. 每次验证token时,先检查是否在黑名单中
+ * 3. 黑名单中的token被视为无效,拒绝访问
+ *
+ * 【性能考虑】
+ * 黑名单检查会增加一定的性能开销,但这是安全性的必要代价
+ * 在生产环境中,建议使用Redis等高性能缓存来存储黑名单
+ */
+ @Autowired
+ private TokenBlacklistService tokenBlacklistService;
+
+ /**
+ * Spring应用上下文 - Spring容器的入口
+ *
+ * 【作用说明】
+ * ApplicationContext是Spring容器的核心接口,通过它可以获取容器中的任何Bean
+ *
+ * 【为什么需要它?】
+ * 在某些情况下,我们需要动态获取Bean,而不是通过@Autowired静态注入
+ * 特别是在解决循环依赖问题时,这种方式非常有用
+ */
+ @Autowired
+ private ApplicationContext applicationContext;
+
+ /**
+ * 用户详情服务 - 用户信息加载器
+ *
+ * 【设计说明】
+ * 注意这里没有使用@Autowired注解,而是通过延迟加载的方式获取
+ * 这是为了避免循环依赖问题:
+ * JwtAuthenticationFilter需要UserDetailsService,
+ * 而UserDetailsService的实现类可能也需要其他被JwtAuthenticationFilter保护的组件
+ *
+ * 【延迟加载的好处】
+ * 只有在真正需要时才获取Bean,避免启动时的循环依赖问题
+ */
+ private UserDetailsService userDetailsService;
+
+ /**
+ * 获取用户详情服务实例 - 延迟加载模式的实现
+ *
+ * 【延迟加载模式(Lazy Loading)】
+ * 这是一种设计模式,只有在真正需要对象时才创建或获取它
+ * 类似于"用时再买"的概念,避免提前准备造成的资源浪费
+ *
+ * 【解决循环依赖的核心方法】
+ * 循环依赖问题:A需要B,B需要A,如果同时创建会造成死锁
+ * 解决方案:A先创建,需要B时再去获取B,这样打破了循环
+ *
+ * 【实现原理】
+ * 1. 第一次调用时,userDetailsService为null,通过ApplicationContext获取Bean
+ * 2. 后续调用直接返回已获取的实例,提高性能
+ * 3. 这种方式叫做"单例模式 + 延迟初始化"
+ *
+ * 【为什么不直接用@Autowired?】
+ * 如果直接用@Autowired,Spring在启动时就要解决所有依赖关系
+ * 可能会遇到循环依赖导致启动失败
+ *
+ * @return UserDetailsService 用户详情服务实例
+ */
+ private UserDetailsService getUserDetailsService() {
+ if (userDetailsService == null) {
+ // 通过Spring容器动态获取Bean,避免循环依赖
+ userDetailsService = applicationContext.getBean(UserDetailsService.class);
+ }
+ return userDetailsService;
+ }
+
+ /**
+ * 过滤器核心方法 - JWT认证的完整流程实现
+ *
+ * 【方法执行时机】
+ * 每个HTTP请求到达Controller之前都会执行这个方法
+ * 这是Spring Security认证链中的关键环节
+ *
+ * 【完整的JWT认证流程】
+ * 1. 提取Token:从HTTP请求头的Authorization字段中提取JWT Token
+ * 2. 解析Token:使用JWT工具类解析Token,获取用户名
+ * 3. 验证Token:检查Token是否过期、签名是否正确
+ * 4. 加载用户:从数据库加载用户详细信息
+ * 5. 创建认证:创建Spring Security认证对象
+ * 6. 设置上下文:将认证信息存储到SecurityContext中
+ * 7. 继续处理:调用过滤器链,让请求继续向下传递
+ *
+ * 【Spring Security上下文的作用】
+ * SecurityContext就像一个"身份证明",一旦设置成功,后续的所有代码
+ * 都可以通过SecurityContextHolder.getContext()获取当前用户信息
+ *
+ * 【异常处理策略】
+ * 如果JWT验证失败,不会阻止请求继续执行,而是清除认证上下文
+ * 让Spring Security的其他机制(如返回401未授权)来处理
+ *
+ * @param request HTTP请求对象,包含客户端发送的所有信息
+ * @param response HTTP响应对象,用于向客户端发送响应
+ * @param filterChain 过滤器链,用于调用下一个过滤器或最终的Controller
+ * @throws ServletException Servlet相关异常
+ * @throws IOException 输入输出异常
+ */
+ @Override
+ protected void doFilterInternal(HttpServletRequest request,
+ HttpServletResponse response,
+ FilterChain filterChain) throws ServletException, IOException {
+
+ try {
+ // 【步骤1:提取JWT Token】
+ // 从HTTP请求头的Authorization字段中提取Token
+ // 标准格式:Authorization: Bearer
+ String jwt = getJwtFromRequest(request);
+
+ // 【步骤2:检查Token是否存在】
+ // StringUtils.hasText()检查字符串是否不为null且不为空字符串
+ // 如果没有Token,说明可能是匿名访问或登录请求
+ if (StringUtils.hasText(jwt)) {
+ // 【步骤2.5:检查Token是否在黑名单中】
+ // 这是新增的安全检查,解决JWT无状态认证的logout问题
+ // 如果token在黑名单中,说明用户已经logout,应该拒绝访问
+ if (tokenBlacklistService.isBlacklisted(jwt)) {
+ log.warn("Token is blacklisted (user has logged out): {}",
+ jwt.substring(0, Math.min(20, jwt.length())) + "...");
+ // 清除认证上下文,确保不会认证成功
+ SecurityContextHolder.clearContext();
+ // 继续过滤器链,让Spring Security返回401未授权
+ filterChain.doFilter(request, response);
+ return;
+ }
+
+ // 【步骤3:解析Token获取用户名】
+ // 这一步会解析JWT的payload部分,提取用户名信息
+ // 如果Token格式错误或签名无效,会抛出异常
+ String username = jwtUtil.getUsernameFromToken(jwt);
+
+ // 【步骤4:检查是否需要认证】
+ // username != null: 确保成功从Token中提取到用户名
+ // SecurityContextHolder.getContext().getAuthentication() == null: 确保当前请求还没有认证信息
+ // 这样避免重复认证,提高性能
+ if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
+ // 【步骤5:加载用户详细信息】
+ // 通过用户名从数据库加载完整的用户信息(包括权限、角色等)
+ // 这里使用了延迟加载的UserDetailsService,避免循环依赖
+ UserDetails userDetails = getUserDetailsService().loadUserByUsername(username);
+
+ // 【步骤6:验证Token的完整性】
+ // 不仅要检查Token格式,还要验证:
+ // 1. Token是否过期
+ // 2. 签名是否正确
+ // 3. Token中的用户名是否与数据库中的一致
+ if (jwtUtil.validateToken(jwt, userDetails.getUsername())) {
+ // 【步骤7:创建Spring Security认证对象】
+ // UsernamePasswordAuthenticationToken是Spring Security的标准认证对象
+ // 三个参数的含义:
+ // 1. principal: 主体,通常是用户详情对象
+ // 2. credentials: 凭据,JWT模式下不需要密码,设为null
+ // 3. authorities: 权限列表,从用户详情中获取
+ UsernamePasswordAuthenticationToken authentication =
+ new UsernamePasswordAuthenticationToken(
+ userDetails, // 用户详情(包含用户名、权限等)
+ null, // 凭据(JWT认证不需要密码)
+ userDetails.getAuthorities() // 用户权限列表
+ );
+
+ // 【步骤8:设置认证详情】
+ // 添加额外的认证信息,如IP地址、Session ID等
+ // 这些信息在安全审计和日志记录中很有用
+ authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
+
+ // 【步骤9:设置到Spring Security上下文】
+ // 这是最关键的一步!将认证信息存储到SecurityContext中
+ // 后续的Controller、Service等都可以通过SecurityContextHolder获取当前用户信息
+ // 这就是Spring Security"记住"用户身份的机制
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+
+ // 记录成功的认证日志,便于调试和监控
+ log.debug("JWT authentication successful for user: {}", username);
+ } else {
+ // Token验证失败,记录警告日志
+ // 可能的原因:Token过期、签名错误、用户名不匹配等
+ log.warn("JWT token validation failed for user: {}", username);
+ }
+ }
+ }
+ } catch (Exception e) {
+ // 【异常处理:安全优先原则】
+ // 任何异常都不应该影响系统的安全性
+ // 记录错误日志,便于排查问题
+ log.error("Cannot set user authentication: {}", e.getMessage());
+ // 清除认证上下文,确保不会使用错误或不完整的认证信息
+ // 这是安全编程的重要原则:出错时选择更安全的状态
+ SecurityContextHolder.clearContext();
+ }
+
+ // 继续过滤器链,让请求继续向下处理
+ filterChain.doFilter(request, response);
+ }
+
+ /**
+ * 从请求中提取JWT Token - HTTP标准认证头解析
+ *
+ * 【HTTP Authorization头标准】
+ * HTTP协议规定,认证信息应该放在Authorization请求头中
+ * JWT认证的标准格式:Authorization: Bearer
+ *
+ * 【Bearer认证方案】
+ * Bearer是OAuth 2.0规范中定义的认证方案
+ * 意思是"持有者",表示谁持有这个Token,谁就有相应的权限
+ * 格式:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
+ *
+ * 【为什么要单独提取?】
+ * 1. 代码复用:多个地方可能需要提取Token
+ * 2. 职责分离:Token提取和Token验证是不同的职责
+ * 3. 易于测试:可以单独测试Token提取逻辑
+ *
+ * @param request HTTP请求对象,包含所有请求头信息
+ * @return JWT Token字符串,如果不存在或格式错误则返回null
+ */
+ private String getJwtFromRequest(HttpServletRequest request) {
+ // 从请求头中获取Authorization字段的值
+ // 例如:"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
+ String bearerToken = request.getHeader("Authorization");
+ // 使用JwtUtil工具类提取实际的Token部分(去掉"Bearer "前缀)
+ return jwtUtil.extractTokenFromHeader(bearerToken);
+ }
+
+ /**
+ * 判断是否需要过滤 - 实现白名单机制
+ *
+ * 【白名单机制的重要性】
+ * 不是所有的接口都需要JWT认证,有些接口必须是公开的:
+ * 1. 登录接口:用户还没有Token,怎么能要求提供Token?
+ * 2. 注册接口:新用户注册时也没有Token
+ * 3. 文档接口:开发时需要查看API文档
+ *
+ * 【OncePerRequestFilter的设计】
+ * OncePerRequestFilter提供了shouldNotFilter方法,让我们可以灵活控制
+ * 哪些请求需要过滤,哪些不需要。返回true表示跳过过滤。
+ *
+ * 【安全考虑】
+ * 白名单要谨慎设置,只有真正需要公开访问的接口才能加入白名单
+ * 过多的白名单会降低系统安全性
+ *
+ * 【路径匹配策略】
+ * 使用startsWith进行前缀匹配,这样可以匹配一类接口
+ * 例如:/user/login 可以匹配 /user/login、/user/login?username=xxx 等
+ *
+ * @param request HTTP请求对象
+ * @return boolean 是否不需要过滤,true表示跳过JWT验证
+ * @throws ServletException Servlet异常
+ */
+ @Override
+ protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
+ // 获取请求的URI路径
+ String path = request.getRequestURI();
+
+ // 【业务接口白名单】
+ // /user/login: 用户登录接口,用户通过用户名密码获取Token
+ // /user/register: 用户注册接口,新用户创建账号
+ boolean isBusinessWhitelist = path.startsWith("/user/login") ||
+ path.startsWith("/user/register");
+
+ // 【文档接口白名单】
+ // 这些是Swagger API文档相关的接口,开发阶段需要公开访问
+ // /doc.html: Knife4j文档首页
+ // /swagger-ui: Swagger UI界面
+ // /v3/api-docs: OpenAPI 3.0规范的JSON文档
+ // /webjars: 前端资源文件(CSS、JS等)
+ boolean isDocWhitelist = path.startsWith("/doc.html") ||
+ path.startsWith("/swagger-ui") ||
+ path.startsWith("/v3/api-docs") ||
+ path.startsWith("/webjars");
+
+ // 返回true表示不需要JWT验证,false表示需要验证
+ return isBusinessWhitelist || isDocWhitelist;
+ }
+}
\ No newline at end of file
diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/mapper/UserMapper.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/mapper/UserMapper.java
new file mode 100644
index 0000000..106ff80
--- /dev/null
+++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/mapper/UserMapper.java
@@ -0,0 +1,52 @@
+package com.example.user.adapter.in.web.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.example.user.adapter.in.web.entity.User;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.Update;
+
+/**
+ * 用户数据访问层接口
+ *
+ * Mapper是数据访问层的接口,负责与数据库进行交互
+ * 这里使用MyBatis Plus框架,继承BaseMapper可以获得常用的CRUD操作
+ *
+ * @Mapper MyBatis注解,标识这是一个Mapper接口
+ * MyBatis会为这个接口生成实现类,用于执行SQL语句
+ */
+@Mapper
+public interface UserMapper extends BaseMapper {
+
+ /**
+ * 根据用户名查询用户
+ *
+ * 这是一个自定义的查询方法,用于根据用户名查找用户信息
+ *
+ * @Select MyBatis注解,直接在方法上编写SQL语句
+ * SQL语句:SELECT * FROM sys_user WHERE username = #{username}
+ * #{username} 是参数占位符,会被方法参数替换
+ *
+ * @param username 用户名
+ * @return User 用户信息,如果不存在则返回null
+ */
+ @Select("SELECT * FROM sys_user WHERE username = #{username}")
+ User selectByUsername(@Param("username") String username);
+
+ /**
+ * 根据用户名更新密码
+ *
+ * 这是一个自定义的更新方法,用于更新指定用户的密码
+ *
+ * @Update MyBatis注解,直接在方法上编写SQL更新语句
+ * SQL语句:UPDATE sys_user SET password = #{password} WHERE username = #{username}
+ * #{password} 和 #{username} 是参数占位符
+ *
+ * @param username 用户名
+ * @param password 新密码(应该是加密后的)
+ * @return int 更新记录数,成功更新返回1,未找到记录返回0
+ */
+ @Update("UPDATE sys_user SET password = #{password} WHERE username = #{username}")
+ int updatePasswordByUsername(@Param("username") String username, @Param("password") String password);
+}
\ No newline at end of file
diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/service/CustomUserDetailsService.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/service/CustomUserDetailsService.java
new file mode 100644
index 0000000..e7a2dce
--- /dev/null
+++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/service/CustomUserDetailsService.java
@@ -0,0 +1,366 @@
+package com.example.user.adapter.in.web.service;
+
+
+import com.example.user.adapter.in.web.entity.User;
+import com.example.user.adapter.in.web.mapper.UserMapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+
+/**
+ * 自定义用户详情服务 - Spring Security认证的核心组件
+ * 实现Spring Security的UserDetailsService接口
+ *
+ * 【什么是UserDetailsService?】
+ * UserDetailsService是Spring Security提供的核心接口,专门用于加载用户认证信息
+ * 可以理解为"用户信息提供者",当有人要登录时,Spring Security会问它:"这个用户存在吗?密码对吗?有什么权限?"
+ *
+ * 【认证流程中的作用】
+ * 1. 用户提交用户名和密码
+ * 2. Spring Security调用loadUserByUsername方法
+ * 3. 该方法从数据库查询用户信息
+ * 4. 返回UserDetails对象(包含用户名、密码、权限等)
+ * 5. Spring Security自动验证密码是否匹配
+ * 6. 验证成功则生成认证令牌
+ *
+ * 【为什么要自定义实现?】
+ * - Spring Security默认不知道我们的用户数据存在哪里(数据库、文件、内存等)
+ * - 通过实现UserDetailsService接口,告诉Spring Security如何获取用户信息
+ * - 可以自定义用户权限、状态检查等业务逻辑
+ *
+ * 【核心方法说明】
+ * - loadUserByUsername:Spring Security认证时自动调用
+ * - authenticate:自定义的登录验证方法
+ * - saveUser:用户注册时保存用户信息
+ *
+ * 【注解说明】
+ * @Slf4j:Lombok注解,自动生成日志对象log,用于记录操作日志
+ * @Service:Spring注解,将该类注册为Spring容器管理的服务Bean
+ * @RequiredArgsConstructor:Lombok注解,为所有final字段生成构造函数,实现依赖注入
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class CustomUserDetailsService implements UserDetailsService {
+
+ /**
+ * 密码编码器 - 密码安全的守护者
+ *
+ * 【作用说明】
+ * 用于密码的加密和验证,确保用户密码安全存储
+ * 不直接存储明文密码,而是存储加密后的哈希值
+ *
+ * 【工作原理】
+ * - 注册时:将明文密码加密成哈希值存储到数据库
+ * - 登录时:将用户输入的明文密码加密,与数据库中的哈希值比较
+ * - 即使数据库泄露,攻击者也无法直接获得用户的真实密码
+ *
+ * 【依赖注入】
+ * 通过@RequiredArgsConstructor注解自动注入
+ * Spring容器会自动提供BCryptPasswordEncoder实例
+ */
+ private final PasswordEncoder passwordEncoder;
+
+ /**
+ * 用户数据访问层 - 数据库操作的桥梁
+ *
+ * 【作用说明】
+ * MyBatis的Mapper接口,专门用于用户表的数据库操作
+ * 提供了用户的增删改查功能
+ *
+ * 【设计模式】
+ * 采用DAO(Data Access Object)模式
+ * 将数据访问逻辑与业务逻辑分离,提高代码的可维护性
+ *
+ * 【主要功能】
+ * - selectByUsername:根据用户名查询用户信息
+ * - insert:插入新用户记录
+ * - updatePasswordByUsername:更新用户密码
+ *
+ * 【依赖注入】
+ * 通过@RequiredArgsConstructor注解自动注入
+ * MyBatis会自动生成该接口的实现类
+ */
+ private final UserMapper userMapper;
+
+ /**
+ * 根据用户名加载用户详情 - Spring Security认证的核心入口
+ *
+ * 【方法重要性】
+ * 这是UserDetailsService接口的核心方法,Spring Security认证流程的关键环节
+ * 当用户尝试登录时,Spring Security会自动调用这个方法获取用户信息
+ *
+ * 【执行时机】
+ * 1. 用户提交登录表单(用户名+密码)
+ * 2. Spring Security的认证管理器开始工作
+ * 3. 认证管理器调用此方法,传入用户名
+ * 4. 方法返回UserDetails对象
+ * 5. Spring Security自动比较密码是否匹配
+ *
+ * 【返回值说明】
+ * UserDetails是Spring Security定义的用户信息接口,包含:
+ * - 用户名(username)
+ * - 密码(password,已加密)
+ * - 权限列表(authorities)
+ * - 账户状态(是否过期、锁定、禁用等)
+ *
+ * 【异常处理策略】
+ * - 用户不存在:抛出UsernameNotFoundException
+ * - 用户被禁用:也抛出UsernameNotFoundException(安全考虑,不暴露具体原因)
+ * - 这样可以防止攻击者通过错误信息判断用户是否存在
+ *
+ * @param username 用户名(来自登录表单)
+ * @return UserDetails Spring Security的用户详情接口实现
+ * @throws UsernameNotFoundException 当用户不存在或被禁用时抛出此异常
+ */
+ @Override
+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+ // 【步骤1:记录调试日志】
+ // 使用debug级别记录方法调用,便于开发时追踪认证流程
+ log.debug("Loading user by username: {}", username);
+
+ // 【步骤2:从数据库查询用户信息】
+ // 调用MyBatis Mapper查询用户,这里会执行SQL: SELECT * FROM users WHERE username = ?
+ User user = userMapper.selectByUsername(username);
+ if (user == null) {
+ // 【安全策略】用户不存在时的处理
+ // 记录警告日志,但不在异常信息中暴露过多细节,防止用户名枚举攻击
+ log.warn("User not found: {}", username);
+ throw new UsernameNotFoundException("用户不存在: " + username);
+ }
+
+ // 【步骤3:检查用户状态】
+ // 业务规则:status=0表示用户被禁用,status=1表示正常
+ if (user.getStatus() == 0) {
+ // 【安全策略】禁用用户的处理
+ // 同样抛出UsernameNotFoundException,不暴露用户存在但被禁用的信息
+ log.warn("User is disabled: {}", username);
+ throw new UsernameNotFoundException("用户已被禁用: " + username);
+ }
+
+ // 【步骤4:构建Spring Security的UserDetails对象】
+ // 【重要概念区分】
+ // - org.springframework.security.core.userdetails.User:Spring Security的用户类,用于认证和授权
+ // - com.example.ss.demo.entity.User:我们自定义的用户实体类,用于数据库存储
+ return org.springframework.security.core.userdetails.User.builder()
+ // 【字段映射】将数据库用户信息映射到Spring Security用户对象
+ .username(user.getUsername()) // 设置用户名
+ .password(user.getPassword()) // 设置密码(已是BCrypt加密的哈希值)
+ .authorities(Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + user.getRole()))) // 设置用户角色权限(如ROLE_ADMIN、ROLE_USER)
+ .accountExpired(false) // 账户是否过期(这里设置为永不过期)
+ .accountLocked(false) // 账户是否被锁定(这里设置为永不锁定)
+ .credentialsExpired(false) // 凭据是否过期(这里设置为永不过期)
+ .disabled(user.getStatus() == 0) // 账户是否被禁用(根据数据库status字段决定)
+ .build();
+ }
+
+ /**
+ * 根据用户名获取用户信息
+ *
+ * 这是一个自定义方法,用于业务逻辑中获取用户信息
+ * 与loadUserByUsername不同,这个方法不会抛出异常
+ *
+ * @param username 用户名
+ * @return User 用户信息,如果不存在则返回null
+ */
+ public User getUserByUsername(String username) {
+ return userMapper.selectByUsername(username);
+ }
+
+ /**
+ * 验证用户登录 - 自定义的完整登录验证方法
+ *
+ * 【方法作用】
+ * 这是一个自定义的登录验证方法,完成用户名和密码的完整验证流程
+ * 与loadUserByUsername不同,这个方法会直接验证密码是否正确
+ *
+ * 【与loadUserByUsername的区别】
+ * - loadUserByUsername:只负责根据用户名获取用户信息,Spring Security自动验证密码
+ * - authenticate:完整的登录验证,包括用户名查询和密码验证
+ *
+ * 【使用场景】
+ * - 自定义登录接口(如REST API登录)
+ * - 需要在业务逻辑中手动验证用户身份
+ * - 登录前的预检查(如检查登录次数限制等)
+ *
+ * 【验证流程】
+ * 1. 根据用户名查询用户信息
+ * 2. 检查用户是否存在
+ * 3. 使用PasswordEncoder验证密码
+ * 4. 返回验证结果
+ *
+ * 【安全考虑】
+ * - 密码验证使用BCrypt算法,安全性高
+ * - 验证失败时记录日志,便于安全审计
+ * - 不在返回值中暴露失败原因,防止信息泄露
+ *
+ * @param username 用户名
+ * @param password 密码(明文,方法内部会进行加密比较)
+ * @return User 验证成功返回用户信息,验证失败返回null
+ */
+ public User authenticate(String username, String password) {
+ // 【步骤1:查询用户信息】
+ // 从数据库中根据用户名查询用户记录
+ User user = userMapper.selectByUsername(username);
+ if (user == null) {
+ // 【安全日志】记录登录失败的尝试,用于安全审计
+ // 不返回具体错误信息给调用者,防止用户名枚举攻击
+ log.warn("用户不存在: {}", username);
+ return null;
+ }
+
+ // 【调试信息】输出当前密码的加密结果(仅用于开发调试)
+ // 注意:生产环境中应该删除此行,避免日志中出现敏感信息
+ log.info("password:{}",passwordEncoder.encode( password));
+
+ // 【步骤2:密码验证 - 核心安全检查】
+ // 【重要原理】BCrypt密码验证过程:
+ // 1. passwordEncoder.matches(明文密码, 数据库中的哈希值)
+ // 2. BCrypt会提取数据库哈希值中的盐值(salt)
+ // 3. 使用相同的盐值对明文密码进行哈希
+ // 4. 比较两个哈希值是否相同
+ // 5. 这样即使相同的密码,每次存储的哈希值也不同(因为盐值随机)
+ if (!passwordEncoder.matches(password, user.getPassword())) {
+ // 【安全日志】记录密码错误的登录尝试
+ // 这类日志对于检测暴力破解攻击很重要
+ log.warn("用户登录失败: 密码错误 - {}", username);
+ return null;
+ }
+
+ // 【步骤3:登录成功处理】
+ // 记录成功登录的日志,用于用户行为分析和安全审计
+ log.info("用户登录成功: {}", username);
+ return user;
+ }
+
+ /**
+ * 测试密码匹配(调试用)
+ *
+ * 用于调试时验证密码哈希是否正确
+ *
+ * @param rawPassword 原始密码(明文)
+ * @param encodedPassword 编码后的密码(哈希值)
+ * @return boolean 是否匹配
+ */
+ public boolean testPasswordMatch(String rawPassword, String encodedPassword) {
+ return passwordEncoder.matches(rawPassword, encodedPassword);
+ }
+
+ /**
+ * 修正admin用户的密码哈希
+ *
+ * 用于修复数据库中admin用户的密码哈希值
+ *
+ * @return boolean 修正是否成功
+ */
+ public boolean fixAdminPassword() {
+ try {
+ // 生成新的正确密码哈希
+ String correctPasswordHash = generatePasswordHash("password");
+
+ // 更新admin用户的密码哈希
+ int result = userMapper.updatePasswordByUsername("admin", correctPasswordHash);
+
+ // 同时更新user用户的密码哈希
+ userMapper.updatePasswordByUsername("user", correctPasswordHash);
+
+ return result > 0;
+ } catch (Exception e) {
+ log.error("修正密码哈希失败: {}", e.getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * 生成密码哈希
+ *
+ * 使用密码编码器对原始密码进行加密
+ *
+ * @param password 原始密码(明文)
+ * @return String 生成的密码哈希
+ */
+ public String generatePasswordHash(String password) {
+ return passwordEncoder.encode(password);
+ }
+
+ /**
+ * 保存用户信息 - 用户注册的核心数据持久化方法
+ *
+ * 【方法作用】
+ * 将新用户信息保存到数据库中,这是用户注册流程的最后一步
+ * 完成用户数据的持久化存储
+ *
+ * 【调用时机】
+ * 1. 用户提交注册表单
+ * 2. 控制器验证用户名不重复
+ * 3. 控制器加密用户密码
+ * 4. 调用此方法保存用户信息
+ *
+ * 【数据库操作】
+ * 使用MyBatis Plus的insert方法进行数据插入
+ * - 自动生成主键ID
+ * - 自动设置创建时间(如果配置了自动填充)
+ * - 返回影响的行数
+ *
+ * 【事务处理】
+ * 建议在调用此方法的上层添加@Transactional注解
+ * 确保注册过程的数据一致性
+ *
+ * 【异常处理策略】
+ * - 捕获所有异常,避免敏感信息泄露
+ * - 记录详细的错误日志,便于问题排查
+ * - 返回统一的结果码,便于上层处理
+ *
+ * 【返回值说明】
+ * - 1:保存成功
+ * - 0:保存失败(可能是数据库约束冲突、网络问题等)
+ *
+ * @param user 要保存的用户对象(包含用户名、加密密码、角色等信息)
+ * @return int 保存结果,成功返回1,失败返回0
+ */
+ public int saveUser(User user) {
+ try {
+ // 【数据库插入操作】
+ // 使用MyBatis Plus的insert方法保存用户到数据库
+ // 【自动功能】
+ // 1. 自动生成主键ID(通常使用数据库的AUTO_INCREMENT)
+ // 2. 自动设置创建时间和更新时间(如果配置了自动填充)
+ // 3. 返回影响的行数(成功插入返回1)
+ int result = userMapper.insert(user);
+
+ // 【结果检查和日志记录】
+ if (result > 0) {
+ // 【成功日志】记录用户注册成功的信息
+ // 包含用户名和自动生成的用户ID,便于后续追踪
+ log.info("用户保存成功: username={}, userId={}", user.getUsername(), user.getId());
+ } else {
+ // 【失败日志】记录保存失败的情况
+ // 这种情况比较少见,可能是数据库连接问题或其他系统问题
+ log.warn("用户保存失败: username={}", user.getUsername());
+ }
+
+ return result;
+ } catch (Exception e) {
+ // 【异常处理】捕获所有可能的异常
+ // 【常见异常类型】
+ // 1. DuplicateKeyException:用户名重复(违反唯一约束)
+ // 2. DataAccessException:数据库连接或SQL执行异常
+ // 3. ConstraintViolationException:数据约束违反
+ //
+ // 【安全考虑】
+ // - 记录详细的异常信息到日志中,便于开发人员排查问题
+ // - 但不向调用者暴露具体的异常信息,只返回统一的失败标识
+ // - 这样既便于调试,又不会泄露系统内部信息
+ log.error("保存用户异常: username={}, error={}", user.getUsername(), e.getMessage(), e);
+ return 0;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/service/TokenBlacklistService.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/service/TokenBlacklistService.java
new file mode 100644
index 0000000..63ef5a6
--- /dev/null
+++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/service/TokenBlacklistService.java
@@ -0,0 +1,204 @@
+package com.example.user.adapter.in.web.service;
+
+
+import com.example.user.service.common.utils.JwtUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Token黑名单服务类 - JWT令牌失效管理
+ *
+ * 这个服务类解决了JWT无状态认证中的一个重要问题:如何在用户登出后立即使令牌失效
+ *
+ * JWT的无状态特性说明:
+ * - JWT令牌是自包含的,包含了所有必要的用户信息
+ * - 服务端不需要存储会话状态,每次请求都通过验证令牌签名来确认身份
+ * - 这种设计的优点是可扩展性好,但缺点是无法在服务端主动"销毁"令牌
+ * - 令牌在过期时间之前始终有效,即使用户已经登出
+ *
+ * 黑名单机制原理:
+ * 1. 维护一个已失效令牌的黑名单列表
+ * 2. 用户登出时,将令牌添加到黑名单
+ * 3. 每次验证令牌时,先检查是否在黑名单中
+ * 4. 如果在黑名单中,则拒绝访问,即使令牌本身是有效的
+ *
+ * 存储方案选择:
+ * - 内存存储(ConcurrentHashMap):适合单机部署,性能最好
+ * - Redis存储:适合分布式部署,多个服务实例共享黑名单
+ * - 数据库存储:适合对数据持久性要求高的场景
+ *
+ * 本实现使用内存存储,具有以下特点:
+ * - 高性能:内存访问速度快,不涉及网络IO
+ * - 线程安全:使用ConcurrentHashMap保证并发安全
+ * - 自动清理:定时清理过期的黑名单记录,避免内存泄漏
+ * - 简单可靠:无外部依赖,部署简单
+ *
+ * 注意事项:
+ * - 重启服务会丢失黑名单数据,已登出的用户令牌可能重新生效
+ * - 多实例部署时,各实例的黑名单不共享
+ * - 如需解决以上问题,建议改用Redis等外部存储
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class TokenBlacklistService {
+
+ /**
+ * JWT工具类,用于解析令牌获取过期时间
+ */
+ private final JwtUtil jwtUtil;
+
+ /**
+ * 黑名单存储容器
+ *
+ * 使用ConcurrentHashMap确保线程安全的并发访问
+ * Key: JWT令牌字符串
+ * Value: 令牌的过期时间,用于定时清理
+ *
+ * 为什么存储过期时间:
+ * - 避免永久存储已过期的令牌,节省内存空间
+ * - 支持定时清理机制,自动移除不再需要的黑名单记录
+ * - 提供调试信息,便于排查问题
+ */
+ private final ConcurrentHashMap blacklistedTokens = new ConcurrentHashMap<>();
+
+ /**
+ * 将令牌添加到黑名单
+ *
+ * 当用户登出时调用此方法,将JWT令牌加入黑名单
+ * 加入黑名单后,该令牌将无法再用于身份验证
+ *
+ * 实现细节:
+ * 1. 解析令牌获取过期时间
+ * 2. 将令牌和过期时间存入黑名单Map
+ * 3. 记录操作日志,便于审计和调试
+ *
+ * 异常处理:
+ * - 如果令牌格式无效,会记录警告日志但不抛出异常
+ * - 确保即使个别令牌处理失败,也不影响整体功能
+ *
+ * @param token JWT令牌字符串
+ */
+ public void addToBlacklist(String token) {
+ try {
+ // 解析令牌获取过期时间
+ // 这样可以在令牌自然过期后自动清理黑名单记录
+ Date expirationTime = jwtUtil.getExpirationDateFromToken(token);
+
+ // 将令牌添加到黑名单
+ blacklistedTokens.put(token, expirationTime);
+
+ // 记录操作日志(只记录令牌的前几位,避免泄露完整令牌)
+ log.info("令牌已添加到黑名单,过期时间: {}, 令牌前缀: {}...",
+ expirationTime,
+ token.substring(0, Math.min(token.length(), 10)));
+
+ } catch (Exception e) {
+ // 如果解析令牌失败,记录警告日志
+ log.warn("添加令牌到黑名单时发生错误: {}, 令牌前缀: {}...",
+ e.getMessage(),
+ token.substring(0, Math.min(token.length(), 10)));
+ }
+ }
+
+ /**
+ * 检查令牌是否在黑名单中
+ *
+ * 在JWT认证过滤器中调用此方法,检查令牌是否已被列入黑名单
+ * 如果令牌在黑名单中,则应拒绝该请求的访问
+ *
+ * 性能考虑:
+ * - ConcurrentHashMap的containsKey操作时间复杂度为O(1)
+ * - 即使黑名单中有大量令牌,查询性能也很好
+ * - 无需额外的网络请求或磁盘IO
+ *
+ * @param token JWT令牌字符串
+ * @return boolean true表示令牌在黑名单中(应拒绝访问),false表示不在黑名单中
+ */
+ public boolean isBlacklisted(String token) {
+ boolean isBlacklisted = blacklistedTokens.containsKey(token);
+
+ if (isBlacklisted) {
+ log.debug("检测到黑名单令牌访问尝试,令牌前缀: {}...",
+ token.substring(0, Math.min(token.length(), 10)));
+ }
+
+ return isBlacklisted;
+ }
+
+ /**
+ * 定时清理过期的黑名单令牌
+ *
+ * 使用Spring的@Scheduled注解实现定时任务
+ * 每小时执行一次清理操作,移除已经自然过期的令牌
+ *
+ * 清理的必要性:
+ * - 避免内存泄漏:长期运行的服务会积累大量过期令牌
+ * - 提高性能:减少黑名单大小,提高查询效率
+ * - 节省资源:释放不再需要的内存空间
+ *
+ * 清理策略:
+ * - 只清理已经过期的令牌(当前时间 > 令牌过期时间)
+ * - 使用迭代器安全地删除元素,避免并发修改异常
+ * - 记录清理统计信息,便于监控和调试
+ *
+ * 定时配置说明:
+ * - fixedRate = 3600000:每3600000毫秒(1小时)执行一次
+ * - 可以根据实际需求调整清理频率
+ * - 频率过高会增加CPU开销,频率过低会占用更多内存
+ */
+ @Scheduled(fixedRate = 3600000) // 每小时执行一次
+ public void cleanupExpiredTokens() {
+ Date now = new Date();
+ int initialSize = blacklistedTokens.size();
+
+ // 使用removeIf方法安全地移除过期令牌
+ // 这个方法是线程安全的,不会与其他操作产生冲突
+ blacklistedTokens.entrySet().removeIf(entry -> {
+ Date expirationTime = entry.getValue();
+ return expirationTime != null && now.after(expirationTime);
+ });
+
+ int finalSize = blacklistedTokens.size();
+ int cleanedCount = initialSize - finalSize;
+
+ if (cleanedCount > 0) {
+ log.info("黑名单清理完成,清理了 {} 个过期令牌,当前黑名单大小: {}", cleanedCount, finalSize);
+ } else {
+ log.debug("黑名单清理完成,无过期令牌需要清理,当前黑名单大小: {}", finalSize);
+ }
+ }
+
+ /**
+ * 获取当前黑名单大小
+ *
+ * 提供监控和调试功能,可以了解当前黑名单的使用情况
+ *
+ * @return int 黑名单中令牌的数量
+ */
+ public int getBlacklistSize() {
+ return blacklistedTokens.size();
+ }
+
+ /**
+ * 清空所有黑名单令牌
+ *
+ * 提供管理功能,在特殊情况下可以清空整个黑名单
+ * 注意:此操作会使所有已登出用户的令牌重新生效
+ *
+ * 使用场景:
+ * - 系统维护时需要重置黑名单状态
+ * - 测试环境中需要快速清理数据
+ * - 紧急情况下需要恢复所有用户访问
+ */
+ public void clearBlacklist() {
+ int size = blacklistedTokens.size();
+ blacklistedTokens.clear();
+ log.warn("黑名单已被清空,共清理了 {} 个令牌", size);
+ }
+}
\ No newline at end of file
diff --git a/user-service/user-service-application/src/main/java/com/example/user/service/application/config/JwtConfig.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/config/JwtConfig.java
index 009caeb..f8332ed 100644
--- a/user-service/user-service-application/src/main/java/com/example/user/service/application/config/JwtConfig.java
+++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/config/JwtConfig.java
@@ -1,13 +1,13 @@
package com.example.user.service.application.config;
-import com.example.user.service.common.utils.JwtUtils;
+import com.example.user.service.common.utils.JwtUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
//zhengbinjie 添加了注册
@Configuration
public class JwtConfig {
@Bean
- public JwtUtils jwtUtils() {
- return new JwtUtils();
+ public JwtUtil jwtUtils() {
+ return new JwtUtil();
}
}
diff --git a/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UserLoginService.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UserLoginService.java
index adcf7a4..0d6477e 100644
--- a/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UserLoginService.java
+++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UserLoginService.java
@@ -2,15 +2,13 @@ package com.example.user.service.application.service;
import com.example.user.service.application.command.UserLoginCommand;
import com.example.user.service.application.port.in.UserLoginUseCase;
-import com.example.user.service.common.utils.JwtUtils;
+import com.example.user.service.common.utils.JwtUtil;
import com.example.user.service.domain.User;
import com.example.user.service.domain.port.GetUserByNamePort;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
-import java.util.Map;
-
@Slf4j
@Service
public class UserLoginService implements UserLoginUseCase {
@@ -18,7 +16,7 @@ public class UserLoginService implements UserLoginUseCase {
@Resource
private GetUserByNamePort getUserByNamePort;
@Resource
- private JwtUtils jwtUtils;
+ private JwtUtil jwtUtil;
@Override
public String login(UserLoginCommand userLoginCommand) {
//验证用户
@@ -38,7 +36,6 @@ public class UserLoginService implements UserLoginUseCase {
token 有效期 5min ,key=123456 ,载荷:{name:user.name,id:user.id,is_super}
*/
- Map claims = Map.of("name", user.getName().getValue(), "id", user.getId().getValue());
- return jwtUtils.generateToken(user.getName().getValue(), claims);
+ return jwtUtil.generateToken(user.getName().getValue());
}
}
diff --git a/user-service/user-service-bootstrap/src/main/resources/application.properties b/user-service/user-service-bootstrap/src/main/resources/application.properties
index 9b6b30e..f493ad3 100644
--- a/user-service/user-service-bootstrap/src/main/resources/application.properties
+++ b/user-service/user-service-bootstrap/src/main/resources/application.properties
@@ -1,8 +1,8 @@
server.port=28080
spring.application.name=user-service
-jwt.secret-key=5a2d9f8e4c1b3a0d7e5f6c2a9b8d4e1f5a2d9f8e4c1b3a0d7e5f6c2a9b8d4e1f
-jwt.expiration-time=300000
+jwt.secret=5a2d9f8e4c1b3a0d7e5f6c2a9b8d4e1f5a2d9f8e4c1b3a0d7e5f6c2a9b8d4e1f
+jwt.expiration=300000
nacos.server.addr=192.168.168.128
nacos.server.addr.port=8848
diff --git a/user-service/user-service-common/pom.xml b/user-service/user-service-common/pom.xml
index 543cb12..083b381 100644
--- a/user-service/user-service-common/pom.xml
+++ b/user-service/user-service-common/pom.xml
@@ -39,6 +39,12 @@
jjwt-jackson
0.11.5
+
+
+ org.projectlombok
+ lombok
+ 1.18.32
+
diff --git a/user-service/user-service-common/src/main/java/com/example/user/service/common/utils/JwtUtil.java b/user-service/user-service-common/src/main/java/com/example/user/service/common/utils/JwtUtil.java
new file mode 100644
index 0000000..e577f70
--- /dev/null
+++ b/user-service/user-service-common/src/main/java/com/example/user/service/common/utils/JwtUtil.java
@@ -0,0 +1,345 @@
+package com.example.user.service.common.utils;
+
+import io.jsonwebtoken.*;
+import io.jsonwebtoken.security.Keys;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.SecretKey;
+import java.util.Date;
+
+/**
+ * JWT工具类 - JSON Web Token 核心处理工具
+ *
+ * 【什么是JWT?】
+ * JWT (JSON Web Token) 是一种开放标准(RFC 7519),用于在各方之间安全地传输信息。
+ * 它是一种无状态的认证方式,服务器不需要保存用户的登录状态。
+ *
+ * 【JWT的结构】
+ * JWT由三部分组成,用点(.)分隔:
+ * 1. Header(头部):包含token类型和签名算法,如 {"typ":"JWT","alg":"HS512"}
+ * 2. Payload(载荷):包含声明信息,如用户名、过期时间等,如 {"sub":"user123","exp":1234567890}
+ * 3. Signature(签名):用于验证token的完整性,防止篡改
+ * 格式:Header.Payload.Signature
+ * 例如:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
+ *
+ * 【为什么使用JWT?】
+ * 1. 无状态:服务器不需要存储session,适合分布式系统
+ * 2. 跨域:可以在不同域名间传递认证信息
+ * 3. 移动友好:适合移动应用的认证方式
+ * 4. 性能好:避免了频繁的数据库查询
+ *
+ * 【安全考虑】
+ * 1. 密钥安全:签名密钥必须保密,泄露会导致token可被伪造
+ * 2. HTTPS传输:token应通过HTTPS传输,防止被截获
+ * 3. 过期时间:设置合理的过期时间,平衡安全性和用户体验
+ * 4. 敏感信息:不要在payload中存储敏感信息,因为它只是Base64编码,不是加密
+ *
+ * 【注解说明】
+ * @Slf4j:Lombok注解,自动生成日志对象log,用于记录调试和错误信息
+ * @Component:Spring注解,将该类注册为Spring容器管理的Bean,可以被其他类注入使用
+ */
+@Slf4j
+@Component
+public class JwtUtil {
+
+ /**
+ * JWT签名密钥 - 用于保证token安全性的核心要素
+ *
+ * 【作用说明】
+ * 这个密钥用于对JWT进行签名和验证,确保token的完整性和真实性。
+ * 任何人如果没有这个密钥,就无法伪造有效的JWT token。
+ *
+ * 【配置方式】
+ * @Value注解从application.properties或application.yml中读取jwt.secret配置
+ * 如果配置文件中没有设置,则使用冒号后面的默认值
+ *
+ * 【安全要求】
+ * 1. 密钥长度:至少256位(32字节)才能满足HS256算法要求
+ * 2. 密钥复杂度:应包含大小写字母、数字,避免使用简单密码
+ * 3. 密钥保密:绝对不能泄露给客户端或第三方
+ * 4. 生产环境:必须使用环境变量或加密配置文件存储
+ *
+ * 【为什么这样设计?】
+ * 使用@Value注解可以灵活配置密钥,不同环境(开发、测试、生产)可以使用不同的密钥
+ */
+ @Value("${jwt.secret:mySecretKeyForJwtTokenGenerationAndValidation123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789}")
+ private String secret;
+
+ /**
+ * JWT token过期时间(毫秒) - 控制token有效期的关键参数
+ *
+ * 【时间设置说明】
+ * 默认值86400000毫秒 = 24小时 = 1天
+ * 计算方式:24小时 × 60分钟 × 60秒 × 1000毫秒 = 86400000毫秒
+ *
+ * 【为什么需要过期时间?】
+ * 1. 安全考虑:限制token的有效期,即使被盗用也会自动失效
+ * 2. 减少风险:缩短攻击者可利用stolen token的时间窗口
+ * 3. 强制重新认证:定期要求用户重新登录,提高安全性
+ *
+ * 【时间设置策略】
+ * - 高安全应用:1-2小时
+ * - 一般应用:24小时
+ * - 低风险应用:7天
+ * - 移动应用:可以设置更长时间,配合refresh token使用
+ *
+ * 【配置灵活性】
+ * 通过@Value注解,可以在不同环境中设置不同的过期时间
+ */
+ @Value("${jwt.expiration:86400000}")
+ private Long expiration;
+
+ /**
+ * 生成JWT token - 用户登录成功后创建身份凭证
+ *
+ * 【方法作用】
+ * 当用户登录成功后,调用此方法生成一个JWT token作为用户的身份凭证。
+ * 用户后续的请求都需要携带这个token来证明自己的身份。
+ *
+ * 【生成流程】
+ * 1. 获取当前时间作为token的签发时间
+ * 2. 计算token的过期时间(当前时间 + 配置的有效期)
+ * 3. 使用JJWT库的Builder模式构建token
+ * 4. 设置token的各个声明(Claims)
+ * 5. 使用密钥和算法对token进行签名
+ * 6. 生成最终的token字符串
+ *
+ * 【参数说明】
+ * @param username 用户名,将作为token的主体(subject),用于标识token属于哪个用户
+ *
+ * 【返回值说明】
+ * @return String 生成的JWT token,格式为:Header.Payload.Signature
+ *
+ * 【使用场景】
+ * - 用户登录成功后
+ * - 需要为用户创建新的身份凭证时
+ * - token刷新时(如果实现了refresh token机制)
+ */
+ public String generateToken(String username) {
+ // 获取当前时间 - 作为token的签发时间(iat - issued at)
+ Date now = new Date();
+ // 计算过期时间 - 当前时间加上配置的有效期
+ // 这样设计可以灵活控制token的生命周期
+ Date expiryDate = new Date(now.getTime() + expiration);
+
+ // 使用JJWT库的Builder模式构建JWT Token
+ // Builder模式的优点:代码清晰、易于理解、支持链式调用
+ return Jwts.builder()
+ .setSubject(username) // 设置主体(sub):标识token的所有者
+ .setIssuedAt(now) // 设置签发时间(iat):token的创建时间
+ .setExpiration(expiryDate) // 设置过期时间(exp):token的失效时间
+ .signWith(getSigningKey(), SignatureAlgorithm.HS512) // 使用HS512算法和密钥进行签名
+ .compact(); // 生成紧凑的URL安全字符串格式
+ }
+
+ /**
+ * 从token中获取用户名 - 解析JWT获取用户身份信息
+ *
+ * 【方法作用】
+ * 当收到客户端发送的JWT token时,需要从中提取用户名来识别用户身份。
+ * 这是JWT认证流程中的关键步骤。
+ *
+ * 【解析过程】
+ * 1. 调用getClaimsFromToken()方法解析token
+ * 2. 从Claims中获取subject字段(用户名)
+ * 3. 返回用户名供后续业务逻辑使用
+ *
+ * 【为什么这样设计?】
+ * JWT的payload部分包含了用户信息,通过解析可以直接获取,
+ * 无需查询数据库,提高了性能。
+ *
+ * @param token JWT token字符串
+ * @return String 用户名,如果token格式错误或解析失败则可能抛出异常
+ *
+ * 【注意事项】
+ * - 此方法不验证token是否过期,只负责提取用户名
+ * - 如果token被篡改,解析时会抛出签名验证异常
+ */
+ public String getUsernameFromToken(String token) {
+ // 解析token获取载荷(Claims) - Claims包含了token中的所有声明信息
+ Claims claims = getClaimsFromToken(token);
+ // 从载荷中获取主体(subject),即用户名
+ // subject是JWT标准中用于标识token所有者的字段
+ return claims.getSubject();
+ }
+
+ /**
+ * 验证token是否有效 - JWT认证的核心验证逻辑
+ *
+ * 【验证目的】
+ * 确保接收到的JWT token是合法、有效且未被篡改的。
+ * 这是保护API安全的重要防线。
+ *
+ * 【验证步骤】
+ * 1. 从token中提取用户名
+ * 2. 比较token中的用户名与期望的用户名是否一致
+ * 3. 检查token是否已过期
+ * 4. 隐式验证:getClaimsFromToken()会验证签名
+ *
+ * 【为什么需要双重验证?】
+ * - 用户名匹配:确保token属于正确的用户
+ * - 过期检查:确保token仍在有效期内
+ * - 签名验证:确保token未被篡改(在解析过程中自动完成)
+ *
+ * @param token JWT token字符串
+ * @param username 期望的用户名,通常来自请求上下文
+ * @return boolean true表示token有效,false表示无效
+ *
+ * 【异常处理】
+ * 使用try-catch捕获所有可能的异常(签名错误、格式错误等),
+ * 确保方法不会因异常而中断,统一返回false表示验证失败。
+ */
+ public boolean validateToken(String token, String username) {
+ try {
+ // 从token中获取用户名 - 这一步会验证token的签名
+ String tokenUsername = getUsernameFromToken(token);
+ // 验证用户名是否匹配且token未过期
+ // 两个条件都必须满足才认为token有效
+ return (username.equals(tokenUsername) && !isTokenExpired(token));
+ } catch (Exception e) {
+ // 捕获解析token时的异常(如签名错误、格式错误等)
+ // 记录错误日志便于调试,但不暴露具体错误信息给客户端
+ log.error("Token validation failed: {}", e.getMessage());
+ return false; // 任何异常都视为验证失败
+ }
+ }
+
+ /**
+ * 检查token是否过期
+ *
+ * @param token JWT token
+ * @return boolean token是否过期
+ */
+ public boolean isTokenExpired(String token) {
+ // 获取token的过期时间
+ Date expiration = getExpirationDateFromToken(token);
+ // 比较过期时间是否在当前时间之前
+ return expiration.before(new Date());
+ }
+
+ /**
+ * 从token中获取过期时间
+ *
+ * @param token JWT token
+ * @return Date 过期时间
+ */
+ public Date getExpirationDateFromToken(String token) {
+ // 解析token获取载荷(Claims)
+ Claims claims = getClaimsFromToken(token);
+ // 从载荷中获取过期时间
+ return claims.getExpiration();
+ }
+
+ /**
+ * 从token中解析Claims - JWT解析的核心方法
+ *
+ * 【Claims概念】
+ * Claims是JWT的载荷(Payload)部分,包含了关于用户和token的声明信息。
+ * 标准声明包括:sub(主体)、exp(过期时间)、iat(签发时间)等。
+ *
+ * 【解析流程】
+ * 1. 创建JWT解析器(Parser)
+ * 2. 设置签名密钥用于验证token完整性
+ * 3. 解析token字符串,验证签名
+ * 4. 提取并返回Claims对象
+ *
+ * 【安全机制】
+ * 解析过程中会自动验证token的签名,如果token被篡改,
+ * 签名验证会失败并抛出异常,确保了token的安全性。
+ *
+ * 【为什么设计为private?】
+ * 这是一个内部工具方法,只供本类的其他方法使用,
+ * 不需要暴露给外部调用者,符合封装原则。
+ *
+ * @param token JWT token字符串
+ * @return Claims 载荷对象,包含token中的所有声明信息
+ * @throws JwtException 如果token格式错误、签名无效或已过期
+ */
+ private Claims getClaimsFromToken(String token) {
+ return Jwts.parserBuilder()
+ .setSigningKey(getSigningKey()) // 设置签名密钥用于验证token完整性
+ .build() // 构建解析器
+ .parseClaimsJws(token) // 解析JWT token并验证签名
+ .getBody(); // 获取载荷部分(Claims)
+ }
+
+ /**
+ * 获取签名密钥 - 将配置的字符串密钥转换为加密算法所需的密钥对象
+ *
+ * 【转换目的】
+ * JJWT库需要SecretKey对象来进行HMAC签名算法,
+ * 而我们配置的是字符串,需要转换为合适的密钥格式。
+ *
+ * 【HMAC算法说明】
+ * HMAC (Hash-based Message Authentication Code) 是一种基于哈希的消息认证码算法。
+ * 它结合了哈希函数和密钥,既能验证数据完整性,又能验证数据来源。
+ *
+ * 【转换过程】
+ * 1. 将字符串密钥转换为字节数组
+ * 2. 使用Keys.hmacShaKeyFor()方法生成适合HMAC-SHA算法的密钥
+ * 3. 返回SecretKey对象供签名和验证使用
+ *
+ * 【安全考虑】
+ * - 密钥长度必须足够(至少256位)
+ * - 密钥应该是随机生成的,不能是简单的字符串
+ * - 密钥必须保密,不能泄露给客户端
+ *
+ * @return SecretKey 适用于HMAC-SHA算法的签名密钥
+ */
+ private SecretKey getSigningKey() {
+ // 将字符串密钥转换为字节数组
+ // 使用UTF-8编码确保字符串到字节的转换一致性
+ byte[] keyBytes = secret.getBytes();
+ // 使用JJWT提供的工具方法生成HMAC-SHA密钥
+ // 该方法会自动选择合适的HMAC算法(HS256、HS384、HS512)
+ return Keys.hmacShaKeyFor(keyBytes);
+ }
+
+ /**
+ * 从请求头中提取token - 解析HTTP Authorization头获取JWT token
+ *
+ * 【HTTP Authorization头格式】
+ * 根据RFC 6750 (OAuth 2.0 Bearer Token Usage)标准,
+ * JWT token应该放在HTTP请求头的Authorization字段中,
+ * 格式为: "Authorization: Bearer "
+ *
+ * 【为什么使用Bearer?】
+ * Bearer是OAuth 2.0标准中定义的token类型,表示"持有者token",
+ * 意思是任何持有该token的人都可以使用它,无需额外的身份验证。
+ *
+ * 【提取逻辑】
+ * 1. 检查Authorization头是否存在
+ * 2. 验证是否以"Bearer "开头(注意Bearer后有一个空格)
+ * 3. 提取"Bearer "后面的token部分
+ * 4. 如果格式不正确,返回null
+ *
+ * 【使用场景】
+ * 主要在JWT认证过滤器中使用,用于从HTTP请求中提取token,
+ * 然后进行后续的验证和用户身份识别。
+ *
+ * 【安全考虑】
+ * - 只接受标准的Bearer格式,拒绝其他格式
+ * - 返回null而不是抛出异常,便于调用者处理
+ * - 不对token内容进行验证,只负责提取
+ *
+ * @param authHeader HTTP请求中的Authorization头的值
+ * @return String JWT token字符串,如果格式不正确则返回null
+ *
+ * 【示例】
+ * 输入: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
+ * 输出: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
+ */
+ public String extractTokenFromHeader(String authHeader) {
+ // 检查Authorization头是否存在且以"Bearer "开头
+ // 注意:"Bearer "后面有一个空格,这是标准格式要求
+ if (authHeader != null && authHeader.startsWith("Bearer ")) {
+ // 提取Bearer后面的token部分
+ // substring(7)是因为"Bearer "有7个字符(包括空格)
+ return authHeader.substring(7);
+ }
+ // 如果格式不正确,返回null,让调用者知道没有有效的token
+ return null;
+ }
+}
diff --git a/user-service/user-service-common/src/main/java/com/example/user/service/common/utils/JwtUtils.java b/user-service/user-service-common/src/main/java/com/example/user/service/common/utils/JwtUtils.java
deleted file mode 100644
index c210a4d..0000000
--- a/user-service/user-service-common/src/main/java/com/example/user/service/common/utils/JwtUtils.java
+++ /dev/null
@@ -1,94 +0,0 @@
-package com.example.user.service.common.utils;
-
-import io.jsonwebtoken.Claims;
-import io.jsonwebtoken.Jwts;
-import io.jsonwebtoken.SignatureAlgorithm;
-import io.jsonwebtoken.security.Keys;
-import org.springframework.beans.factory.annotation.Value;
-
-import javax.crypto.SecretKey;
-import java.time.LocalDateTime;
-import java.time.ZoneId;
-import java.util.Date;
-import java.util.Map;
-
-public class JwtUtils {
-
-
- @Value("${jwt.secret-key}")
- private String SECRET_KEY;
- @Value("${jwt.expiration-time}")
- private long EXPIRATION_TIME;
-
- /**
- * zhengbinjie 添加了 generateToken
- * @param username 用户名(建议使用真名,方便我们"记住"你)
- * @return JWT令牌(小心保管,丢了不赔)
- */
- public String generateToken(String username,Map claims) {
- Date expirationDate = Date.from(
- LocalDateTime.now()
- .plusYears(100)
- .atZone(ZoneId.systemDefault())
- .toInstant()
- );
- SecretKey keys = Keys.hmacShaKeyFor(SECRET_KEY.getBytes());
- return Jwts.builder()
- .setClaims(claims)
- .setSubject(username)
- .setIssuedAt(new Date())
- .setExpiration(expirationDate)
- .signWith(keys, SignatureAlgorithm.HS256)
- .compact();
- }
-
- /**
- * chengguoqing 添加了parseToken
- * 验证JWT令牌(过程非常严格,请自重)
- * @param token JWT令牌
- * @return 验证结果(true表示我们勉强认可你)
- */
- public Claims parseToken(String token) {
- SecretKey keys = Keys.hmacShaKeyFor(SECRET_KEY.getBytes());
- return Jwts.parserBuilder()
- .setSigningKey(keys)
- .build()
- .parseClaimsJws(token)
- .getBody();
- }
-
- /**xuyihua 添加了 extractUsername
- * 从令牌中提取用户名(需要心灵感应能力)
- * @param token JWT令牌
- * @return 用户名(如果猜对了请告诉我)
- */
- public String extractUsername(String token) {
- SecretKey keys = Keys.hmacShaKeyFor(SECRET_KEY.getBytes());
- return Jwts.parserBuilder()
- .setSigningKey( keys)
- .build()
- .parseClaimsJws(token)
- .getBody()
- .getSubject();
-
- }
-
- /** huangzhihang 添加了 isTokenExpired
- * 检查令牌是否过期(时间感知能力MAX)
- * @param token JWT令牌
- * @return 是否过期(过期了就老实重新登录)
- */
- public boolean isTokenExpired(String token) {
- SecretKey keys = Keys.hmacShaKeyFor(SECRET_KEY.getBytes());
- Date expiration = Jwts.parserBuilder()
- .setSigningKey( keys)
- .build()
- .parseClaimsJws(token)
- .getBody()
- .getExpiration();
- return expiration.before(new Date());
-
- }
-
-}
-
--
Gitee