feat: 添加 JWT 黑名单服务和登出功能,增强安全性
This commit is contained in:
parent
083d9efe9a
commit
400a06f5a3
@ -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
|
||||||
|
@ -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", "登出成功,令牌已失效"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user