目录
一、JWT简介
1.什么是JWT?
2.为什么要使用JWT?
二、JWT的工作原理
三、JWT的组成
1.Header(头部)
2.Payload(载荷)
Reserved claims(保留)
Public claims(公有)
Private claims(私有)
3.signature
四、JWT的验证过程
五、JWT令牌刷新思路
1.首先前后端跨域配置
2.JWT配置
3.配置JWT验证过滤器
4.登陆方法成功后,将生成的JWT令牌通过响应头返回给客户端
5.在state.js中定义jwt变量拿来保存令牌
6.getters.js取值方法
7.mutations.js中设置值的方法
8.main.js中获取根实列
9.配置前端服务器拦截器
一、JWT简介
1.什么是JWT?
JWT(JSON WEBTOKEN):JSON网络令牌,JWT是一个轻便的安全跨平台传输格式,定义了一个紧凑的自包含的方式在不同实体之间安全传输信息(JSON格式)。它是在Web环境下两个实体之间传输数据的一项标准。实际上传输的就是一个字符串。广义上讲JWT是一个标准的名称;狭义上JWT指的就是用来传递的那个token字符串
JSON Web Token (JWT),它是目前最流行的跨域身份验证解决方案
2.为什么要使用JWT?
JWT的精髓在于:“去中心化”,就是数据保存在各个客户端而不是服务器
二、JWT的工作原理
- 客户端通过Web表单将正确的用户名和密码发送到服务端的接口。这一过程一般是POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
- 服务端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT。形成的JWT就是一个形lll.zzz.xxx的字符串,并设置有效时间。
- 服务端将JWT字符串作为登录成功的返回结果返回给客户端。
- 客户端将返回的JWT以cookie的形式保存在浏览器上,并设置cookie的有效时间(建议客户端cookie和服务端JWT的有效时间设置为一致),用户登出时客户端需删除cookie。
- 客户端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题)
- 服务端对收到的JWT进行解密和校验,如检查签名是否正确、Token是否过期、Token的接收方是否是自己等。
- 验证通过后服务端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果,否则返回401。
三、JWT的组成
JWT由三部分组成:
- Header(头部)
- Payload(负载)
- Signature(签名)
因此,JWT通常如下所示:
hhhhhhh.pppppp.ssssss
1.Header(头部)
由令牌的类型(typ)和正在使用的签名算法(alg)组成。如:
{"typ":"JWT","alg":"HS256"}
- typ跟alg属性的全称其实是type跟algorithm,分别是类型跟算法的意思。之所以都用三个字母来表示,也是基于JWT最终字串大小的考虑,
- 同时也是跟JWT这个名称保持一致,这样就都是三个字符了…typ跟alg是JWT中标准中规定的属性名称
2.Payload(载荷)
payload用来承载要传递的数据,它的json结构实际上是对JWT要传递的数据的一组声明,这些声明被JWT标准称为claims
载荷的属性有三类:
保留(Registered) 公有(public) 私有(private)
{"sub":"123","name":"Tom","admin":true}
Reserved claims(保留)
它的含义就像是编程语言的保留字一样,属于JWT标准里面规定的一些claim。JWT标准里面定义好的claim有:
- 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的唯一标识。
Public claims(公有)
在使用 JWT 时可以额外定义的载荷。为了避免冲突,应该使用 IANA JSON Web Token Registry中定义好的,或者给额外载荷加上类似命名空间的唯一标识。
Private claims(私有)
这个指的就是自定义的claim,比如前面那个示例中的admin和name都属于自定的claim。这些claim跟JWT标准规定的claim区别在于:JWT规定的claim,
- JWT的接收方在拿到JWT之后,都知道怎么对这些标准的claim进行验证;而private claims不会验证,除非明确告诉接收方要对这些claim进行验证以及规则才行
- 按照JWT标准的说明:保留的claims都是可选的,在生成payload不强制用上面的那些claim,你可以完全按照自己的想法来定义payload的结构,不过这样搞根本没必要:
- 第一是,如果把JWT用于认证, 那么JWT标准内规定的几个claim就足够用了,甚至只需要其中一两个就可以了,假如想往JWT里多存一些用户业务信息,
- 比如角色和用户名等,这倒是用自定义的claim来添加;第二是,JWT标准里面针对它自己规定的claim都提供了有详细的验证规则描述,
- 每个实现库都会参照这个描述来提供JWT的验证实现,所以如果是自定义的claim名称,那么你用到的实现库就不会主动去验证这些claim
3.signature
- 签名是把header和payload对应的json结构进行base64url编码之后得到的两个串用英文句点号拼接起来,然后根据header里面alg指定的签名算法生成出来的。
- 算法不同,签名结果不同。以alg: HS256为例来说明前面的签名如何来得到。
- 按照前面alg可用值的说明,HS256其实包含的是两种算法:HMAC算法和SHA256算法,前者用于生成摘要,后者用于对摘要进行数字签名。这两个算法也可以用HMACSHA256来统称
四、JWT的验证过程
它验证的方法其实很简单,只要把header做base64url解码,就能知道JWT用的什么算法做的签名,然后用这个算法,再次用同样的逻辑对header和payload做一次签名,并比较这个签名是否与JWT本身包含的第三个部分的串是否完全相同,只要不同,就可以认为这个JWT是一个被篡改过的串,自然就属于验证失败了。接收方生成签名的时候必须使用跟JWT发送方相同的密钥。
注1:在验证一个JWT的时候,签名认证是每个实现库都会自动做的,但是payload的认证是由使用者来决定的。因为JWT里面可能会包含一个自定义claim,所以它不会自动去验证这些claim,以jjwt-0.7.0.jar为例:
1.如果签名认证失败会抛出如下的异常:
io.jsonwebtoken.SignatureException
即签名错误,JWT的签名与本地计算机的签名不匹配
2.JWT过期异常
io.jsonwebtoken.ExpiredJwtException
就是令牌超过了过期时间
五、JWT令牌刷新思路
1.首先前后端跨域配置
CorsFilter:
package com.zking.vue.util;import java.io.IOException;import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;/*** 配置tomcat允许跨域访问* * @author Administrator**/
public class CorsFilter implements Filter {@Overridepublic void init(FilterConfig filterConfig) throws ServletException {}// @Override// public void doFilter(ServletRequest servletRequest, ServletResponse// servletResponse, FilterChain filterChain)// throws IOException, ServletException {// HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;//// // Access-Control-Allow-Origin就是我们需要设置的域名// // Access-Control-Allow-Headers跨域允许包含的头。// // Access-Control-Allow-Methods是允许的请求方式// httpResponse.addHeader("Access-Control-Allow-Origin", "*");// *,任何域名// httpResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT,// DELETE");// // httpResponse.setHeader("Access-Control-Allow-Headers", "Origin,// // X-Requested-With, Content-Type, Accept");//// // 允许请求头Token// httpResponse.setHeader("Access-Control-Allow-Headers",// "Origin,X-Requested-With, Content-Type, Accept, Token");// HttpServletRequest req = (HttpServletRequest) servletRequest;// System.out.println("Token=" + req.getHeader("Token"));// if("OPTIONS".equals(req.getMethod())) {// return;// }////// filterChain.doFilter(servletRequest, servletResponse);// }@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)throws IOException, ServletException {HttpServletResponse resp = (HttpServletResponse) servletResponse;HttpServletRequest req = (HttpServletRequest) servletRequest;// Access-Control-Allow-Origin就是我们需要设置的域名// Access-Control-Allow-Headers跨域允许包含的头。// Access-Control-Allow-Methods是允许的请求方式resp.setHeader("Access-Control-Allow-Origin", "*");// *,任何域名resp.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE");// resp.setHeader("Access-Control-Allow-Headers", "Origin,X-Requested-With,// Content-Type, Accept");// 允许客户端,发一个新的请求头jwtresp.setHeader("Access-Control-Allow-Headers", "Origin,X-Requested-With, Content-Type, Accept, jwt");// 允许客户端,处理一个新的响应头jwtresp.setHeader("Access-Control-Expose-Headers", "jwt");// String sss = resp.getHeader("Access-Control-Expose-Headers");// System.out.println("sss=" + sss);// 允许请求头Token// httpResponse.setHeader("Access-Control-Allow-Headers","Origin,X-Requested-With,// Content-Type, Accept, Token");// System.out.println("Token=" + req.getHeader("Token"));if ("OPTIONS".equals(req.getMethod())) {// axios的ajax会发两次请求,第一次提交方式为:option,直接返回即可return;}filterChain.doFilter(servletRequest, servletResponse);}@Overridepublic void destroy() {}
}
2.JWT配置
JwtFilter
package com.zking.vue.jwt;import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;import io.jsonwebtoken.Claims;/*** * JWT验证过滤器,配置顺序 :CorsFilte-->JwtFilter-->struts2中央控制器* * @author Administrator**/
public class JwtFilter implements Filter {// 排除的URL,一般为登陆的URL(请改成自己登陆的URL)private static String EXCLUDE = "^/userAction\\.action?.*$";private static Pattern PATTERN = Pattern.compile(EXCLUDE);private boolean OFF = false;// true关闭jwt令牌验证功能@Overridepublic void init(FilterConfig filterConfig) throws ServletException {}@Overridepublic void destroy() {}@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletException {HttpServletRequest req = (HttpServletRequest) request;HttpServletResponse resp = (HttpServletResponse) response;String path = req.getServletPath();if (OFF || isExcludeUrl(path)) {// 登陆直接放行chain.doFilter(request, response);return;}// 从客户端请求头中获得令牌并验证String jwt = req.getHeader(JwtUtils.JWT_HEADER_KEY);Claims claims = this.validateJwtToken(jwt);if (null == claims) {// resp.setCharacterEncoding("UTF-8");resp.sendError(403, "JWT令牌已过期或已失效");return;} else {String newJwt = JwtUtils.copyJwt(jwt, JwtUtils.JWT_WEB_TTL);resp.setHeader(JwtUtils.JWT_HEADER_KEY, newJwt);chain.doFilter(request, response);}}/*** 验证jwt令牌,验证通过返回声明(包括公有和私有),返回null则表示验证失败*/private Claims validateJwtToken(String jwt) {Claims claims = null;try {if (null != jwt) {claims = JwtUtils.parseJwt(jwt);}} catch (Exception e) {e.printStackTrace();}return claims;}/*** 是否为排除的URL* * @param path* @return*/private boolean isExcludeUrl(String path) {Matcher matcher = PATTERN.matcher(path);return matcher.matches();}// public static void main(String[] args) {// String path = "/sys/userAction_doLogin.action?username=zs&password=123";// Matcher matcher = PATTERN.matcher(path);// boolean b = matcher.matches();// System.out.println(b);// }}
JwtUtils
package com.zking.vue.jwt;import java.util.Date;
import java.util.Map;
import java.util.UUID;import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;import org.apache.commons.codec.binary.Base64;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;/*** JWT验证过滤器:配置顺序 CorsFilte->JwtUtilsr-->StrutsPrepareAndExecuteFilter**/
public class JwtUtils {/*** JWT_WEB_TTL:WEBAPP应用中token的有效时间,默认30分钟*/public static final long JWT_WEB_TTL = 30 * 60 * 1000;/*** 将jwt令牌保存到header中的key*/public static final String JWT_HEADER_KEY = "jwt";// 指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。private static final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;private static final String JWT_SECRET = "f356cdce935c42328ad2001d7e9552a3";// JWT密匙private static final SecretKey JWT_KEY;// 使用JWT密匙生成的加密keystatic {byte[] encodedKey = Base64.decodeBase64(JWT_SECRET);JWT_KEY = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");}private JwtUtils() {}/*** 解密jwt,获得所有声明(包括标准和私有声明)* * @param jwt* @return* @throws Exception*/public static Claims parseJwt(String jwt) {Claims claims = Jwts.parser().setSigningKey(JWT_KEY).parseClaimsJws(jwt).getBody();return claims;}/*** 创建JWT令牌,签发时间为当前时间* * @param claims* 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)* @param ttlMillis* JWT的有效时间(单位毫秒),当前时间+有效时间=过期时间* @return jwt令牌*/public static String createJwt(Map<String, Object> claims, long ttlMillis) {// 生成JWT的时间,即签发时间long nowMillis = System.currentTimeMillis();// 下面就是在为payload添加各种标准声明和私有声明了// 这里其实就是new一个JwtBuilder,设置jwt的bodyJwtBuilder builder = Jwts.builder()// 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的.setClaims(claims)// 设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。// 可以在未登陆前作为身份标识使用.setId(UUID.randomUUID().toString().replace("-", ""))// iss(Issuser)签发者,写死// .setIssuer("zking")// iat: jwt的签发时间.setIssuedAt(new Date(nowMillis))// 代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可放数据{"uid":"zs"}。此处没放// .setSubject("{}")// 设置签名使用的签名算法和签名使用的秘钥.signWith(SIGNATURE_ALGORITHM, JWT_KEY)// 设置JWT的过期时间.setExpiration(new Date(nowMillis + ttlMillis));return builder.compact();}/*** 复制jwt,并重新设置签发时间(为当前时间)和失效时间* * @param jwt* 被复制的jwt令牌* @param ttlMillis* jwt的有效时间(单位毫秒),当前时间+有效时间=过期时间* @return*/public static String copyJwt(String jwt, Long ttlMillis) {Claims claims = parseJwt(jwt);// 生成JWT的时间,即签发时间long nowMillis = System.currentTimeMillis();// 下面就是在为payload添加各种标准声明和私有声明了// 这里其实就是new一个JwtBuilder,设置jwt的bodyJwtBuilder builder = Jwts.builder()// 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的.setClaims(claims)// 设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。// 可以在未登陆前作为身份标识使用//.setId(UUID.randomUUID().toString().replace("-", ""))// iss(Issuser)签发者,写死// .setIssuer("zking")// iat: jwt的签发时间.setIssuedAt(new Date(nowMillis))// 代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可放数据{"uid":"zs"}。此处没放// .setSubject("{}")// 设置签名使用的签名算法和签名使用的秘钥.signWith(SIGNATURE_ALGORITHM, JWT_KEY)// 设置JWT的过期时间.setExpiration(new Date(nowMillis + ttlMillis));return builder.compact();}
}
3.配置JWT验证过滤器
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID" version="3.1"><display-name>j2eeVue</display-name><welcome-file-list><welcome-file>index.html</welcome-file><welcome-file>index.htm</welcome-file><welcome-file>index.jsp</welcome-file><welcome-file>default.html</welcome-file><welcome-file>default.htm</welcome-file><welcome-file>default.jsp</welcome-file></welcome-file-list><!-- Vue之axios跨域调用过滤器 --><filter><filter-name>corsFilter</filter-name><filter-class>com.zking.vue.util.CorsFilter</filter-class></filter><filter-mapping><filter-name>corsFilter</filter-name><url-pattern>/*</url-pattern></filter-mapping><filter><filter-name>jwtFilter</filter-name><filter-class>com.zking.vue.jwt.JwtFilter</filter-class></filter><filter-mapping><filter-name>jwtFilter</filter-name><url-pattern>/*</url-pattern></filter-mapping><!-- 中文乱码过滤器 --><filter><filter-name>encodingFilter</filter-name><filter-class>com.zking.vue.util.EncodingFilter</filter-class></filter><filter-mapping><filter-name>encodingFilter</filter-name><url-pattern>/*</url-pattern></filter-mapping><!-- Servlet核心控制器 --><servlet><servlet-name>ActionServlet</servlet-name><servlet-class>com.zking.mvc.framework.ActionServlet</servlet-class><init-param><param-name>config</param-name><param-value>/mvc.xml</param-value></init-param></servlet><servlet-mapping><servlet-name>ActionServlet</servlet-name><url-pattern>*.action</url-pattern></servlet-mapping>
</web-app>
4.登陆方法成功后,将生成的JWT令牌通过响应头返回给客户端
public String userLogin(HttpServletRequest req,HttpServletResponse resp) throws ServletException,IOException{Map<String,Object> json=new HashMap<String,Object>();User us = userDao.userLogin(user);if(null!=us) {json.put("code","1");json.put("msg", "登录成功");//生成JWT,并设置到response响应头中String jwt=JwtUtils.createJwt(json, JwtUtils.JWT_WEB_TTL);resp.setHeader(JwtUtils.JWT_HEADER_KEY, jwt);}else {json.put("code", "-1");json.put("msg", "用户名或密码错误");}mapper.writeValue(resp.getOutputStream(),json);return null;}
5.在state.js中定义jwt变量拿来保存令牌
export default{yourname:'名字',collapsed:false,jwt:null
}
6.getters.js取值方法
export default {gettername: (state) => {return state.yourname;},getcollapsed:(state)=>{return state.collapsed;},getJwt:(state)=>{return state.jwt;}
}
7.mutations.js中设置值的方法
export default {// mutations:处理数据的唯一途径,state的改变或赋值只能在这里// type(事件类型): 其值为setterName// state:数据状态的定义// payload:官方给它还取了一个高大上的名字:载荷,其实就是一个保存要传递参数的容器 传递json对象setterName: (state, payload) => {state.yourname = payload.name;},setcollapsed:(state)=>{state.collapsed=!state.collapsed;},setJwt:(state,payload)=>{state.jwt=payload.jwt;}
}
8.main.js中获取根实列
window.vm=new Vue({el: '#app',router,store, //在main.js中导入store实例components: { App },template: '<App/>'
})
9.配置前端服务器拦截器
/*** vue项目对axios的全局配置*/
import axios from 'axios'
import qs from 'qs'//引入action模块,并添加至axios的类属性urls上
import action from '@/api/action'
axios.urls = action// axios默认配置
axios.defaults.timeout = 10000; // 超时时间
// axios.defaults.baseURL = 'http://localhost:8080/j2eeVue'; // 默认地址
axios.defaults.baseURL = action.SERVER;//整理数据
// 只适用于 POST,PUT,PATCH,transformRequest` 允许在向服务器发送前,修改请求数据
axios.defaults.transformRequest = function(data) {data = qs.stringify(data);return data;
};// 请求拦截器
axios.interceptors.request.use(function(config) {console.log(config);//先从vuex中获取令牌let jwt =window.vm.$store.getters.getJwt;if(null!=jwt)config.headers['jwt']=jwt;return config;
}, function(error) {return Promise.reject(error);
});// 响应拦截器
axios.interceptors.response.use(function(response) {console.log(response);//获取jwt令牌存入vuexlet jwt=response.headers['jwt']console.log(jwt);window.vm.$store.commit('setJwt',{jwt:jwt});return response;
}, function(error) {return Promise.reject(error);
});// // 路由请求拦截
// // http request 拦截器
// axios.interceptors.request.use(
// config => {
// //config.data = JSON.stringify(config.data);
// //config.headers['Content-Type'] = 'application/json;charset=UTF-8';
// //config.headers['Token'] = 'abcxyz';
// //判断是否存在ticket,如果存在的话,则每个http header都加上ticket
// // if (cookie.get("token")) {
// // //用户每次操作,都将cookie设置成2小时
// // cookie.set("token", cookie.get("token"), 1 / 12)
// // cookie.set("name", cookie.get("name"), 1 / 12)
// // config.headers.token = cookie.get("token");
// // config.headers.name = cookie.get("name");
// // }
// return config;
// },
// error => {
// return Promise.reject(error.response);
// });// // 路由响应拦截
// // http response 拦截器
// axios.interceptors.response.use(
// response => {
// if (response.data.resultCode == "404") {
// console.log("response.data.resultCode是404")
// // 返回 错误代码-1 清除ticket信息并跳转到登录页面
// // cookie.del("ticket")
// // window.location.href='http://login.com'
// return
// } else {
// return response;
// }
// },
// error => {
// return Promise.reject(error.response) // 返回接口返回的错误信息
// });export default axios;