JWT官网: https://jwt.io/
JWT(Java版)的github地址:https://github.com/jwtk/jjwt
JWT请求流程
- 用户使用账号和面发出post请求;
- 服务器使用私钥创建一个jwt;
- 服务器返回这个jwt给浏览器;
- 浏览器将该jwt串在请求头中像服务器发送请求;
- 服务器验证该jwt;
- 返回响应的资源给浏览器。
JWT的主要应用场景
- 前后端分离
- 单点登录(sso)
- 分布式微服务
一旦用户完成了登陆,在接下来的每个请求中包含JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证。由于它的开销非常小,可以轻松的在不同域名的系统中传递,所有目前在单点登录(SSO)中比较广泛的使用了该技术。 信息交换在通信的双方之间使用JWT对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。
优点
- 简洁(Compact): 可以通过
URL
,POST
参数或者在HTTP header
发送,因为数据量小,传输速度也很快 - 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库
- 因为
Token
是以JSON
加密的形式保存在客户端的,所以JWT
是跨语言的,原则上任何web形式都支持。 - 不需要在服务端保存会话信息,特别适用于分布式微服务。
- JWT只通过算法实现对Token合法性的验证,不依赖数据库,Memcached的等存储系统,因此可以做到跨服务器验证,只要密钥和算法相同,不同服务器程序生成的Token可以互相验证。
JWT的token机制
JWT标准的token包含三部分:
- header(头部),头部信息主要包括(参数的类型--JWT,签名的算法--HS256)
- poyload(负荷),负荷基本就是自己想要存放的信息(因为信息会暴露,不应该在载荷里面加入任何敏感的数据),有两个形式,下边会讲到
- sign(签名),签名的作用就是为了防止恶意篡改数据
为什么要使用签名
签名解决了数据传输过程中参数被篡改的风险
一般而言,加密算法对于不同的输入产生的输出总是不一样的,如果有人对Header以及Payload的内容解码之后进行修改,再进行编码的话,那么新的头部和载荷的签名和之前的签名就将是不一样的。而且,如果不知道服务器加密的时候用的密钥的话,得出来的签名也一定会是不一样的。
服务器应用在接受到JWT后,会首先对头部和载荷的内容用同一算法再次签名。如果服务器应用对头部和载荷再次以同样方法签名之后发现,自己计算出来的签名和接受到的签名不一样,那么就说明这个Token的内容被别人动过的,我们应该拒绝这个Token
<!-- json web token -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
package com.application.utils;
import java.util.Calendar;
import java.util.Date;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
public class TokenUtil {
/**
* 加密
* @author Jason
* @date 2019-03-29
* @param id 用户id
* @param username 用户名称
* @param password 用户密码
* @param deviceId 设备号
* @param formatDate 过期日期
* @return 加密的token
*/
public static String createToken(String id, String username, String password, String deviceId, String formatDate) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
long nowMillis = Calendar.getInstance().getTimeInMillis();
Date now = new Date(nowMillis);
// 加密秘钥
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary("123456789");
SecretKeySpec signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
JwtBuilder builder = Jwts.builder()
.setIssuedAt(now)
.setId(id)
.setIssuer(username)
.setSubject(password)
.setAudience(deviceId)
.signWith(signatureAlgorithm,signingKey);
// 设置过期日期
Date expirationDate = null;
if (null != formatDate) {
try {
expirationDate = DateUtil.stringToDate(formatDate);
} catch (Exception e) {
expirationDate = getDefultExpirationDate();
}
if(null == expirationDate || expirationDate.before(new Date())){
expirationDate = getDefultExpirationDate();
}
} else {
expirationDate = getDefultExpirationDate();
}
builder.setExpiration(expirationDate);
return builder.compact();
}
/**
* 获取默认的过期时间,默认是七天以后
* @author Arwen Liu
* @date 2018-10-29
* @return Date
*/
private static Date getDefultExpirationDate(){
long timeInMillis = Calendar.getInstance().getTimeInMillis();
Long time = 1 * 1000 * 60 * 60 * 24 * 7l;
time += timeInMillis;
return new Date(time);
}
/**
* 解密
* @author Arwen Liu
* @date 2018-10-29
* @param token
* @return Claims
*/
public static Claims parseToken(String token) {
Claims claims = Jwts.parser()
// s解密密钥,需要和 加密秘钥一致
.setSigningKey(DatatypeConverter.parseBase64Binary("123456789"))
.parseClaimsJws(token).getBody();
return claims;
}
public static void main(String[] args) {
String secretToken = createToken("1001", "staff", "123456","deviceId", "2018-12-28");
System.out.println("加密后---->" + secretToken);
// ss解密
Claims claims = parseToken(secretToken);
System.out.println("解密后---->");
System.out.println("id: " + claims.getId());
System.out.println("username: " + claims.getIssuer());
System.out.println("password: " + claims.getSubject());
System.out.println("deviceId: "+ claims.getAudience());
System.out.println("expiration: " + DateUtil.dateToString(claims.getExpiration(), "yyyy-MM-dd"));
}
}
验证token的策略
- 让客户端保存到 localStorage 每次请求带着token
- 放到cookie里面 然后定义一个拦截器 每次都从cookie里面查询并验证token
- 放到header里面 和 cookie类似
在退出登录时怎样实现JWT Token失效呢?
退出登录, 只要客户端端把Token丢弃就可以了,服务器端不需要废弃Token。
怎样保持客户端长时间保持登录状态?
服务器端提供刷新Token的接口, 客户端负责按一定的逻辑刷新服务器Token。
服务器端是否应该从JWT中取出userid用于业务查询?
REST API是无状态的,意味着服务器端每次请求都是独立的,即不依赖以前请求的结果,因此也不应该依赖JWT token做业务查询, 应该在请求报文中单独加个userid 字段。
为了做用户水平越权的检查,可以在业务层判断传入的userid和从JWT token中解析出的userid是否一致, 有些业务可能会允许查不同用户的数据。
如何防范Replay Attacks
重复攻击
所谓重复攻击就是攻击者发送一个后端服务器已接收过的包,来达到攻击系统的目的。
比如在浏览器端通过用户名/密码验证获得签名的Token被木马窃取。即使用户登出了系统,黑客还是可以利用窃取的Token模拟正常请求,而服务器端对此完全不知道,因为JWT机制是无状态的。
可以在Payload里增加时间戳并且前后端都参与来解决:
1.前端生成token时,在payload里增加当前时间戳
2.后端接收后,对解析出来的时间戳和当前时间进行判断,
3.如果相差特定时间内(比如2秒),允许请求否则判定为重复攻击
参考 https://www.jianshu.com/p/e88d3f8151db
https://www.jianshu.com/p/836df92c06eb
https://blog.csdn.net/why15732625998/article/details/78534711
https://www.jianshu.com/p/e88d3f8151db
http://www.cnblogs.com/xiekeli/p/5607107.html