JWT,全称是Json Web Token, 是JSON风格轻量级的授权和身份认证规范,可实现无状态、分布式的Web应用授权;
官网:https://jwt.io
GitHub上jwt的java客户端:https://github.com/jwtk/jjwt
现在有一个接口, http://www.xxxx.com/user/account?userId = 1 , 它的功能是获取用户当前账户信息,比如用户名、余额等等,这些属于用户的隐私信息,不能随意被别人获取,在不做任何限制的情况下,我只要知道接口地址,并且改变请求参数,比如“userId=2”、“userId=3”,我就可以获取到任意用户的信息,这显示不是我们想要的。
所以服务端要对请求的人进行身份认证,知道你是谁,确保你有权限访问数据
实现方法大体是,服务端提前给用户分配一个id 和对应密钥,比如 id =100、secretKey=HD38Jab15R,客户端和服务端都要保存。
客户端请求时,将secretKey和请求参数拼接,得到: userId=100&arg1=xx&arg2=yyy&secretKey=HD38Jab15R
再将上面的字符串进行MD5计算: MD5( userId=100&arg1=xx&arg2=yyy&secretKey=HD38Jab15R ) = 2b405bcf0f21eb6ea4ade30b75f7b709
得到签名:2b405bcf0f21eb6ea4ade30b75f7b709
最后将请求参数和签名进行组合请求(不要传送secretKey)
http://www.xxxx.com/user/account?userId = 100 &arg1=xx&arg2=yyy & sign = 2b405bcf0f21eb6ea4ade30b75f7b709
服务端收到请求后,把保存的userId=100对应的密钥取出(secretKey=HD38Jab15R),和客户端做相同的运算步骤,也得到一个签名B,签名B和用户传送过来的sign参数做对比,如果相同身份认证成功,代表请求者的确是userId=100,可以返回它的数据。如果请求方生成sign之后,再请求时任何一个参数变化了,那么最后服务端计算的的MD5值和传过来的sign一定不相等,认为身份认证不通过。
这种方法, 需要客户端和服务端都保存 ID + secretKey,每次请求客户端和服务端都要进行计算,无需用户名与密码,无需登录,适用于客户端也是一台服务器的时候(或者有条件并适合保存ID + secretKey的终端),假如客户端是一个游览器,就不能用此方法。
传统的cookie和session实现方法本文就不详细介绍了,基本思路就是用户在游览器上通过userName和password进行登录,登录后的用户信息维护在cookie或session里,在请求接口时检查cookie或session里的值来判断身份。此方法只适用于单服务器。
假如有A、B、C多台服务器时, 由于每台服务器session是在各自的内存中相互隔离的,用户在A服务器上登录后,A保存了用户的session,但是B 和 C并没有保存session,需要重新登录,退出时也要在ABC服务器上都退出。
比如说,我们请求
总结:现在前后端分离、微服务、分布式系统普及的情况下,这种传统的cookie或session做法显然已不适用。
客户端使用用户名跟密码请求登录,服务端收到请求,去验证用户名与密码;
验证成功后,服务端会根据一定规则(比如包含用户标识id)生成一个 Token,再把这个 Token 发送给客户端
客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里
客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据
个人认为token身份认证是一个技术规范,JWT是其中一种实现方式,也可以自行实现token+redis存储。
一个jwt字符串如下,由3段组成: header.playload.sign
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMDAiLCJpYXQiOjE1OTExNzI5MzAsImV4cCI6MTU5MTE3NjUzMH0.IF7NPMOu7mYSehHl-7_Zunyj1Fk8LINeZ7V9RxHMNs8
头部用于描述关于该JWT的最基本的信息,最重要的一个参数是"alg",它代表签名时的算法,默认是HS256(HMAC-SHA256),点击查看其他算法
header的明文是一个json字符串,比如: {"alg":"HS256"}
header的base64值是 : eyJhbGciOiJIUzI1NiJ9
载荷就是存放有效信息的地方,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。
JWT指定七个默认字段供选择,不是必填项。
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
exp: jwt的过期时间,这个过期时间必须要大于签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
playload的明文,比如: {"jti":"100","iat":1591172930,"exp":1591176530}
它的base64值是:eyJqdGkiOiIxMDAiLCJpYXQiOjE1OTExNzI5MzAsImV4cCI6MTU5MTE3NjUzMH0
服务端会保存一个密钥secretKey,将上面的 header+“.”+playload 拼接好后,配合secretKey使用HS256进行计算,
得到签名sign,在一起组装成 header.playload.sign 的样子,将它返回给用户,最终token如下
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMDAiLCJpYXQiOjE1OTExNzI5MzAsImV4cCI6MTU5MTE3NjUzMH0.IF7NPMOu7mYSehHl-7_Zunyj1Fk8LINeZ7V9RxHMNs8
客户端保存这个好这个token,在每一次请求的时候带上,服务端收到token时会把传过来的header.playload 和 自己保存secretKey计算后,得到新的sign2,再和客户端传过来的sign做一次对比。
工具类JJWT(Java Json Web Token),导入maven依赖,参考文档 https://github.com/jwtk/jjwt
io.jsonwebtoken
jjwt-api
0.11.1
io.jsonwebtoken
jjwt-impl
0.11.1
runtime
io.jsonwebtoken
jjwt-jackson
0.11.1
runtime
public static void main(String[] args){
Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256); // 呼叫JJWT帮我们生成一个密钥
System.out.println("secretKey:"+ new String(Base64.getEncoder().encode(key.getEncoded()) ) );
}
JJWT帮我生成的密钥是: k15M96V0XyjcoLs+7bPbpAjMuBPRMsoM2wTRpIZdxfw=
public static void main(String[] args) {
String userId = "100"; //在用户登录成功之后,我们假如得到了用户id
String secretKey = "k15M96V0XyjcoLs+7bPbpAjMuBPRMsoM2wTRpIZdxfw="; //刚才JJWT帮我生成的密钥
SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey)); //召唤JJWT帮我们封装成密钥对象
Date now = new Date();
Date exp = new Date(now.getTime() + 3600 * 1000); //过期时间1小时
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("创建时间:" + df.format(now));
System.out.println("过期时间:" + df.format(exp));
//生成令牌
String token = Jwts.builder().setId(userId)
.setIssuedAt(now)
.setExpiration(exp)
.signWith(key)
.compact();
System.out.println(token);
}
得到token : eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMDAiLCJpYXQiOjE1OTExNzI5MzAsImV4cCI6MTU5MTE3NjUzMH0.IF7NPMOu7mYSehHl-7_Zunyj1Fk8LINeZ7V9RxHMNs8
public static void test(String[] args){
try {
String secretKey = "k15M96V0XyjcoLs+7bPbpAjMuBPRMsoM2wTRpIZdxfw="; //刚才JJWT帮我生成的密钥,必须和加密时用同一个密钥
SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey)); //召唤JJWT帮我们封装成密钥对象
String token ="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMDAiLCJpYXQiOjE1OTExNzI5MzAsImV4cCI6MTU5MTE3NjUzMH0.IF7NPMOu7mYSehHl-7_Zunyj1Fk8LINeZ7V9RxHMNs8";
Jws jwt = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
}
catch (JwtException e){
e.printStackTrace();
}
}
如果验证失败,会抛出异常 io.jsonwebtoken.security.SignatureException
如果过期,会抛出异常 io.jsonwebtoken.ExpiredJwtException
public static void test(String[] args){
try {
String secretKey = "k15M96V0XyjcoLs+7bPbpAjMuBPRMsoM2wTRpIZdxfw="; //刚才JJWT帮我生成的密钥,必须和加密时用同一个密钥
SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey)); //召唤JJWT帮我们封装成密钥对象
String token ="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMDAiLCJpYXQiOjE1OTExNzI5MzAsImV4cCI6MTU5MTE3NjUzMH0.IF7NPMOu7mYSehHl-7_Zunyj1Fk8LINeZ7V9RxHMNs8";
Jws jwt = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
String userId = jwt.getBody().getId();
System.out.println(userId ); //看看发起请求的是谁
}
catch (JwtException e){
e.printStackTrace();
}
}
1、客户端从其中一个服务器拿到token后,部署多个服务器只要配置相同的secretKey就能成功解析,所以JWT实现了分布式的Web应用授权;
2、服务端生成token后直接返回给客户端了,服务端并不保存token(所以不依赖redis或者数据库),每一次客户端请求时服务端都是要通过secretKey重新计算来认证的,所以JWT是无状态的;
3、由于客户端并不保存secretKey,所以客户端没有泄露secretKey的风险,变更secretKey时只要服务端自己变就行了;
4、由于服务端并不保存token,所以JWT无法对token进行一些业务操作,比如统计当前token数量,退出登录
5、JWT生成token时通常会指定有效期,而playload的任何字段一旦更改,校验就不会通过,所以无法通过修改有效期给token续签延期,到期后必须重新申请令牌
6、所有token实现方式都要面临“token泄露”的问题,初级的做法是设置token有效期不要太长(一般1-2小时,最长不超过1天),更好的方案楼主也在学习中;
针对4、5的问题,可以使用自实现token+redis的方法解决。
授权和身份认证是核心需求,它的解决方案有很多,根据项目复杂度和需求选择合适的解决方案才是最好的