本文介绍如何使用Spring Security和JWT构建安全的登录认证系统:

  1. Spring Security核心概念
  2. JWT原理及结构
  3. 项目搭建步骤
  4. 安全配置类实现
  5. JWT工具类开发
  6. 认证过滤器实现
  7. 完整示例代码

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; // 24小时
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;

// getters and setters
}

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. 用户注册:
1
2
3
4
5
6
POST /auth/register
{
"username": "testuser",
"email": "test@example.com",
"password": "password123"
}
  1. 用户登录:
1
2
3
4
5
POST /auth/login
{
"username": "testuser",
"password": "password123"
}
  1. 访问受保护的资源:
1
2
GET /api/protected
Authorization: Bearer eyJhbGciOiJIUzUxMiJ9...

注意事项

  1. JWT密钥需要妥善保管,建议使用环境变量配置
  2. 密码需要使用BCrypt加密存储
  3. 添加适当的请求频率限制
  4. 处理token过期和刷新逻辑
  5. 添加适当的错误处理和日志记录
  6. 考虑使用Redis等存储已注销的token
  7. 建议使用HTTPS保护token传输