目录
一、JWT
1、什么是Jwt
2、为什么要使用Jwt
3、应用场景
4.Jwt的组成
4.1、Header
4.2、Payload
4.3、signature
二、Jwt验证过程
1、生成Jwt令牌
2、解析旧的Jwt
3、复制Jwt
4、Jwt有效时间测试
三、Jwt令牌刷新思路
1、配置JwtFilter过滤器
2、登录生成Jwt令牌
3、配置CorsFilter过滤器
4、Vuex存储Jwt令牌
5、修改main.js
6、配置axios请求响应头
前言:
随着前后端分离的发展,以及数据中心的建立,越来越多的公司会创建一个中心服务器,服务于各种产品线。而这些产品线上的产品,它们可能有着各种终端设备,包括但不仅限于浏览器、桌面应用、移动端应用、平板应用、甚至智能家居。
这些设备与中心服务器之间会进行http通信,一般来说,中心服务器至少承担着认证和授权的功能。例如登录:各种设备发送消息到中心服务器,然后中心服务器响应一个身份令牌,当这种结构出现后,就出现一个问题:它们之间还能使用传统的cookie方式传递令牌信息吗?
其实,也是可以的,因为cookie在传输中无非是一个消息头而已,只不过浏览器对这个消息头有特殊处理罢了。但浏览器之外的设备肯定不喜欢cookie,因为浏览器有着对cookie完善的管理机制,但是在其他设备上,就需要开发者自己手动处理了。
JWT 的出现就是为了解决这个问题!!!
一、JWT
1、什么是Jwt
JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。是目前最流行的跨域身份验证解决方案;
RFC 7519:RFC 7519 - JSON Web Token (JWT)
2、为什么要使用Jwt
它要解决的问题,就是为多种终端设备,提供统一的、安全的令牌格式。因此,jwt只是一个令牌格式而已,你可以把它存储到cookie,也可以存储到localstorage,没有任何限制!同样的,对于传输,你可以使用任何传输方式来传输jwt,一般来说,我们会使用消息头header来传输它。
比如:当登录成功后,服务器可以给客户端响应一个Jwt。
HTTP/1.1 200 OK
...
set-cookie:token=jwt令牌
authorization:jwt令牌
...{..., token:jwt令牌}
可以看到,jwt令牌可以出现在响应的任何一个地方,客户端和服务器自行约定即可。
当然,它也可以出现在响应的多个地方,比如为了充分利用浏览器的 cookie,同时为了照顾其他设备,也可以让 jwt 出现在 set-cookie 和 authorization 或 body 中,尽管这会增加额外的传输量。
总之,Jwt的精髓在于去中心化,数据是保存在客户端的。
3、应用场景
- 认证:认证是JWT的最常用场景。只要用户完成登录,其随后的请求都会包含JWT,以允许用户访问经由当前JWT授权的路由、服务或者是资源。由于开销小且能够被简单应用在跨域访问上,JWT在分布式站点上所支持的单点登录(SSO)已经是当前它被广泛应用的一个特性。
- 信息交换:JWT是一种在各参与方之间安全传递信息的良好方法。由于JWT可以被签名(例:使用公钥/秘钥对),因而可用于确认发送者自称的身份。除此之外,由于signature使用header和payload进行计算,也可以验证内容没有被篡改。
JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
4.Jwt的组成
一个Jwt实际上就是一个字符串,它由三部分组成:
-
Header(头部)
-
Payload(载荷)
-
Signature(签名)
eyJhbGciOiJIUzI1NiJ9.
eyJzdWIiOiJ7fSIsImlzcyI6InpraW5nIiwiZXhwIjoxNTYyODUwMjM3LCJpYXQiOjE1NjI4NDg0MzcsImp0aSI6ImM5OWEyMzRmMDc4NzQyZWE4YjlmYThlYmYzY2VhNjBlIiwidXNlcm5hbWUiOiJ6c3MifQ.
WUfqhFTeGzUZCpCfz5eeEpBXBZ8-lYg1htp-t7wD3I4
它是一个很长的字符串因此,中间用点(.)分隔成三个部分。
注意,Jwt 内部是没有换行的,这里只是为了便于展示,将它写成了几行。
4.1、Header
典型的JWT header包含两个部分:
-
typ(type)属性,用来标识整个token字符串是一个JWT字符串,即JWT。
-
alg(algorithm)属性,用来说明这个JWT签发的时候所使用的签名和摘要算法,比如HMAC SHA256或者RSA。
{"alg": "HS256","typ": "JWT"
}
4.2、Payload
JWT token的第二部分是包含了声明的payload,声明(要求)是一个实体的表述加上额外信息,一共有三种形式的声明:注册、公有和私有;
- 注册声明
它的json结构实际上是对JWT要传递的数据的一组声明,这些声明被JWT标准称为claims,它的一个属性值对其实就是一个claim(要求),每一个claim的都代表特定的含义和作用。
{"iss":"发行者","iat":"发布时间","exp":"到期时间","sub":"主题","aud":"听众","nbf":"在此之前不可用","jti":"JWT ID"
}
属性 | 说明 |
---|---|
iss(Issuser) | 代表这个JWT的签发主体 |
sub(Subject) | 代表这个JWT的主体,即它的所有人 |
aud(Audience) | 代表这个JWT的接收对象 |
exp(Expiration time) | 是一个时间戳,代表这个JWT的过期时间 |
nbf(Not Before) | 是一个时间戳,代表这个JWT生效的开始时间,意味着在这个时间之前验证JWT是会失败的 |
iat(Issued at) | 是一个时间戳,代表这个JWT的签发时间 |
jti(JWT ID) | 是JWT的唯一标识 |
- 公有声明
公有声明可以加入任何信息,一般会添加用户相关信息或者业务需要的信息,但不建议添加敏感信息,因为该部分会在客户端解密。
- 私有声明
私有声明是提供者和使用者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密,基本等同于明文信息。
如果把JWT用于认证, 那么JWT标准内规定的几个claim就足够用了,甚至只需要其中一两个就可以了,假如想往JWT里多存一些用户业务信息,比如角色和用户名等,这倒是可以用私有声明来添加。
4.3、signature
JWT的第三部分是签名信息,Signature由三部分组成:
-
header(base64加密后)
-
payload(base64加密后)
-
secret (盐)
签名需要将base64加密后的header和payload使用.
连接,然后通过header所使用的加密方式进行加盐(secret)组合加密,产生了jwt的第三部分。以使用HMAC SHA256算法为例;
按照前面alg可用值的说明,HS256其实包含的是两种算法:HMAC算法和SHA256算法,前者用于生成摘要,后者用于对摘要进行数字签名。这两个算法也可以用HMAC SHA256来统称;
HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
签名是用于验证消息在传递过程中有没有被更改,并且,对于使用私钥签名的token,它还可以验证JWT的发送方是否为它所称的发送方。
算法不同,签名结果不同。
二、Jwt验证过程
可将绑定的资源中src(ElementUI之Jwt)目录下的JwtUtils.java
和JwtDemo.java
导入到项目中的util包下:
1、生成Jwt令牌
//定义私有声明
Map<String, Object> claims = new HashMap<String, Object>();
claims.put("username", "zss");
claims.put("age", 18);
//根据私有声明和JWT的token有效时间生成JWT
String jwt = JwtUtils.createJwt(claims, JwtUtils.JWT_WEB_TTL);//获取JWT中的声明信息,包括:私有声明和标准声明
Claims parseJwt = JwtUtils.parseJwt(jwt);
for (Map.Entry<String, Object> entry : parseJwt.entrySet()) {System.out.println(entry.getKey() + "=" + entry.getValue());
}
//获取token令牌的签发时间
Date d1 = parseJwt.getIssuedAt();
//获取token令牌的过期时间
Date d2 = parseJwt.getExpiration();
System.out.println("令牌签发时间:" + sdf.format(d1));
System.out.println("令牌过期时间:" + sdf.format(d2));
2、解析旧的Jwt
解析旧的JWT。如果超过JWT本身定义的过期时间,则直接抛出ExpiredJwtException
//旧的JWT令牌
String oldJwt = "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NjM2NzY0NDQsImlhdCI6MTU2MzY3NDY0NCwiYWdlIjoxOCwianRpIjoiMzE5MmYxOTg4NzFkNGVkZWIyMzU0MmY3NWVhMWI5NDciLCJ1c2VybmFtZSI6InpzcyJ92.4VnkdvGNNe8U1EiKaLz7h6bOJkVGSGtfqojcP_y-0Cc";
Claims parseJwt = JwtUtils.parseJwt(oldJwt);
for (Map.Entry<String, Object> entry : parseJwt.entrySet()) {System.out.println(entry.getKey() + "=" + entry.getValue());
}
Date d1 = parseJwt.getIssuedAt();
Date d2 = parseJwt.getExpiration();
System.out.println("令牌签发时间:" + sdf.format(d1));
System.out.println("令牌过期时间:" + sdf.format(d2));
3、复制Jwt
模拟Session功能(Tomcat默认session过期时间:30分钟)。当有访问的情况下,自动延长jwt中的token令牌过期时间,在原有时间上+30秒;
String oldJwt = "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NjM2NzUzMDAsImlhdCI6MTU2MzY3MzUwMCwiYWdlIjoxOCwianRpIjoiYjY5MmE5ZjZkMGZhNDMyN2I1YWY2NTI5OGMzMjQ5MTYiLCJ1c2VybmFtZSI6InpzcyJ9.asHGMPFKURMLnooK29abAuKEdLOHBycAuTovNuGQio0";
String jwt = JwtUtils.copyJwt(oldJwt, JwtUtils.JWT_WEB_TTL);
Claims parseJwt = JwtUtils.parseJwt(jwt);
for (Map.Entry<String, Object> entry : parseJwt.entrySet()) {System.out.println(entry.getKey() + "=" + entry.getValue());
}
Date d1 = parseJwt.getIssuedAt();
Date d2 = parseJwt.getExpiration();
System.out.println("令牌签发时间:" + sdf.format(d1));
System.out.println("令牌过期时间:" + sdf.format(d2));
4、Jwt有效时间测试
生成新的Jwt令牌,设置令牌有效时间3秒钟,在3秒钟后再次解析Jwt令牌提示令牌已过期。
Map<String, Object> claims = new HashMap<String, Object>();
claims.put("username", "zss");
String jwt = JwtUtils.createJwt(claims, 3 * 1000L);
System.out.println(jwt);
Claims parseJwt = JwtUtils.parseJwt(jwt);
Date d1 = parseJwt.getIssuedAt();
Date d2 = parseJwt.getExpiration();
System.out.println("令牌签发时间:" + sdf.format(d1));
System.out.println("令牌过期时间:" + sdf.format(d2));
三、Jwt令牌刷新思路
使用ssm+vue+elementui实现jwt验证。
1、配置JwtFilter过滤器
导入JwtFilter.java
到utils包下,并注意配置顺序问题:CorsFilter
> JwtFilter
> SpringMVC核心控制器
<!-- JwtFilter -->
<filter><filter-name>jwtFilter</filter-name><filter-class>com.wsl.ssm.jwt.JwtFilter</filter-class>
</filter>
<filter-mapping><filter-name>jwtFilter</filter-name><url-pattern>/*</url-pattern>
</filter-mapping>
2、登录生成Jwt令牌
在登录成功后,使用JwtUtil工具类生成JWT,并将Jwt令牌添加到响应头(response)中带到前端页面。在这里可以将用户对应用户基本信息和用户角色信息保存到私有claim中。
if(userVo.getUsername().equals("admin")&&userVo.getPassword().equals("123")){//私有要求claimMap<String,Object> json=new HashMap<String,Object>();json.put("username", userVo.getUsername());//生成JWT,并设置到response响应头中String jwt=JwtUtils.createJwt(json, JwtUtils.JWT_WEB_TTL);response.setHeader(JwtUtils.JWT_HEADER_KEY, jwt);return new JsonResponseBody<>("用户登陆成功!",true,0,null);
}else{return new JsonResponseBody<>("用户名或密码错误!",false,0,null);
}
3、配置CorsFilter过滤器
浏览器只能访问默认的响应头,例如:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma等,如果想让浏览器能访问到其他的响应头的话,需要在服务器上设置。在CorsFilter跨域过滤器的doFilter方法中配置其他响应头的支持。
//允许客户端发一个新的请求头jwt
httpResponse.setHeader("Access-Control-Allow-Headers","Origin,X-Requested-With, Content-Type, Accept, jwt");
//允许客户端处理一个新的响应头jwt
httpResponse.setHeader("Access-Control-Expose-Headers", "jwt");
4、Vuex存储Jwt令牌
使用vuex
保存后端服务器提交过来的Jwt
,分别在state、getters、mutation
中定义信息。
- state.js
export default {jwt:null }
- getters.js
export default {getJwt:(state)=>{return state.jwt;} }
- mutation.js
export default {setJwt:function (state, payload){state.jwt = payload.jwt;} }
5、修改main.js
解决axios从响应头获得jwt令牌并保存到vuex,通过修改main.js中的配置window.vm=new Vue({});获取Vue根实例对象。
window.vm = new Vue({el: '#app',router,store,components: { App },template: '<App/>'
})
6、配置axios请求响应头
在vue的http.js中配置axios的请求响应拦截器,用于存取Jwt,并将Jwt信息保存到Vuex中。
// 请求拦截器
axios.interceptors.request.use(function(config) {let jwt=window.vm.$store.getters.jwt;//console.log(jwt);if(jwt)config.headers['jwt']=jwt;return config;
}, function(error) {return Promise.reject(error);
});// 响应拦截器
axios.interceptors.response.use(function(response) {let jwt=response.headers['jwt'];if(jwt)window.vm.$store.commit('setJwt',{jwt:jwt});return response;
}, function(error) {return Promise.reject(error);
});
注:可通过使用vuex-along解决刷新state初始化问题。