从入门到运用,Jwt其实并不难!
小Hub领读:
2018年写的一篇文章,哈哈哈,体验一下我当时的写作风格咧~
一般情况下,web项目都是通过session进行认证,每次请求数据时,都会把jsessionid放在cookie中,以便与服务端保持会话。
前后端分离项目中,通过token进行认证(登录时,生成唯一的token凭证),每次请求数据时,都会把token放在header中,服务端解析token,并确定用户身份及用户权限,数据通过json交互。
但是token一般都是UUID生成的一个随机码,作为一个key使用,从缓存中获取具体的用户信息。所以一般需要一个存储介质来保存token和用户信息。在一些场景中,如单点登录时候有点麻烦。
有没一种更方便的方式呢?答案是有的,就是我们今天要讲的jwt。jwt也算是一个特殊的token,不过jwt中自带了用户的相关信息,所以不需要存储介质,只需要验证签名保证安全的前提下就可以直接获取到用户的相关信息。
在讲jwt之前,我们先回顾一下session、token的相关内容。
session与cookie
我们都知道http是无状态的,所以需要某种机制来识别用户和保存用户的状态。而这个机制就是session。session是保存在服务端的,服务器通过session辨别用户,然后做权限认证等。
那如何才知道用户的session是哪个?这时候cookie就出场了,浏览器第一次与服务器建立连接的时候,服务器会生成一个sessionid返回浏览器,浏览器把这个sessionid存储到cookie当中,以后每次发起请求都会在请求头cookie中带上这个sessionid信息,所以服务器就是根据这个sessionid所以key获取到具体session。
google浏览器中查看cookie内容的方法有两个:
(一)F12,查看具体请求链接的请求头信息
(二)点击浏览器输入框的认证小锁,可以查看这个域名的相关cookie信息。
涉及到集群环境得话,session需要弄成分布式session,从而保证多个应用的会话状态一致性。spring项目可以使用spring session+redis来解决session共享问题。shiro项目可以重写redis版SessionDAO,把会话信息存到redis中实现共享。
(所以,明白没?)
接下来我们再来聊聊token。
Token
token,就是我们常说的用户身份令牌。只有涉及到受限资源的访问时候才需要身份令牌,所以,在访问开放资源时候http中是没有token的信息的,也即是说这时候会话是完全无状态的。token的是在用户登录以后生成的。用户登录之后我们会生成一个token作为key保存用户的信息并返回给客户端。保存方式set(token,用户信息)存储到redis等介质。之后客户端发起的请求只要在请求头中附带token的信息就可以完成身份认证。
开源项目renren-fast采用了前后分离的机制,使用token来完成身份认证,并且集成了shiro框架,所以想实战的可以去clone下来玩玩~
https://gitee.com/renrenio/renren-fast
(只能帮你到这了~)
好了,说了这么拓展知识,接下来我们进入我们的正题!jwt。
JWT
jwt是什么
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519)。该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。
JWT的声明一般被用来在身份提供者和服务提供者间传递*被认证的用户身份信息*,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
jwt的特点
简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快
自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库或缓存。
说了这么多~
真面目:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
可能太长看不清,来个图片:
傻眼了吧?看不懂吧,哈哈哈哈哈~~~~~~~~~
那么我们一一把这串数据分析一下。
首先从jwt的消息结构开始分析:
jwt消息结构
jwt有3个组成部分,分别是
*头部(header) *
*载荷(payload) *
签证(signature)
先回头看看jwt真面目那个例子。仔细点,认真点,有没在茫茫字母和数字中发现两个.(点号)。
就是这两个点号把jwt分成了3部分,分别对应着上面说的头部,载荷,签证。
先来讲讲头部:
Jwt的头部承载两部分信息:
声明类型,这里是jwt
声明加密的算法,通常直接使用HMACSHA256,就是HS256了
{"alg":"HS256","typ":"JWT"}
然后将头部进行base64编码构成了第一部分:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
(base64懂吧?) Base64是一种用64个字符来表示任意二进制数据的方法
Base64是一种任意二进制到文本字符串的编码方法,常用于在URL、Cookie、网页中传输少量二进制数据。
Java中可以使用java.util.Base64进行编码解码。
(我没骗你吧?头部就是这样来的)
然后我们看第二部分:载荷。
我依稀记得物理老师说过:直接施加在结构上的各种力,习惯上称为载荷(荷载)。
好了不吹牛了,这里是承载的意思。也就是说这里是承载消息具体内容的地方。
内容又可以分为3中标准
标准中注册的声明
公共的声明
私有的声明
payload-标准中注册的声明 (建议但不强制使用) :
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
payload-公共的声明 :
公共的声明可以添加任何的信息。一般这里我们会存放一下用户的基本信息(非敏感信息)。
payload-私有的声明 :
私有声明是提供者和消费者所共同定义的声明。
需要注意的是,不要存放敏感信息,不要存放敏感信息,不要存放敏感信息!!!
因为:这里也是base64编码,任何人获取到jwt之后都可以解码!!(产品应该就不懂)
好了,请容许我Base64解码一下载荷部分内容到底是啥。
{"sub":"1234567890","name":"John Doe","iat":1516239022}
sub和iat是标准声明,分别代表所面向的用户和jwt签发时间。从上面我知道了:
这个是发给一个账号是1234567890的用户(也许是ID)
名字叫John Doe
签发时间是1516239022(2018/1/18 9:30:22)
(牛逼~)
只剩下最后一部分了,待我一个闪现,外加一个大招秒杀它!
(emmmmm~~~,请求集合!!求救~)
签证部分貌似和Base64没啥关系呀,那到底是啥的~
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
别急,先来说说签证用来干啥的,其实就是一个签名信息,使用了自定义的一个密钥然后加密后的结果,目的就是为了保证签名的信息没有被别人改过!(也就是保证jwt安全可用) 头部那里我们不是定义了一个加密算法么,就是它
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
(我的密钥)
)
也就是说,签证部分的信息有3个组成部分:
头部-header (base64后的)
载荷-payload (base64后的)
密钥-secret
然后HMACSHA256只有两个参数,
base64后的头部 + "." + base64后的载荷
密钥-secret
好了,这部分我就不做代码演示了。因为有现成的工具类可以直接用,哈哈
下面我们就介绍一下常用的生成jwt的工具类:
https://jwt.io/
以上就是官网给我们介绍的几种java可用的jar包。其中,io.jsonwebtoken是最常用的工具包。
使用步骤如下:
第一步,导入jar包:
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
第二步:稍微封装一下,方便集成到项目中:
/**
* jwt工具类
* @author chenshun
* @email sunlightcs@gmail.com
* @date 2017/9/21 22:21
*/
@ConfigurationProperties(prefix = "renren.jwt")
@Component
public class JwtUtils {
private Logger logger = LoggerFactory.getLogger(getClass());
private String secret;
private long expire;
private String header;
/**
* 生成jwt token
*/
public String generateToken(long userId) {
Date nowDate = new Date();
//过期时间
Date expireDate = new Date(nowDate.getTime() + expire * 1000);
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(userId+"")
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public Claims getClaimByToken(String token) {
try {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}catch (Exception e){
logger.debug("validate is token error ", e);
return null;
}
}
/**
* token是否过期
* @return true:过期
*/
public boolean isTokenExpired(Date expiration) {
return expiration.before(new Date());
}
//getter、setter
}
生成jwt:
//生成token
String token = jwtUtils.generateToken(userId);
获取jwt的有效信息:
//获取载荷信息
Claims claims = jwtUtils.getClaimByToken(token);
if(claims == null || jwtUtils.isTokenExpired(claims.getExpiration())){
throw new RRException(jwtUtils.getHeader() + "失效,请重新登录", HttpStatus.UNAUTHORIZED.value());
}
long userId = Long.parseLong(claims.getSubject())
嘿嘿、以上例子来自之前说个的renren-fast项目。所以呀,你还是去看看吧~
(又学了一个技能,好有成就感!)
使用场景
闲聊一下jwt的使用场景
详细可以看看这篇文章:https://baijiahao.baidu.com/s?id=1598976581711450442&wfr=spider&for=pc
总结
最后的最后,再来个小总结:
在Web应用中,别再把JWT当做session使用,绝大多数情况下,传统的cookie-session机制工作得更好
JWT适合一次性的命令认证,颁发一个有效期极短的JWT,即使暴露了危险也很小,由于每次操作都会生成新的JWT,因此也没必要保存JWT,真正实现无状态。
至此,我想说的都说完了,我是吕一明,欢迎关注我公众号:MarkerHub。记得星标置顶我哇哈哈
(让我们一起摇摆~~)
(完)
MarkerHub文章索引:(点击阅读原文直达)
https://github.com/MarkerHub/JavaIndex
【推荐阅读】
超详细!4小时开发一个SpringBoot+vue前后端分离博客项目!!