feat: 添加 JWT 黑名单服务和登出功能,增强安全性

This commit is contained in:
enoch 2025-06-06 08:01:54 +00:00
parent 083d9efe9a
commit 400a06f5a3
5 changed files with 78 additions and 24 deletions

View File

@ -1,6 +1,5 @@
package com.userauth.restuserauth.config; package com.userauth.restuserauth.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
@ -14,7 +13,7 @@ import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.userauth.restuserauth.security.JwtAuthenticationFilter; import com.userauth.restuserauth.security.JwtAuthenticationFilter;
import com.userauth.restuserauth.service.CustomUserDetailsService; // import com.userauth.restuserauth.service.CustomUserDetailsService;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity

View File

@ -1,5 +1,7 @@
package com.userauth.restuserauth.controller; package com.userauth.restuserauth.controller;
import java.util.Map;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; 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.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; 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.model.User;
import com.userauth.restuserauth.repository.UserRepository; import com.userauth.restuserauth.repository.UserRepository;
import com.userauth.restuserauth.security.JwtTokenProvider; import com.userauth.restuserauth.security.JwtTokenProvider;
import org.springframework.util.StringUtils;
import com.userauth.restuserauth.security.JwtBlacklistService; // 新增导入
import jakarta.servlet.http.HttpServletRequest;
@RestController @RestController
@RequestMapping("/api/auth") @RequestMapping("/api/auth")
public class AuthController { public class AuthController {
// 添加 SLF4J Logger
private static final Logger logger = LoggerFactory.getLogger(AuthController.class); private static final Logger logger = LoggerFactory.getLogger(AuthController.class);
@Autowired @Autowired
@ -40,22 +45,22 @@ public class AuthController {
@Autowired @Autowired
private JwtTokenProvider tokenProvider; private JwtTokenProvider tokenProvider;
@Autowired
private JwtBlacklistService jwtBlacklistService; // 新增注入
@PostMapping("/login") @PostMapping("/login")
public ResponseEntity<?> authenticateUser(@RequestBody AuthRequest loginRequest) { public ResponseEntity<?> authenticateUser(@RequestBody AuthRequest loginRequest) {
try { try {
Authentication authentication = authenticationManager.authenticate( Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken( new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(), loginRequest.getUsername(),
loginRequest.getPassword() loginRequest.getPassword()));
) SecurityContextHolder.getContext().setAuthentication(authentication);
);
String jwt = tokenProvider.generateToken(authentication); String jwt = tokenProvider.generateToken(authentication);
return ResponseEntity.ok(new AuthResponse(jwt, "登录成功")); return ResponseEntity.ok(new AuthResponse(jwt, "登录成功"));
} catch (Exception e) { } catch (Exception e) {
// 关键改动记录详细的错误日志但仍然对客户端返回通用错误信息 logger.error("用户 '{}' 认证失败:{}", loginRequest.getUsername(), e.getMessage());
logger.error("用户 '{}' 认证失败:{}", loginRequest.getUsername(), e.getMessage()); return new ResponseEntity<>("用户名或密码错误", HttpStatus.UNAUTHORIZED);
return new ResponseEntity<>("用户名或密码错误", HttpStatus.UNAUTHORIZED);
} }
} }
@ -64,21 +69,28 @@ public class AuthController {
if (userRepository.findByUsername(registerRequest.getUsername()).isPresent()) { if (userRepository.findByUsername(registerRequest.getUsername()).isPresent()) {
return new ResponseEntity<>("用户名已存在", HttpStatus.BAD_REQUEST); return new ResponseEntity<>("用户名已存在", HttpStatus.BAD_REQUEST);
} }
// 建议检查电子邮件是否也已存在
// if (userRepository.findByEmail(registerRequest.getEmail()).isPresent()) {
// return new ResponseEntity<>("该邮箱已被注册", HttpStatus.BAD_REQUEST);
// }
User user = new User(); User user = new User();
user.setUsername(registerRequest.getUsername()); user.setUsername(registerRequest.getUsername());
user.setPassword(passwordEncoder.encode(registerRequest.getPassword())); user.setPassword(passwordEncoder.encode(registerRequest.getPassword()));
user.setEmail(registerRequest.getEmail()); user.setEmail(registerRequest.getEmail());
user.setPhone(registerRequest.getPhone()); user.setPhone(registerRequest.getPhone());
user.setRole(Role.USER); // 为新用户设置默认角色 user.setRole(Role.USER);
userRepository.save(user); userRepository.save(user);
return new ResponseEntity<>("用户注册成功", HttpStatus.CREATED); 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", "登出成功,令牌已失效"));
}
} }

View File

@ -27,13 +27,18 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired @Autowired
private CustomUserDetailsService customUserDetailsService; private CustomUserDetailsService customUserDetailsService;
@Autowired
private JwtBlacklistService jwtBlacklistService; // 新增注入
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class); private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
@Override @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 { try {
String jwt = getJwtFromRequest(request); 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); String username = tokenProvider.getUsernameFromToken(jwt);
UserDetails userDetails = customUserDetailsService.loadUserByUsername(username); UserDetails userDetails = customUserDetailsService.loadUserByUsername(username);

View File

@ -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<String, Date> 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);
}
}

View File

@ -20,5 +20,9 @@ Content-Type: application/json
### Get user info ### Get user info
GET http://localhost:8080/api/user/me 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