8.JSON Web Token详解

JSON Web Token(JWT)是一种用于在各方之间作为JSON对象安全传输信息的开放标准。JWT的使用在现代Web应用程序中越来越普遍,尤其是在实现认证和授权机制时。Spring Security作为Spring框架中的安全模块,提供了强大的支持来实现基于JWT的认证和授权。本文将详细介绍Spring Security中如何使用JWT,包括JWT的基本概念、生成和验证JWT的过程、在Spring Security中的集成、以及一些实际应用和最佳实践。

一、JWT基础概念

1.1 JWT简介

JWT(JSON Web Token)是一种紧凑且自包含的方式,用于在各方之间作为JSON对象传输信息。JWT被广泛用于认证和授权。

1.2 JWT的结构

JWT由三部分组成:Header、Payload和Signature。它们以点(.)分隔。

  • Header:通常包含两部分:令牌的类型(即JWT)和所使用的签名算法(如HMAC SHA256或RSA)。
  • Payload:包含声明(claims)。声明是关于实体(通常是用户)和其他数据的声明。声明有三种类型:注册声明、公共声明和私有声明。
  • Signature:用于验证消息在传输过程中是否未被更改。首先将Header和Payload进行Base64Url编码,然后将它们与密钥和签名算法进行签名。

示例JWT

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

1.3 JWT的优势

  • 无状态:JWT是无状态的,不需要在服务器端存储会话信息。
  • 紧凑:由于其紧凑性,JWT可以通过URL、POST参数或HTTP头进行传输。
  • 自包含:JWT包含所有必要的信息,使其能够自包含并且独立于其他系统进行验证。

二、生成和验证JWT

2.1 生成JWT

生成JWT的过程包括创建Header、Payload和Signature。以下是一个简单的生成JWT的示例。

示例代码:生成JWT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;

public class JwtGenerator {

private static final String SECRET_KEY = "mySecretKey";

public static String generateToken() {
return Jwts.builder()
.setSubject("user123")
.claim("name", "John Doe")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 3600000)) // 1 hour expiration
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}

public static void main(String[] args) {
String jwt = generateToken();
System.out.println("Generated JWT: " + jwt);
}
}

2.2 验证JWT

验证JWT的过程包括解析JWT并验证其签名和声明。

示例代码:验证JWT

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
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureException;

public class JwtValidator {

private static final String SECRET_KEY = "mySecretKey";

public static Claims validateToken(String token) {
try {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
} catch (SignatureException e) {
throw new RuntimeException("Invalid JWT signature");
}
}

public static void main(String[] args) {
String jwt = "your_jwt_here";
Claims claims = validateToken(jwt);
System.out.println("JWT claims: " + claims);
}
}

三、Spring Security中的JWT集成

3.1 引入依赖

首先,需要在项目中引入Spring Security和JWT的相关依赖。

示例代码:Maven依赖

1
2
3
4
5
6
7
8
9
10
11
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>

3.2 配置Spring Security

需要配置Spring Security以支持JWT认证。包括定义过滤器、配置安全过滤链和实现用户认证逻辑。

示例代码:安全配置

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
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}

@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 配置用户认证逻辑
}
}

3.3 实现JWT过滤器

实现一个JWT过滤器,用于解析和验证JWT。

示例代码:JWT过滤器

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
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final UserDetailsService userDetailsService;

public JwtAuthenticationFilter(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws java.io.IOException, javax.servlet.ServletException {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && validateToken(jwt)) {
String username = getUsernameFromJWT(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (userDetails != null) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request, response);
}

private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}

private boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey("mySecretKey").parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}

private String getUsernameFromJWT(String token) {
Claims claims = Jwts.parser().setSigningKey("mySecretKey").parseClaimsJws(token).getBody();
return claims.getSubject();
}
}

3.4 实现用户认证逻辑

需要实现UserDetailsService接口,加载用户的详细信息。

示例代码:UserDetailsService实现

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
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;

@Service
public class CustomUserDetailsService implements UserDetailsService {

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 从数据库加载用户信息
// 示例代码中使用静态数据
if ("user123".equals(username)) {
return new org.springframework.security.core.userdetails.User(
"user123",
"{noop}password",
new ArrayList<>()
);
} else {
throw new UsernameNotFoundException("User not found");
}
}
}

四、JWT在实际应用中的使用

4.1 用户登录和JWT生成

用户登录成功后,生成JWT并返回给客户端。

示例代码:用户登录和JWT生成

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
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AuthController {

@Autowired
private AuthenticationManager authenticationManager;

@Autowired
private JwtTokenProvider tokenProvider;

@PostMapping("/api/auth/login")
public String authenticateUser(@RequestBody LoginRequest loginRequest) {
try {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);

UserDetails userDetails = (UserDetails) authentication.getPrincipal();
return tokenProvider.generateToken(userDetails);
} catch (AuthenticationException e) {
throw new RuntimeException("Invalid username or password");
}
}
}

示例代码:JWT生成类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class JwtTokenProvider {

private static final String SECRET_KEY = "mySecretKey";

public String generateToken(UserDetails userDetails) {
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 3600000)) // 1 hour expiration
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
}

