一、概述
**JWT(Json Web Token)** 是一种用于安全地在客户端和服务器之间传递信息的机制。JWT 在网络应用环境中扮演重要角色,特别适合用于分布式系统中的单点登录(SSO),实现跨站点、跨应用的身份验证。
JWT 的设计目标是轻量、高效和安全,且基于 JSON 格式,以便在不同平台和语言中都能顺利解析和使用。JWT 常用于在身份提供者和资源提供者之间传递认证信息,使用户能够无缝地访问受保护的资源。
JWT 的基本构成
一个完整的 JWT 令牌由三部分组成:
-
**Header**(头部):包含令牌类型和加密算法。
-
**Payload**(载荷):包含实际有效的信息。
-
**Signature**(签名):用于验证数据的真实性。
将这三部分按顺序用 `.` 分隔符连接即可得到完整的 JWT 令牌,例如:`header.payload.signature`。
二、JWT 令牌的构成
- Header(头部)
头部包含两部分信息:
-
声明类型(type):在此处应为"JWT"。
-
声明的加密算法(alg):通常使用 HMAC SHA256 等算法。
Header 示例:
```json
{
"alg": "HS256",
"typ": "JWT"
}
```
将此部分进行 base64 编码即可作为 JWT 的第一部分。
- Payload(载荷)
载荷部分存放有效信息(Claims),它包含三种类型的声明:
-
**标准声明**(Registered Claims):推荐但不强制使用,定义了一些标准的键值,比如:
-
`iss`(Issuer):签发人
-
`sub`(Subject):面向的用户
-
`aud`(Audience):接收方
-
`exp`(Expiration time):过期时间
-
`nbf`(Not Before):在此时间之前不可用
-
`iat`(Issued At):签发时间
-
`jti`(JWT ID):唯一标识
-
**公共声明**(Public Claims):可以由双方共同定义的键值,用于传递公开信息。公共声明可以存储非敏感的用户相关信息。
-
**私有声明**(Private Claims):提供者和消费者自定义的信息,不应包含敏感数据。
将 Payload 部分进行 base64 编码得到 JWT 的第二部分。
- Signature(签名)
签名部分用于验证令牌的完整性和防篡改。签名生成过程如下:
-
将 base64 编码的 Header 和 Payload 连接成一个字符串。
-
使用 Header 中指定的加密算法和服务器端的密钥对该字符串进行签名,生成签名部分。
Signature 的格式如下:
```
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
```
需要注意的是,`secret` 是保存在服务器端的密钥,客户端无法获取到该密钥。签名的存在是为了防止 JWT 被篡改。
三、使用 JWT 的注意事项
-
**密钥(secret)要保密**:`secret` 是服务器生成和验证 JWT 的密钥,不应在任何情况下泄露给客户端。
-
**Token 的有效期**:JWT 的过期时间是非常重要的,应设定合理的过期时间以保证安全性,避免 Token 长时间存储。
-
**敏感信息**:Payload 中的数据在客户端是可以解码的,因此不应包含敏感信息,比如密码、银行账号等。
-
**Token 的存储**:在前端,应将 JWT 安全地存储在 `LocalStorage`、`SessionStorage` 或 `HttpOnly Cookie` 中。
四、Spring Boot 集成 JWT 实现认证
在 Java 开发中,Spring Boot 可以方便地集成 JWT 进行认证。以下将展示如何通过 JWT 实现用户登录认证和授权。
- 引入 JWT 依赖
首先,在项目的 `pom.xml` 中引入 JWT 库:
```xml
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
```
- 配置 JWT 属性
在 `application.properties` 文件中定义 JWT 的相关属性,例如:
```properties
Token 的请求头
jwt.header=Authorization
Token 的密钥
jwt.secret=your-secret-key
Token 的过期时间(以毫秒为单位)
jwt.expired=86400000
```
- 创建 JWT 工具类
在 Spring Boot 中创建一个工具类 `JwtTokenUtils` 来生成和解析 JWT 令牌。以下是关键代码示例:
```java
@Component
public class JwtTokenUtils {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expired}")
private Long expired;
// 生成过期时间
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + expired);
}
// 根据用户信息生成 token
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("username", userDetails.getUsername());
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
}
// 从 token 中获取用户名
public String getUsernameFromToken(String token) {
return getClaimsFromToken(token).getSubject();
}
// 从 token 中解析出 Claims
private Claims getClaimsFromToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
// 验证 token 是否过期
public boolean isTokenExpired(String token) {
return getClaimsFromToken(token).getExpiration().before(new Date());
}
// 验证 token 的合法性
public boolean validateToken(String token, UserDetails userDetails) {
return userDetails.getUsername().equals(getUsernameFromToken(token)) && !isTokenExpired(token);
}
}
```
该工具类提供了生成 Token、解析 Token 及验证 Token 合法性的方法。
- 使用 JWT 实现登录逻辑
在用户登录时,校验用户信息,如果验证成功则生成 JWT Token,并将该 Token 返回给客户端。以下是一个简单的示例:
```java
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private JwtTokenUtils jwtTokenUtils;
@Autowired
private AuthenticationManager authenticationManager;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
try {
// 1. 验证用户名和密码
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
// 2. 生成 JWT Token
String token = jwtTokenUtils.generateToken(userDetails);
// 3. 返回 Token
return ResponseEntity.ok(new JwtResponse(token));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid Credentials");
}
}
}
```
客户端发送登录请求后,如果认证通过,服务器会生成 JWT 并返回,客户端可以将该 Token 存储在 `LocalStorage` 或 `HttpOnly Cookie` 中。
5. 使用 JWT 进行请求验证
每次客户端请求 API 时,应在请求头中携带 JWT 进行身份验证。配置过滤器拦截请求并验证 JWT。
首先,创建 `JwtAuthenticationFilter` 来验证每个请求中的 Token:
```java
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenUtils jwtTokenUtils;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String token = request.getHeader("Authorization");
if (token != null && jwtTokenUtils.validateToken(token)) {
String username = jwtTokenUtils.getUsernameFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
}
```
在 Spring Security 配置类中注册过滤器,以便在每次请求时进行 JWT 验证:
```java
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
```
五、总结
通过 JWT 的使用,可以实现安全、高效的用户身份认证。利用 JWT,我们可以在无状态的环境中传递用户身份信息,并将其应用于分布式系统中的单点登录场景。
**安全性建议**:
-
避免在 Payload 中存放敏感信息。
-
设置合理的 Token 过期时间,防止长期有效 Token 被滥用。
-
在生产环境中确保 `secret` 安全性,并对 `secret` 进行定期更新。
六、JWT 的进阶使用
1. Token 刷新机制
在实际应用中,通常需要设置 JWT 的过期时间以确保安全,但频繁地登录会影响用户体验。为此,通常可以设计一个刷新机制来延长 Token 的有效期。
实现 Token 刷新通常有以下几种方式:
-
**定期刷新 Token**:在 Token 快要过期时,前端请求一个特定的刷新接口来获得新的 Token,这样可以延长用户的会话时间。
-
**使用 Refresh Token**:除了常规的 Access Token,还引入一个有效期较长的 Refresh Token。当 Access Token 过期时,可以用 Refresh Token 换取一个新的 Access Token。
Refresh Token 的实现步骤如下:
-
**登录时生成两个 Token**:一个 Access Token(有效期较短,用于认证)、一个 Refresh Token(有效期较长,用于续签)。
-
**创建刷新接口**:当 Access Token 过期后,前端调用刷新接口,将 Refresh Token 传给服务器。
-
**验证 Refresh Token**:服务器验证 Refresh Token 的合法性,如果有效,则重新生成新的 Access Token 并返回给客户端。
-
**定期轮换 Refresh Token**:避免 Refresh Token 被滥用,设置其有效期,超过一定时间需要用户重新登录。
2. 使用多层加密提升安全性
在 JWT 的生成和验证过程中,除了基础的 HMAC SHA256 加密方式,实际上可以利用更加复杂的加密方式,比如:
-
**RSA 或 EC**:RSA(非对称加密)通过私钥生成 Token,用公钥验证 Token,这样即使客户端获取到公钥,也无法伪造 Token。相较之下,RSA 的安全性更高,但签名和验证的速度会稍慢。
-
**HS512**:比 HMAC SHA256 更加安全,计算复杂度更高。
3. 对不同用户角色的 JWT 控制
在实际应用中,我们可能需要对不同用户角色设置不同的 Token 过期时间或权限。例如:
-
**管理员 Token**:有效期短,每次操作要求重新验证。
-
**普通用户 Token**:有效期长,但访问权限受限。
可以在生成 JWT 时为不同角色设置不同的过期时间和权限信息。比如,管理员的 Payload 中可以增加 `role: "admin"` 的声明,从而在验证时附加额外的权限验证逻辑。
七、JWT 的最佳安全实践
虽然 JWT 可以提升性能和用户体验,但由于其在客户端存储的特性,安全性尤为重要。以下是一些安全实践:
1. 确保密钥(Secret Key)安全
JWT 签名使用的密钥应该高度保密,以下是一些具体的措施:
-
**使用环境变量存储密钥**:在不同的环境(如开发、测试、生产)使用不同的密钥,并且将密钥保存在环境变量中,避免硬编码到代码中。
-
**定期轮换密钥**:在 Token 使用量大且时间久的系统中,定期轮换密钥可以减少密钥泄露带来的风险。通常可以通过更换密钥或重新生成 Token 来实现。
2. 禁止在 JWT 中存储敏感数据
由于 JWT Payload 是 Base64 编码的,任何人都可以轻松解码并查看其中的数据。因此,不建议在 Payload 中存储敏感数据,例如密码或信用卡信息。
如果确实需要传递敏感信息,考虑通过对 Token 进行二次加密或者将 Token 直接保存在服务器端。
3. 使用 HTTPS 确保传输安全
确保所有的客户端请求都通过 HTTPS 传输,以防止 Token 在传输过程中被窃听。此外,将 Token 存储在 `HttpOnly` 的 Cookie 中,以防止被 JavaScript 获取到,降低 XSS 攻击的风险。
4. 使用短期有效的 Token
设置较短的 Token 有效期(比如 5 分钟到 15 分钟),即使 Token 被窃取,也可以降低被滥用的风险。通常建议采用 Token 刷新机制,提供短期有效的 Access Token 和较长期有效的 Refresh Token。
5. 防止 Replay 攻击
Replay 攻击是攻击者截获合法的请求并重新发送以获得不正当访问。可以通过以下措施来降低 Replay 攻击的风险:
-
**设置 `jti`(JWT ID)字段**:为每个 Token 设置一个唯一的 `jti`,用于标识 Token,避免 Token 被重放攻击。
-
**基于 IP 地址或设备 ID**:Token 的生成和验证可以基于用户的 IP 地址、设备 ID 等信息,确保 Token 只能在特定设备或网络环境下使用。
八、JWT 中的常见错误和处理方法
1. Token 过期
JWT 过期后,解析时会抛出 `ExpiredJwtException` 异常。可以在解析 Token 时捕获该异常,提示用户重新登录或者调用刷新接口。
2. Token 签名验证失败
当 JWT 签名错误时,系统会抛出 `SignatureException`。这通常是由于密钥不匹配引起的。应确保所有环境的密钥一致,且在解析 Token 时密钥不泄露。
3. Token 格式错误
如果 Token 的格式不符合规范(比如缺少 `.` 分隔符),则可能会抛出 `MalformedJwtException`。在解析前,可以对 Token 进行基本的格式检查。
4. Token 无效或为空
当 Token 为 null 或空字符串时,系统会抛出 `IllegalArgumentException`。确保 Token 在请求中存在并正确传输。可以在 Spring Security 的过滤器中统一处理无效 Token 的异常。
九、示例:JWT 刷新机制的完整实现
为了更好地理解 JWT 刷新机制,以下是一个基于 Spring Boot 的实现示例:
1. 创建 Refresh Token
```java
public String generateRefreshToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("username", userDetails.getUsername());
return Jwts.builder()
.setClaims(claims)
.setExpiration(new Date(System.currentTimeMillis() + refreshTokenExpired))
.signWith(SignatureAlgorithm.HS256, refreshSecret)
.compact();
}
```
##### 2. 实现 Token 刷新接口
```java
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private JwtTokenUtils jwtTokenUtils;
@Autowired
private UserDetailsService userDetailsService;
@PostMapping("/refresh")
public ResponseEntity<?> refreshAccessToken(@RequestBody RefreshRequest refreshRequest) {
String refreshToken = refreshRequest.getRefreshToken();
try {
// 验证 Refresh Token 是否有效
String username = jwtTokenUtils.getUsernameFromToken(refreshToken);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 生成新的 Access Token
String newAccessToken = jwtTokenUtils.generateToken(userDetails);
return ResponseEntity.ok(new JwtResponse(newAccessToken));
} catch (ExpiredJwtException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Refresh Token expired");
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid Refresh Token");
}
}
}
```
3. 前端实现 Token 自动刷新
在前端,可以在 HTTP 拦截器中捕获 401 错误,当 Access Token 过期时,自动调用刷新接口获取新 Token。
例如,在 Axios 中:
```javascript
axios.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
const refreshToken = localStorage.getItem('refreshToken');
// 请求刷新 Token
const response = await axios.post('/auth/refresh', { refreshToken });
if (response.status === 200) {
const newAccessToken = response.data.accessToken;
localStorage.setItem('accessToken', newAccessToken);
originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
return axios(originalRequest);
}
}
return Promise.reject(error);
}
);
这种自动刷新机制可以大大提升用户体验,确保用户在 Token 过期时自动续签而不必频繁登录。
十、总结
通过以上内容,我们详细探讨了 JWT 的基础和进阶应用,包括 Token 刷新机制、多层加密、角色控制等高级用法。在安全性方面,也介绍了密钥管理、传输安全、Replay 防御等实践。希望能帮助您在项目中实现更加安全、稳定的用户认证系统。