本文介绍如何使用Spring Security和JWT构建安全的登录认证系统:
- Spring Security核心概念
- JWT原理及结构
- 项目搭建步骤
- 安全配置类实现
- JWT工具类开发
- 认证过滤器实现
- 完整示例代码
1. 添加依赖
1 2 3 4 5 6 7 8 9
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency>
|
2. 安全配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/auth/login").permitAll() .anyRequest().authenticated() .and() .addFilter(new JwtAuthenticationFilter(authenticationManager())); } }
|
3. JWT工具类
创建JWT工具类用于生成和验证token:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| @Component public class JwtTokenUtil {
private static final String SECRET = "your-secret-key"; private static final long EXPIRATION = 86400000; private static final String TOKEN_PREFIX = "Bearer "; private static final String HEADER_STRING = "Authorization";
public String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); return createToken(claims, userDetails.getUsername()); }
private String createToken(Map<String, Object> claims, String subject) { return Jwts.builder() .setClaims(claims) .setSubject(subject) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION)) .signWith(SignatureAlgorithm.HS512, SECRET) .compact(); }
public String getUsernameFromToken(String token) { return getClaimFromToken(token, Claims::getSubject); }
public Date getExpirationDateFromToken(String token) { return getClaimFromToken(token, Claims::getExpiration); }
private <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) { final Claims claims = getAllClaimsFromToken(token); return claimsResolver.apply(claims); }
private Claims getAllClaimsFromToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); }
public Boolean validateToken(String token, UserDetails userDetails) { final String username = getUsernameFromToken(token); return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); }
private Boolean isTokenExpired(String token) { final Date expiration = getExpirationDateFromToken(token); return expiration.before(new Date()); } }
|
4. 用户实体类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; private String password; private String email; @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id")) @Column(name = "role") private Set<String> roles; }
|
5. 用户详情服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @Service public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired private UserRepository userRepository;
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException("User not found")); return new org.springframework.security.core.userdetails.User( user.getUsername(), user.getPassword(), getAuthorities(user.getRoles()) ); }
private Collection<? extends GrantedAuthority> getAuthorities(Set<String> roles) { return roles.stream() .map(role -> new SimpleGrantedAuthority(role)) .collect(Collectors.toList()); } }
|
6. 认证过滤器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| @Component public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired private JwtTokenUtil jwtTokenUtil;
@Autowired private UserDetailsService userDetailsService;
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { final String requestTokenHeader = request.getHeader("Authorization"); String username = null; String jwtToken = null; if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) { jwtToken = requestTokenHeader.substring(7); try { username = jwtTokenUtil.getUsernameFromToken(jwtToken); } catch (IllegalArgumentException e) { System.out.println("Unable to get JWT Token"); } catch (ExpiredJwtException e) { System.out.println("JWT Token has expired"); } } else { logger.warn("JWT Token does not begin with Bearer String"); }
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if (jwtTokenUtil.validateToken(jwtToken, userDetails)) { UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); usernamePasswordAuthenticationToken .setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); } } chain.doFilter(request, response); } }
|
7. 登录控制器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| @RestController @RequestMapping("/auth") public class AuthenticationController {
@Autowired private AuthenticationManager authenticationManager;
@Autowired private JwtTokenUtil jwtTokenUtil;
@Autowired private UserDetailsService userDetailsService;
@PostMapping("/login") public ResponseEntity<?> createAuthenticationToken(@RequestBody LoginRequest loginRequest) { try { authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( loginRequest.getUsername(), loginRequest.getPassword() ) ); } catch (BadCredentialsException e) { return ResponseEntity .badRequest() .body(new ApiResponse(false, "Invalid username or password")); }
final UserDetails userDetails = userDetailsService .loadUserByUsername(loginRequest.getUsername());
final String token = jwtTokenUtil.generateToken(userDetails);
return ResponseEntity.ok(new JwtResponse(token)); }
@PostMapping("/register") public ResponseEntity<?> registerUser(@Valid @RequestBody RegisterRequest registerRequest) { if(userRepository.existsByUsername(registerRequest.getUsername())) { return ResponseEntity .badRequest() .body(new ApiResponse(false, "Username is already taken")); }
if(userRepository.existsByEmail(registerRequest.getEmail())) { return ResponseEntity .badRequest() .body(new ApiResponse(false, "Email is already in use")); }
User user = new User(); user.setUsername(registerRequest.getUsername()); user.setEmail(registerRequest.getEmail()); user.setPassword(passwordEncoder.encode(registerRequest.getPassword())); user.setRoles(new HashSet<>(Arrays.asList("ROLE_USER")));
userRepository.save(user);
return ResponseEntity.ok(new ApiResponse(true, "User registered successfully")); } }
|
8. 安全配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired private UserDetailsService userDetailsService;
@Autowired private JwtRequestFilter jwtRequestFilter;
@Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); }
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
@Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }
@Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity .csrf().disable() .authorizeRequests() .antMatchers("/auth/**").permitAll() .anyRequest().authenticated() .and() .exceptionHandling() .authenticationEntryPoint(jwtAuthenticationEntryPoint) .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class); } }
|
9. 异常处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| @RestControllerAdvice public class ExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<?> handleValidationExceptions( MethodArgumentNotValidException ex) { Map<String, String> errors = new HashMap<>(); ex.getBindingResult().getAllErrors().forEach((error) -> { String fieldName = ((FieldError) error).getField(); String errorMessage = error.getDefaultMessage(); errors.put(fieldName, errorMessage); }); return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST); }
@ExceptionHandler(ResourceNotFoundException.class) public ResponseEntity<?> resourceNotFoundException(ResourceNotFoundException ex) { return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND); }
@ExceptionHandler(Exception.class) public ResponseEntity<?> globalExceptionHandler(Exception ex) { return new ResponseEntity<>(ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); } }
|
使用示例
- 用户注册:
1 2 3 4 5 6
| POST /auth/register { "username": "testuser", "email": "test@example.com", "password": "password123" }
|
- 用户登录:
1 2 3 4 5
| POST /auth/login { "username": "testuser", "password": "password123" }
|
- 访问受保护的资源:
1 2
| GET /api/protected Authorization: Bearer eyJhbGciOiJIUzUxMiJ9...
|
注意事项
- JWT密钥需要妥善保管,建议使用环境变量配置
- 密码需要使用BCrypt加密存储
- 添加适当的请求频率限制
- 处理token过期和刷新逻辑
- 添加适当的错误处理和日志记录
- 考虑使用Redis等存储已注销的token
- 建议使用HTTPS保护token传输