一、什么是JWT?Json web token (JWT)
是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准
该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。**JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,**也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
jwt的出现,校验方式更加简单便捷化,无需通过redis缓存,而是直接根据token取出保存的用户信息,以及对token可用性校验,单点登录更为简单。
基于session和基于jwt的方式的主要区别就是用户的状态保存的位置,session是保存在服务端的,而jwt是保存在客户端的。
二、 JWT的构成
JWT是由三部分构成的 将这三段信息文本用 . 链接一起就构成了JWT字符串。
第一部分:头部(header) 承载两部分信息
- 声明类型,这里是jwt
- 声明加密的算法 通常直接使用 HMAC SHA256
第二部分:载荷(payload) 存放有效信息的地方
这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
- 标准中注册的声明
- 公共的声明
- 私有的声明
第三部分是签证(signature) 由三部分组成
- header (base64后的)
- payload (base64后的)
- secret
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。 一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
三、JWT验证流程和特点
验证流程:
① 在头部信息中声明加密算法和常量, 然后把header使用json转化为字符串
② 在载荷中声明用户信息,同时还有一些其他的内容;再次使用json 把载荷部分进行转化,转化为字符串
③ 使用在header中声明的加密算法和每个项目随机生成的secret来进行加密, 把第一步分字符串和第二部分的字符串进行加密, 生成新的字符串。词字符串是独一无二的。
④ 解密的时候,只要客户端带着JWT来发起请求,服务端就直接使用secret进行解密。
特点:
① 三部分组成,每一部分都进行字符串的转化
② 解密的时候没有使用数据库,仅仅使用的是secret进行解密
③ JWT的secret千万不能泄密!
④因为json的通用性,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,NodeJS,PHP等很多语言都可以使用。
⑤有了payload部分,所以JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息。
⑥便于传输,jwt的构成非常简单,字节占用很小
⑦不需要在服务端保存会话信息, 易于应用的扩展
安全相关:
- 不应该在jwt的payload部分存放敏感信息,因为该部分是客户端可解密的部分。
- 保护好secret私钥,该私钥非常重要。
- 如果可以,请使用https协议
四、传统Cookie+Session与JWT对比
① 在传统的用户登录认证中,因为http是无状态的,所以都是采用session方式。用户登录成功,服务端会保证一个session,当然会给客户端一个sessionId,客户端会把sessionId保存在cookie中,每次请求都会携带这个sessionId。
cookie+session这种模式通常是保存在内存中,而且服务从单服务到多服务会面临的session共享问题,随着用户量的增多,开销就会越大。而JWT只需要服务端生成token,客户端保存这个token,每次请求携带这个token,服务端认证解析就可。
② JWT方式校验方式更加简单便捷化,无需通过redis缓存,而是直接根据token取出保存的用户信息,以及对token可用性校验,单点登录,验证token更为简单。
适合使用JWT的场景:
有效期短
只希望被使用一次
五、JWT的优点:
1、可扩展性好
应用程序分布式部署的情况下,session需要做多机数据共享,通常可以存在数据库或者redis里面。而jwt不需要。
2、无状态
jwt不在服务端存储任何状态。RESTFUL API的原则之一是无状态,发出请求时,总会返回带有参数的响应,不会产生附加影响。用户的认证状态引入这种附加影响,这破坏了这一原则。另外jwt的载荷中可以存储一些常用信息,用于交换信息,有效地使用 JWT,可以降低服务器查询数据库的次数。
- 简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快
- 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库
- 因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。
- 不需要在服务端保存会话信息,特别适用于分布式微服务。
六、JWT的缺点:
1、安全性
由于jwt的payload是使用base64编码的,并没有加密,因此jwt中不能存储敏感数据。而session的信息是存在服务端的,相对来说更安全。
2、性能
jwt太长。由于是无状态使用JWT,所有的数据都被放到JWT里,如果还要进行一些数据交换,那载荷会更大,经过编码之后导致jwt非常长,cookie的限制大小一般是4k,cookie很可能放不下,所以jwt一般放在local storage里面。并且用户在系统中的每一次http请求都会把jwt携带在Header里面,http请求的Header可能比Body还要大。而sessionId只是很短的一个字符串,因此使用jwt的http请求比使用session的开销大得多。
3、一次性
无状态是jwt的特点,但也导致了这个问题,jwt是一次性的。想修改里面的内容,就必须签发一个新的jwt。
(1)无法废弃
通过上面jwt的验证机制可以看出来,**一旦签发一个jwt,在到期之前就会始终有效,无法中途废弃。**例如你在payload中存储了一些信息,当信息需要更新时,则重新签发一个JWT,但是由于旧的JWT还没过期,拿着这个旧的JWT依旧可以登录,那登录后服务端从JWT中拿到的信息就是过时的。为了解决这个问题,我们就需要在服务端部署额外的逻辑,例如设置一个黑名单,一旦签发了新的jwt,那么旧的就加入黑名单(比如存到redis里面),避免被再次使用。
(2)续签
如果你使用jwt做会话管理,传统的cookie续签方案一般都是框架自带的,session有效期30分钟,30分钟内如果有访问,有效期被刷新至30分钟。一样的道理,**要改变jwt的有效时间,就要签发新的jwt。最简单的一种方式是每次请求刷新jwt,即每个http请求都返回一个新的jwt。**这个方法不仅暴力不优雅,而且每次请求都要做jwt的加密解密,会带来性能问题。另一种方法是在redis中单独为每个jwt设置过期时间,每次访问时刷新jwt的过期时间
可以看出想要破解jwt一次性的特性,就需要在服务端存储jwt的状态。但是引入 redis 之后,就把无状态的jwt硬生生变成了有状态了,违背了jwt的初衷。而且这个方案和session都差不多了。
七、认证流程
- 首先,前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一 个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
- 后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload (负载),将其与头部分别进行Base64编码拼接后签名, 形成一个JWT(Token)。形成的JWT就是一个形同1l1. zzz . xxx的字符串。token head. payload. singurater。后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStor age或sessionStorage上, 退出登录时前端删除保存的JWT即可。
- 前端在每次请求时将JWT放入HTTP Header中的Authorization位。 (解决XSS和XSRF问题) HEADER
- 后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确:检查Token是否过期:检查Token的接收方是否是自己(可选)
- 验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。
八、Java实现JWT
1、jdk1.8以上的 导入依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2、jwt生成一个token以及解密
import java.util.Date;
import java.util.UUID;
/**
* @author 江江江
* @create 2021/9/10 16:08
*/
public class Test {
private long time=1000*60*60*24;
private String signature="admin";
@org.junit.Test
public void jwt(){
JwtBuilder jwtBuilder= Jwts.builder();
//创建一个字符串
String jwToken=jwtBuilder
//header
//类型
.setHeaderParam("type","JWT")
//算法
.setHeaderParam("alg","HS256")
//payload 信息
.claim("username","tom")
.claim("role","admin")
//主题
.setSubject("admin-test")
//时间 系统时间 + 一天14小时
.setExpiration(new Date(System.currentTimeMillis()+time))
//设置id
.setId(UUID.randomUUID().toString())
//signature
.signWith(SignatureAlgorithm.HS256,signature)
//拼接起来 得到一个字符串
.compact();
System.out.println(jwToken);
}
/**
* 解密
*/
@org.junit.Test
public void parse(){
//将需要解密的token赋值给字符串
String token="eyJ0eXBlIjoiSldUIiwiYWxnIjoiSFMyNTYifQ.eyJ1c2VybmFtZSI6InRvbSIsInJvbGUiOiJhZG1pbiIsInN1YiI6ImFkbWluLXRlc3QiLCJleHAiOjE2MzEzNDgyOTgsImp0aSI6IjcwM2Y4ZGEyLWNkMzAtNDI5YS04NjA2LWY0MjNlZDcwYWEyYSJ9.zI2CLWQhnsJpw3v4wJaesaiS8379OcacstybKtdZa_k";
JwtParser jwtParser=Jwts.parser();
//setSigningKey:加密的时候通过key 解密也需要
//parseClaimsJws:解析成数据
Jws<Claims> claimsJws = jwtParser.setSigningKey(signature).parseClaimsJws(token);
//将集合中的数据 存入对象
Claims claims = claimsJws.getBody();
System.out.println(claims.get("username"));
System.out.println(claims.get("role"));
System.out.println(claims.getId());
System.out.println(claims.getSubject());
System.out.println(claims.getExpiration());
}
}
生成的一个完整的token:
eyJ0eXBlIjoiSldUIiwiYWxnIjoiSFMyNTYifQ
.eyJ1c2VybmFtZSI6InRvbSIsInJvbGUiOiJhZG1pbiIsInN1YiI6ImFkbWluLXRlc3QiLCJleHAiOjE2MzYxODEwNzYsImp0aSI6ImNiYzA5NjZmLTgyZjYtNDg2Zi05MmRlLTZhZmI2NTI1ZDNmNCJ9
.OZYwHcOE5btp5CuZ-oCgiI1-GdZwYRdZnUl7G8-LD_g
九、SpringBoot整合JWT
1、创建一个springBoot项目并导入依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.18.1</version>
</dependency>
2、测试类中
@SpringBootTest
class SpringbootJwtApplicationTests {
@Test
void contextLoads() {
HashMap<String, Object> map=new HashMap<>();
Calendar instance=Calendar.getInstance();
instance.add(Calendar.SECOND,100);
String token=JWT.create()
.withHeader(map)
.withClaim("userId",12)
.withClaim("username","zhangsan") //payload
.withExpiresAt(instance.getTime()) //令牌的过期时间
.sign(Algorithm.HMAC256("@#*9nxix%)")); //签名
System.out.println(token);
}
@Test
public void test(){
//创建验证对象
JWTVerifier build = JWT.require(Algorithm.HMAC256("@#*9nxix%)")).build();
DecodedJWT verify = build.verify("");
System.out.println(verify.getClaim("username"));
System.out.println(verify.getClaim("userId"));
System.out.println(verify.getClaims().get("userId").asInt());
System.out.println(verify.getClaims().get("username").asString());
}
}
3、controller层
@GetMapping("/test/test")
public String test(String usernaem, HttpServletRequest request){
//认证成功 放过session
request.getSession().setAttribute("username",usernaem);
return "login ok";
}
4、application.yml配置文件
jwt:
#JWT存储的请求头
tokenHeader: Authorization
#JWT 加解密使用的密钥
secret: yeb-secret
#JWT 的超期限时间
expiration: 604800
#JWT 负载中拿到开头
tokenHead: Bearer
5、JWT token工具类
/**
*
* JwtToken 工具类
* @author 江江江
* @create 2021/9/12 16:49
*/
@Component
public class JwtTokenUtil {
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;
/**
* 根据用户信息生成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);
}
/**
* 从token中获取登录用户名
* @param token
* @return
*/
public String getUserNameFormToken(String token) {
String username;
try {
Claims claims = getClaimsFormToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 从token中获取荷载
* @param token
* @return
*/
private Claims getClaimsFormToken(String token) {
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
e.printStackTrace();
}
return claims;
}
/**
* 验证token是否有效
* @param token
* @param userDetails
* @return
*/
public boolean validateToken(String token, UserDetails userDetails) {
String username = getUserNameFormToken(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
/**
* 判断token是否可以被刷新
* @param token
* @return
*/
public boolean canRefresh(String token) {
return !isTokenExpired(token);
}
/**
* 刷新token
* @param token
* @return
*/
public String refreshToken(String token) {
Claims claims = getClaimsFormToken(token);
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
/**
* 判断token是否失效
* @param token
* @return
*/
private boolean isTokenExpired(String token) {
Date expireDate = getExpiredDateFromToken(token);
return expireDate.before(new Date());
}
/**
* 从token中获取过期时间
* @param token
* @return
*/
private Date getExpiredDateFromToken(String token) {
Claims claims = getClaimsFormToken(token);
return claims.getExpiration();
}
/**
* 根据荷载生成JWT TOKEN
* @param claims
* @return
*/
private String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 生成token失效时间
* @return
*/
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + expiration * 1000);
}
}
6、JWT 登录授权过滤器
/**
* JWT 登录授权过滤器
* @author 江江江
* @create 2021/9/12 17:57
*/
public class JwtAuthencationTokenFilter extends OncePerRequestFilter {
@Value("${jwt.tokenHeader}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader(tokenHeader);
// 存在token
if (null != authHeader && authHeader.startsWith(tokenHead)) {
String authToken = authHeader.substring(tokenHead.length());
String username = jwtTokenUtil.getUserNameFormToken(authToken);
//token存在用户名 但是未登录
if (null != username && null == SecurityContextHolder.getContext().getAuthentication()) {
//登录
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
//验证token是否有效 重新设置用户对象
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authenticationToken=new
UsernamePasswordAuthenticationToken(userDetails, null,
userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
}
filterChain.doFilter(request,response);
}
}