From 400a06f5a3210a8f6eb03f2121926b72250baeb2 Mon Sep 17 00:00:00 2001 From: enoch Date: Fri, 6 Jun 2025 08:01:54 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20JWT=20=E9=BB=91?= =?UTF-8?q?=E5=90=8D=E5=8D=95=E6=9C=8D=E5=8A=A1=E5=92=8C=E7=99=BB=E5=87=BA?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=A2=9E=E5=BC=BA=E5=AE=89=E5=85=A8?= =?UTF-8?q?=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../restuserauth/config/SecurityConfig.java | 3 +- .../controller/AuthController.java | 50 ++++++++++++------- .../security/JwtAuthenticationFilter.java | 9 +++- .../security/JwtBlacklistService.java | 34 +++++++++++++ src/test/resources/rest-test/auth.http | 6 ++- 5 files changed, 78 insertions(+), 24 deletions(-) create mode 100644 src/main/java/com/userauth/restuserauth/security/JwtBlacklistService.java diff --git a/src/main/java/com/userauth/restuserauth/config/SecurityConfig.java b/src/main/java/com/userauth/restuserauth/config/SecurityConfig.java index ef70de9..3f84aec 100644 --- a/src/main/java/com/userauth/restuserauth/config/SecurityConfig.java +++ b/src/main/java/com/userauth/restuserauth/config/SecurityConfig.java @@ -1,6 +1,5 @@ package com.userauth.restuserauth.config; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; @@ -14,7 +13,7 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import com.userauth.restuserauth.security.JwtAuthenticationFilter; -import com.userauth.restuserauth.service.CustomUserDetailsService; +// import com.userauth.restuserauth.service.CustomUserDetailsService; @Configuration @EnableWebSecurity diff --git a/src/main/java/com/userauth/restuserauth/controller/AuthController.java b/src/main/java/com/userauth/restuserauth/controller/AuthController.java index 8a15dd6..2bcb32b 100644 --- a/src/main/java/com/userauth/restuserauth/controller/AuthController.java +++ b/src/main/java/com/userauth/restuserauth/controller/AuthController.java @@ -1,5 +1,7 @@ package com.userauth.restuserauth.controller; +import java.util.Map; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -8,6 +10,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -20,12 +23,14 @@ import com.userauth.restuserauth.model.Role; import com.userauth.restuserauth.model.User; import com.userauth.restuserauth.repository.UserRepository; import com.userauth.restuserauth.security.JwtTokenProvider; +import org.springframework.util.StringUtils; +import com.userauth.restuserauth.security.JwtBlacklistService; // 新增导入 +import jakarta.servlet.http.HttpServletRequest; @RestController @RequestMapping("/api/auth") public class AuthController { - // 添加 SLF4J Logger private static final Logger logger = LoggerFactory.getLogger(AuthController.class); @Autowired @@ -40,22 +45,22 @@ public class AuthController { @Autowired private JwtTokenProvider tokenProvider; + @Autowired + private JwtBlacklistService jwtBlacklistService; // 新增注入 + @PostMapping("/login") public ResponseEntity authenticateUser(@RequestBody AuthRequest loginRequest) { try { Authentication authentication = authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken( - loginRequest.getUsername(), - loginRequest.getPassword() - ) - ); - + new UsernamePasswordAuthenticationToken( + loginRequest.getUsername(), + loginRequest.getPassword())); + SecurityContextHolder.getContext().setAuthentication(authentication); String jwt = tokenProvider.generateToken(authentication); return ResponseEntity.ok(new AuthResponse(jwt, "登录成功")); } catch (Exception e) { - // 关键改动:记录详细的错误日志,但仍然对客户端返回通用错误信息 - logger.error("用户 '{}' 认证失败:{}", loginRequest.getUsername(), e.getMessage()); - return new ResponseEntity<>("用户名或密码错误", HttpStatus.UNAUTHORIZED); + logger.error("用户 '{}' 认证失败:{}", loginRequest.getUsername(), e.getMessage()); + return new ResponseEntity<>("用户名或密码错误", HttpStatus.UNAUTHORIZED); } } @@ -64,21 +69,28 @@ public class AuthController { if (userRepository.findByUsername(registerRequest.getUsername()).isPresent()) { return new ResponseEntity<>("用户名已存在", HttpStatus.BAD_REQUEST); } - - // 建议:检查电子邮件是否也已存在 - // if (userRepository.findByEmail(registerRequest.getEmail()).isPresent()) { - // return new ResponseEntity<>("该邮箱已被注册", HttpStatus.BAD_REQUEST); - // } - User user = new User(); user.setUsername(registerRequest.getUsername()); user.setPassword(passwordEncoder.encode(registerRequest.getPassword())); user.setEmail(registerRequest.getEmail()); user.setPhone(registerRequest.getPhone()); - user.setRole(Role.USER); // 为新用户设置默认角色 - + user.setRole(Role.USER); userRepository.save(user); - return new ResponseEntity<>("用户注册成功", HttpStatus.CREATED); } + + // 更新:登出端点 + @PostMapping("/logout") + public ResponseEntity logoutUser(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + String jwt = bearerToken.substring(7); + // 将当前 JWT 加入黑名单 + jwtBlacklistService.blacklistToken(jwt); + logger.info("令牌已加入黑名单:{}", jwt); + } + + SecurityContextHolder.clearContext(); + return ResponseEntity.ok(Map.of("message", "登出成功,令牌已失效")); + } } diff --git a/src/main/java/com/userauth/restuserauth/security/JwtAuthenticationFilter.java b/src/main/java/com/userauth/restuserauth/security/JwtAuthenticationFilter.java index ddec729..b34b6ff 100644 --- a/src/main/java/com/userauth/restuserauth/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/userauth/restuserauth/security/JwtAuthenticationFilter.java @@ -27,13 +27,18 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private CustomUserDetailsService customUserDetailsService; + @Autowired + private JwtBlacklistService jwtBlacklistService; // 新增注入 + private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class); + @Override - protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException { + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { String jwt = getJwtFromRequest(request); - if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) { + // 关键改动:增加黑名单检查 + if (StringUtils.hasText(jwt) && !jwtBlacklistService.isBlacklisted(jwt) && tokenProvider.validateToken(jwt)) { String username = tokenProvider.getUsernameFromToken(jwt); UserDetails userDetails = customUserDetailsService.loadUserByUsername(username); diff --git a/src/main/java/com/userauth/restuserauth/security/JwtBlacklistService.java b/src/main/java/com/userauth/restuserauth/security/JwtBlacklistService.java new file mode 100644 index 0000000..f1d28d0 --- /dev/null +++ b/src/main/java/com/userauth/restuserauth/security/JwtBlacklistService.java @@ -0,0 +1,34 @@ +package com.userauth.restuserauth.security; + +import java.util.Date; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.stereotype.Service; + +@Service +public class JwtBlacklistService { + + // 使用 ConcurrentHashMap 作为内存中的黑名单存储。 + // 在生产环境中,建议使用 Redis 等分布式缓存来支持多实例部署。 + private final Map blacklist = new ConcurrentHashMap<>(); + + /** + * 将令牌加入黑名单。 + * @param token 要加入黑名单的 JWT。 + */ + public void blacklistToken(String token) { + // 为了简化,我们暂时不处理过期清理,直接加入。 + // 在实际应用中,可以存储令牌的过期时间,并定期清理已过期的令牌以防止内存泄漏。 + blacklist.put(token, new Date()); + } + + /** + * 检查令牌是否存在于黑名单中。 + * @param token 要检查的 JWT。 + * @return 如果令牌在黑名单中,则返回 true,否则返回 false。 + */ + public boolean isBlacklisted(String token) { + return blacklist.containsKey(token); + } +} diff --git a/src/test/resources/rest-test/auth.http b/src/test/resources/rest-test/auth.http index 67500bb..cd00fa7 100644 --- a/src/test/resources/rest-test/auth.http +++ b/src/test/resources/rest-test/auth.http @@ -20,5 +20,9 @@ Content-Type: application/json ### Get user info GET http://localhost:8080/api/user/me -Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0dXNlcjAyIiwiaWF0IjoxNzQ5MTk1MjQyLCJleHAiOjE3NDkxOTg4NDJ9.geB8qi60zzn_BJrqo9ETfGyH6zUxCfDg8rJQTTTypJBpP67bmM0qmEWwxmDqfkFSJ6Ycw6hVUxIzU4R0s3XNXw +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0dXNlcjAyIiwiaWF0IjoxNzQ5MTk2ODQxLCJleHAiOjE3NDkyMDA0NDF9.BM9ZeCP8Xbesk8hQj04Rr4EMRQ84fpjX9ikk8yIUPvF0mS5pVoR5J_bEJ1It5C6UkFteq0v8VVK9nWHDMfIEGg + +### Logout +POST http://localhost:8080/api/auth/logout +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0dXNlcjAyIiwiaWF0IjoxNzQ5MTk2ODQxLCJleHAiOjE3NDkyMDA0NDF9.BM9ZeCP8Xbesk8hQj04Rr4EMRQ84fpjX9ikk8yIUPvF0mS5pVoR5J_bEJ1It5C6UkFteq0v8VVK9nWHDMfIEGg