什么是JWT?
jwt官网:jwt.io/
JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。此信息可以验证和信任,因为它是数字签名的。JWTs可以使用密钥(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名
jwt常见使用场景
- 授权:这是使用JWT最常见的场景。一旦用户登录,随后的每个请求都将包括JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录(Single Sign-On)是目前广泛使用JWT的一个特性,因为它的开销很小,并且可以方便地跨域使用。
- 信息交换:JSON Web令牌是一种在各方之间安全传输信息的好方法。因为jwt可以被签名,例如,使用公钥/私钥对,可以确保发送者是他们所说的那个人。此外,由于签名是使用头和有效负载计算的,因此还可以验证内容是否未被篡改。
为什么要使用JWT?
传统模式
- 基于session和cookie认证由于http协议是一种无状态的协议,当用户登录(用户名和密码)完成之后,服务端是无法获知具体是哪个用户登录了,只能在服务端存储一份用户登录的信息,然后把这个信息返回给客户端,客户端保存下这个登录信息后续访问的时候只需要携带上这份登录信息,服务端就能获知是哪一个用户。这种模式下,服务端是通过session来保存用户信息,返还到客户端的通常是sessionId,而客户端通常情况是使用cookie来保存这份信息。这就是传统的基于session和cookie的认证。
传统模式下登录流程如下:
- 传统模式下的问题
- 首先每个cookie都只能绑定一个域名,无法在别的域名下获取使用,无法跨域。并且cookie容易受到CSRF攻击。如果再遇到服务端(浏览器禁止or移动端对cookie不支持)不支持cookie,又会使问题复杂化。
- sesion通常都保存在内存当中,当用户量超大的时候,服务端的开销就会线性上升。在分布式集群环境下,又会涉及到session共享的问题,额外降低开发效率。
JWT模式
JSON Web令牌由三个部分组成,由点(.)分隔,这三个部分是:
- Header
- Payload
- Signature
通常情况如下所示:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM
Header
header通常由2部分组成,类型和签名算法(如HMAC SHA256或RSA)。例如:
{
"alg": "HS256",
"typ": "JWT"
}
Payload
Payload包含Registered claims、Public claims和Private claims三部分组成。例如:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
其中有几个重要属性,iss (签发者), exp (过期时间), sub (主体), aud(受众)等。
Signature
要创建签名部分,必须获取编码的header、编码的Payload、sign、header中指定的算法,并对其进行签名。
使用JWT
本文以jwt实现无状态登录认证为例。
引入maven依赖
<!--jjwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
编写工具类
生成token
/**
* 生成JWT TOKEN
*
* @param id 这里加密数据id为用户id
* @return
*/
public String generateToken(Long id) {
/**将token设置为jwt格式*/
String baseToken = UUID.randomUUID().toString();
LocalDateTime localDateTimeNow = LocalDateTime.now();
LocalDateTime localDateTimeExpire = localDateTimeNow.plusSeconds(EXPIRE_SECONDS);
Date from = Date.from(localDateTimeNow.atZone(ZoneId.systemDefault()).toInstant());
//token过期时间
Date expire = Date.from(localDateTimeExpire.atZone(ZoneId.systemDefault()).toInstant());
Claims jwtClaims = Jwts.claims().setSubject(baseToken);
jwtClaims.put(CLAIM_ID_KEY, id);
String compactJws = Jwts.builder()
.setClaims(jwtClaims)
.setNotBefore(from)
.setExpiration(expire)
.signWith(SignatureAlgorithm.HS512, jwtKey)
.compact();
return compactJws;
}
解析校验token
/**
* 根据登陆token获取登陆信息
*
* @param token
* @return
*/
public RequestTokenBO getCustomerTokenInfo(String token) {
Long customerId = -1L;
try {
Claims claims = Jwts.parser().setSigningKey(jwtKey).parseClaimsJws(token).getBody();
String idStr = claims.get(CLAIM_ID_KEY).toString();
customerId = Long.valueOf(idStr);
} catch (Exception e) {
//这里如果token过期会报错
//io.jsonwebtoken.ExpiredJwtException: JWT expired at 2020-06-01T11:00:02Z. Current time: 2020-06-01T11:00:03Z, a
//difference of 1603 milliseconds. Allowed clock skew: 0 milliseconds.
//出现如上报错说明woken已经过期,本文统一做登录失效处理,也可以根据具体的异常返回具体的信息
log.error("getCustomerTokenInfo error:{}", e);
return null;
}
CustomerEntity customer = customerService.getById(customerId);
if (customer == null) {
return null;
}
if (CustomerStatusEnum.DISABLED.getValue().equals(customer.getIsDisabled())) {
return null;
}
if (JudgeEnum.YES.equals(customer.getIsDelete())) {
return null;
}
return new RequestTokenBO(customer);
}
登录成功之后返回登录token信息。后续访问只需要携带上token信息即可。后端在拦截器作登录校验。通常情况下,是放在请求的header中,使用bearer形式,如下:
Authorization: Bearer token
也可以将token放到请求query参数中。
登录认证流程
- 用户登录,输入账号密码
- 服务端接受登录请求,常规校验(验证码),根据用户信息生成token,返还客户端
- 客户端收到登录返还信息,将得到的token保存下来,通常保存在localStorge里面
- 再次发起请求授权资源,请求头中携带token信息
- 服务端接受请求,拦截器中校验token信息,校验通过放行,校验不通过作相应异常提示