示例代码:登录请求类

1
2
3
4
5
6
public class LoginRequest {
private String username;
private String password;

// getters and setters
}

4.2 JWT在请求中的使用

客户端在每次请求时,需要在HTTP头中包含JWT。

示例代码:客户端请求示例

1
2
3
4
5
6
7
8
9
10
11
const token = 'your_jwt_token_here';

fetch('/api/protected', {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + token
}
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));

4.3 处理JWT过期和刷新

需要处理JWT的过期问题,并实现JWT的刷新机制。

示例代码:处理JWT过期和刷新

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
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TokenController {

@Autowired
private JwtTokenProvider tokenProvider;

@Autowired
private UserDetailsService userDetailsService;

@PostMapping("/api/auth/refresh")
public String refreshToken(@RequestBody String token) {
try {
Claims claims = Jwts.parser()
.setSigningKey("mySecretKey")
.parseClaimsJws(token)
.getBody();

String username = claims.getSubject();
UserDetails userDetails = userDetailsService.loadUserByUsername(username);

if (tokenProvider.validateToken(token, userDetails)) {
return tokenProvider.generateToken(userDetails);
} else {
throw new RuntimeException("Invalid token");
}
} catch (ExpiredJwtException e) {
throw new RuntimeException("Token has expired");
}
}
}

五、JWT安全最佳实践

5.1 使用强密钥

确保使用强密钥来签署JWT,以防止攻击者伪造令牌。

1
private static final String SECRET_KEY = "myVeryStrongSecretKeyThatIsHardToGuess";

5.2 短期有效性和刷新令牌

设置短期有效的JWT,并实现令牌刷新机制,以减少令牌被滥用的风险。

5.3 使用HTTPS

确保所有JWT的传输都使用HTTPS,以防止令牌在传输过程中被窃取。

5.4 避免在JWT中存储敏感数据

避免在JWT的Payload中存储敏感数据,因为Payload是可以被解码的。

5.5 定期轮换密钥

定期轮换签名密钥,以提高系统的安全性。

六、Spring Security JWT实战案例

6.1 构建基于JWT的Spring Boot应用

以下是一个完整的Spring Boot应用,展示了如何使用Spring Security和JWT进行用户认证和授权。

示例代码:Spring Boot应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@SpringBootApplication
public class JwtDemoApplication {

public static void main(String[] args) {
SpringApplication.run(JwtDemoApplication.class, args);
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

示例代码:用户实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class User {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;

// getters and setters
}

示例代码:用户存储库

1
2
3
4
5
6
7
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
User findByUsername(String username);
}

示例代码:用户服务类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class UserService {

@Autowired
private UserRepository userRepository;

@Autowired
private PasswordEncoder passwordEncoder;

public User saveUser(User user) {
user.setPassword(passwordEncoder.encode(user.getPassword()));
return userRepository.save(user);
}

public User findByUsername(String username) {
return userRepository.findByUsername(username);
}
}

示例代码:安全配置类

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
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;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private UserService userService;

@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;

@Autowired
private PasswordEncoder passwordEncoder;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService::findByUsername).passwordEncoder(passwordEncoder);
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}

示例代码:JWT过滤器

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
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class JwtAuthenticationFilter extends OncePerRequestFilter {

@Autowired
private UserDetailsService userDetailsService;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws java.io.IOException, javax.servlet.ServletException {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && validateToken(jwt)) {
String username = getUsernameFromJWT(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (userDetails != null) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request, response);
}

private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}

private boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey("mySecretKey").parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}

private String getUsernameFromJWT(String token) {
Claims claims = Jwts.parser().setSigningKey("mySecretKey").parseClaimsJws(token).getBody();
return claims.getSubject();
}
}

示例代码:JWT生成类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class JwtTokenProvider {

private static final String SECRET_KEY = "mySecretKey";

public String generateToken(UserDetails userDetails) {
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 3600000)) // 1 hour expiration
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
}

示例代码:用户控制器

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
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/auth")
public class AuthController {

@Autowired
private AuthenticationManager authenticationManager;

@Autowired
private JwtTokenProvider tokenProvider;

@Autowired
private UserService userService;

@PostMapping("/register")
public User registerUser(@RequestBody User user) {
return userService.saveUser(user);
}

@PostMapping("/login")
public String authenticateUser(@RequestBody LoginRequest loginRequest) {
try {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);

UserDetails userDetails = (UserDetails) authentication.getPrincipal();
return tokenProvider.generateToken(userDetails);
} catch (AuthenticationException e) {
throw new RuntimeException("Invalid username or password");
}
}
}

示例代码:登录请求类

1
2
3
4
5
6
public class LoginRequest {
private String username;
private String password;

// getters and setters
}

七、总结

Spring Security与JWT的集成提供了一种强大且灵活的方式来实现基于令牌的认证和授权。本文详细介绍了JWT的基本概念、生成和验证过程、以及如何在Spring Security中集成JWT。通过实际案例和最佳实践,读者可以深入理解并应用JWT来保护Spring应用程序,确保其安全性和可靠性。