JSON Web Token
JWT
JWT是
JSON Web Token
的缩写,它是基于开源标准(RFC 7519)定义的一种可以安全传输的 JSON对象。
- JWT之所以叫JSON Web Token,因为其 header 和 payload 在编码之前都是JSON格式
- JWT规定以JSON格式传递信息,header 和 payload 通常使用
base64
编码成字符串 - JWT是自包含的,Token本身携带了验证信息,不需要借助其他工具就能知道一个Token是否有效。但当需要高级功能如token刷新、黑名单等,还是需要借助缓存和数据库等
为什么要使用JWT?
跨域认证问题
互联网服务用户认证的一般流程如下所示:
- 用户向服务器发送用户名和密码
- 服务器验证通过后,在当前session保存相关数据,如用户角色,登录时间等
- 服务器向用户返回一个 session_id,写入用户的 Cookie
- 用户随后的每一个请求都会通过 Cookie 将 session_id 传回服务器
- 服务器收到 session_id 后,找到前期保存的数据,以此验证用户的身份
这种方式的问题在于,扩展性不好。如果服务器是一个集群或者是跨域的服务导向架构,就要求 session 数据共享,使每台服务器都能够读取 session 。
示例:A网站和B网站都是某公司下的服务,要求用户在登录其中一个网站后,再访问另外一个网站实现自动登录。
- session数据持久化:将session数据写入持久层,各服务器收到请求后都向持久层拿数据。
- JWT:服务器不保存数据,所有数据都保存在客户端,每次请求都发回服务器。
使用session数据持久化会造成工程量增大,且会因为持久层失效导致单点登录失败。
JWT的结构
Header(头部)
存放签名的生成算法和Token类型。
{
"alg": "HS256",
"typ": "JWT"
}
Payload(载荷)
存放携带的用户数据。
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
Payload中的字段:
字段 | 全称 | 作用 |
---|---|---|
iss | Issuer | 代表token的颁发者 |
sub | Subject | 代表token的主题 |
aud | Audience | 代表token的接收目标 |
exp | Expiration Time | 代表token的过期时间,时间戳格式 |
nbf | Not Before | 代表token在这个时间之前不能被处理,纠正服务器的时间偏差 |
iat | Issued At | 代表token的颁发时间 |
jti | JWT ID | 代表token的id |
除了以上标准定义的字段外,用户可以自由添加需要的信息,如用户ID,用户名等。通常添加的是经常使用但安全性要求不高的信息。
Signature(签名)
以 header 和 payload 生成的签名,一旦 header 或 payload 被篡改,验证将失败。
// secret: 密钥, 只有服务器知道
String signature = HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
使用Header中指定的签名算法按上述代码产生签名,然后把Header、Payload、Signature三部分拼车一个字符串,各部分之间使用 .
分隔,最后返回给用户。
下图为JSON Web Token官网首页的一个示例:
JWT认证流程
- 用户使用账号密码登录,调用服务器登录接口
- 服务器登录程序生成token,并返回给用户
- 用户后续请求携带token
- 服务器收到用户请求后,验证token的合法性、有效性,验证通过后处理请求
- 返回请求结果给用户
JWT使用方式
客户端收到服务器返回的JWT,可以存储在Cookie中,也可以存储在localStorage。此后客户端每次与服务器通信都要携带这个JWT,可以放在Cookie中自动发送,但不能跨域,所以更好的做法是放在HTTP请求头的 Authorization
字段中。
JWT特点
- JWT最大的缺点是由于服务器不保存session状态,因此无法在使用过程中废除某个token或更改token权限,即token一旦签发,在到期之间会一直有效,除非服务器部署额外的逻辑
- JWT本身包含认证信息,一旦泄漏,任何人都可以获得该令牌的所有权限。所以JWT的有效期应该设置的比较短,以防盗用
- JWT应该使用HTTPS协议传输,为了减少盗用
Springboot集成JWT
1 添加pom依赖
<!-- SpringSecurity 依赖配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT(Json Web Token)登录支持 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
2 配置application.yml
# 自定义JWT
jwt:
field: Authorization # 请求头字段
secret: 123456 # 密钥
expiration: 604800 # 过期时间(60*60*24s)
tokenHead: Bearer # token开头
3 添加JWT工具类
package com.nudt.demo_02.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @Author: Lzy
* @Time: 2022/4/18
* @Description: JwtToken生成的工具类
*/
@Component
public class JwtTokenUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class);
private static final String CLAIM_KEY_USERNAME = "sub";
private static final String CLAIM_KEY_CREATED = "created";
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
/**
* 生成 JWT Token
* 构成: Header.Payload.Signature
*/
private String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims) //设置payLoad
.setExpiration(generateExpirationDate()) //设置过期时间
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 生成 token 过期时间
*/
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + expiration * 1000); // 一天后过期
}
/**
* PayLoad
* 从 token 中获取JWT中的payload
*/
private Claims getClaimsFromToken(String token) {
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
catch (Exception e) {
LOGGER.info("JWT 格式验证失败 : {}", token);
}
return claims;
}
private Date getExpireDateFromToken(String token) {
Claims claims = getClaimsFromToken(token);
return claims.getExpiration(); //map.get("exp", Date.Class)
}
/**
*
* @param token
* @return True —— 已过期; False —— 未过期
*/
private boolean isTokenExpired(String token) {
Date expireDate = getExpireDateFromToken(token);
return expireDate.before(new Date());
}
public String getUserNameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject(); //从JwtMap中获得主题
}
catch (Exception e) {
username = null;
}
return username;
}
/**
* 验证token是否有效
* @param token 客户端传入的token
* @param userDetails 数据库查询的用户信息
* @return
*/
public boolean isTokenValidate(String token, UserDetails userDetails) {
String username = getUserNameFromToken(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
/**
* 根据用户信息生成 token
* @param userDetails
* @return
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
public boolean canRefresh(String token) {
return !isTokenExpired(token);
}
public String refresh(String token) {
Claims claims = getClaimsFromToken(token);
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
}
4 JWT登录授权
package com.nudt.demo_02.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
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.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Author: Lzy
* @Time: 2022/4/21
* @Description: JWT登录授权过滤器
*/
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Value("${jwt.field}")
private String field;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader(this.field);
if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
String authToken = authHeader.substring(this.tokenHead.length());
String username = jwtTokenUtil.getUserNameFromToken(authToken);
LOGGER.info("Checking username: {}", username);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.isTokenValidate(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
LOGGER.info("Authenticated user: {}", username);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
filterChain.doFilter(request, response);
}
}
参考文章
[2] mall学习教程
[3] 深入浅出之JWT