概念
JWT(JSON Web Tokens):JSON Web令牌
是一种紧凑的,UL安全的方法,用于表示要在两方之间转移的声明(凭证)。
JWT定义了一种紧凑而独立的方法,用于在各方之间安全地将信息作为JSON对象传输。
- 使用json对象在系统之间传递信任的数据,例如{“name”:“lisi”,“id”:1001,“role”,“admin”}
- 是URL安全的,浏览器中作为参数传递JWT
- JWT数据可以使用秘钥(使用HMAC算法)或使用RSA或ECDSA的公用/专用密钥对对JWT进行签名。JWT信息可以使用数字签名,因此可以被验证和信任。
JWT规范,允许我们使用JWT在两个组织之间传递安全可靠的信息。
JWT是一个凭证
用户访问系统:先获取凭据(token),使用凭据(token)访问其他资源
JWT工作原理
用户先到服务器认证身份,认证后服务器返回一个json,就像这个样子
"id":1001,
"创建时间":"2021年5月15日10点21分10秒"
“角色":"经理”,
"过期时间":"2021年5月16日0点0分0秒"
以后用户再发起请求,就是带着这个json数据,服务器拿这个json对象确定用户身份,判断用户能执行操作,获取数据。为了防止用户篡改数据,服务器在生成这个json对象的时候,会加上签名。
服务器就不保存任何session数据了,也无需使用redis存储,服务器变成无状态了,从而比较容易实现扩展。
什么时候使用JWT
- 授权Authorization:这是使用JWT的最常见方案。用户登录后,每个后续请求都将包含JWT,从而允许用户访问该令牌允许的路由,服务和资源。单点登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。
- 信息交换Information Exchange:JSON Web令牌是在各方之间安全地传输信息的一种好方法。因为可以对JWT进行签名(例如,使用公钥/私钥对),所以您可以确保发件人是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否未被篡改。
- 客户端会话(无Session)
认证流程
- 前端通过web表单将自己的用户和密码发送到后端接口(一般是http-post请求,建议使用SSL加密传输(https协议),以免敏感信息被嗅探)
- 后端核对用户名和密码成功后,将用户的Id等其他信息作为JWT Payload(负载),将其与头部分别进行BASE64编码拼接后签名,形成一个JWT(Token),形成的JWT就是一个字符串(head.payload.singueater)
- 后端将JWT字符串作为登录成功的结果返回给前端。前端结果保存在localStorage或sessionStorage上或者是redis中,退出登录时前端删除保存的JWT即可。
- 前端在每次请求时将JWT放入http header中的Authorization位(解决XSS和XSRF问题)
- 后端检查是否存在,如存在验证JWT的有效性,例如:检查签名是否正确,检查token是否过期,检查token的接收方是否是自己(可选)。
- 验证通过后后端使用jwt中包含的用户信息进行其他逻辑操作,返回相应结果。
为什么需要JWT
cookie和session的缺点:
- session信息都存储在服务端内存
- 集群环境中需要额外处理
- csrf:Cross-site request forgery,cookie被截获后可能发生跨站点请求伪造
- cookie的跨域(前后端分离)读写不方便
jwt优势:
- 输出是三个由.分割的Base64-URL字符串,可以在HTML和HTTP环境中轻松传递这些字符串,与基于XML的标准(如SAML)相比,更紧凑。
- 简洁:可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快。
自包含:负载中包含了所有用户所需的信息,避免了多次查询数据库。 - 因为token是以JSON加密的新三国hi保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。
- 不需要再服务端保存会话信息,特别适用于分布式微服务。
JWT组成
JWT规范中,定义组成结构,长这个样子
eyJhbGci0iJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY30DkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWFOIjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
这是一个很长的字符串,中间用点(.)分隔成三个部分。JWT内部是没有换行的,这里只是为了便于展示,将它写成了几行。
JWT的三个部分依次如下:
-
Header(头部)
-
Payload(负载)
-
Signature(签名)
以上三个部分是"."作为分隔,是一行显示:xxxx.yyyy.zzz
Header:是一个json对象,存储元数据
{
"a1g":"Hs256",
"typ":"JWT"
}
a1g:是签名的算法名称(algorithm),默认是HMAC SHA256(写成HS256)
typ属性表示这个令牌(token)的类型(type),JWT令牌统一写为JWT
元数据的json对象使用base64URL编码,翻译为字符串
Payload:负载,是一个json对象。是存放传递的数据,数据分为Public的和Private的。
Public是JWT中规定的一些字段,可以自己选择使用
- iss(issuer):签发人
- exp(expiration time):过期时间
- sub(subject):该JWT所面向的用户
- aud(audience):受众,接收该JwT的一方
- nbf(Not Before):生效时间
- iat(Issued At):签发时间
- jti(JWT ID):编号
Private是自己定义的字段
{
"role":"经理",
"name":"张凡",
"id":2345
}
Playload默认是没有加密的,以上数据都明文传输的,所以不要放敏感数据。此部分数据也是json对象使用base64URL编码,翻译为字符串。
Signature:签名。签名是对Header和Payload两部分的签名,目的是防止数据被篡改
HMACSHA256(
base64UrlEncode (header)+"."+
base64Ur1Encode (payload),
secret)
# 签名算法:先指定一个secret秘钥,把base64URL的header,base64URL的payload和secret秘钥使用
# HMAC SHA256生成签名字符串
最后把base64URL的Header、base64URL的payload、Signature签名的值三个部分拼成一字符串,每个部分之间用”点”(.)分隔,就可以返回给用户。
例如:
eyJhbGciOiJICJ9.eyJzdmFtZSOIjoxNTE2Ij15DIyfQ.Sf1KxwRJ6POk6yJV.adQssw5c
jwt的实现形式
方式一 :jwt(java-jwt)
- 导入依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.11.0</version>
</dependency>
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Calendar;
import java.util.Map;
public class JavaJWTUtil {
//这个secret我们一般写在配置文件或者存放常量的类中,这里为了方便直接写在这儿
private static String secret="WHY2023.11.24";
/**
* 生成token header.payload.signature
* @param map :payload中需要存放的信息,以map方式传入
* @param day :过期时间,以秒为单位
* @return
*/
public static String getToken(Map<String,String> map,Integer day){
Calendar instance=Calendar.getInstance();
instance.add(Calendar.SECOND,day);
//创建jwt builder
JWTCreator.Builder builder=JWT.create();
//payload,这里采用lambda表达式设置
map.forEach((k,v)->{
builder.withClaim(k,v);
});
String token=builder.withExpiresAt(instance.getTime())//指定令牌过期时间
.sign(Algorithm.HMAC256(secret));
return token;
}
/**
* 验证token合法性,不合法会抛出异常信息
* @param token : 前端传来的token
* @return
*/
public static DecodedJWT verify(String token){
//如果有任何验证异常,此处都会抛出异常,因此我们可以捕获这些异常来反馈信息回前端
DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC256(secret)).build().verify(token);
return decodedJWT;
}
}
常见异常信息
- SignatureVerificationException 签名不一致
- TokenExpiredException 令牌过期
- AlgorithmMismatchException 签名算法不匹配
- InvalidClaimException payload不可用
方式二 : jjwt(推荐)
github地址:https:/github.com/jwtk/jjwt
- 导入坐标
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.6.0</version>
</dependency>
- 编写工具类
package com.why.utils;
import cn.hutool.core.lang.UUID;
import io.jsonwebtoken.*;
import java.util.*;
public class JJwtUtils {
/**
* 秘钥,不能公开
*/
private static final String SECRET = "why2023.11.24";
/**
* 第一个版本
*
* @param map map里面是用户信息
* @param expire expire是以毫秒为单位的过期时间
* @return 返回token字符串
*/
public static String CreateToken(Map map, long expire) {
//指定签名的时候使用的算法,也就是header那部分,jjwt已经将这部分内容封装好了。
SignatureAlgorithm hs256 = SignatureAlgorithm.HS256;
//生成jwt构造器
JwtBuilder jwtBuilder = Jwts.builder();
//生成签名的时候使用的秘钥secret,一般可以从本地配置文件中读取,切记这个秘钥不能外露哦。
//它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
//一定要放在前面,否则会使后面的设置无效
jwtBuilder.setClaims(map)
//jwt的唯一标识
.setId(UUID.randomUUID().toString())
//jwt的签发人
.setIssuer("why")
//iat: jwt的签发时间
.setIssuedAt(new Date())
//sub(Subject):代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,
// 可以存放什么userid,username之类的,作为用户的唯一标志。
.setSubject("LoginCheck")
//设置有效期,当前时间+有效时间
.setExpiration(new Date(System.currentTimeMillis() + expire))
//设置签名使用的签名算法和签名使用的秘钥
.signWith(hs256, SECRET);
return jwtBuilder.compact();
}
/**
* 第二个版本
*
* @param expire
* @return
*/
public static String CreateToken(long expire) {
//指定签名的时候使用的算法,也就是header那部分,jjwt已经将这部分内容封装好了。
SignatureAlgorithm hs256 = SignatureAlgorithm.HS256;
//生成jwt构造器
JwtBuilder jwtBuilder = Jwts.builder();
//生成签名的时候使用的秘钥secret,一般可以从本地配置文件中读取,切记这个秘钥不能外露哦。
//它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
//一定要放在前面,否则会使后面的设置无效
jwtBuilder.setId(UUID.randomUUID().toString())//jwt的唯一标识
//jwt的签发人
.setIssuer("why")
//iat: jwt的签发时间
.setIssuedAt(new Date())
//sub(Subject):代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,
// 可以存放什么userid,username之类的,作为用户的唯一标志。
.setSubject("LoginCheck")
//设置有效期,当前时间+有效时间
.setExpiration(new Date(System.currentTimeMillis() + expire))
//设置签名使用的签名算法和签名使用的秘钥
.signWith(hs256, SECRET);
return jwtBuilder.compact();
}
/**
* 解析token获取用户信息
*
* @param token
* @return
*/
public static Claims parseJWT(String token) {
//通过秘钥去解析token
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
//返回解析后的内容
return claimsJws.getBody();
}
}