JWT入门详解

家电修理 2023-07-16 19:17www.caominkang.com电器维修

目录

一、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中定义jt变量拿来保存令牌

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的工作原理
  1. 客户端通过Web表单将正确的用户名和密码发送到服务端的接口。这一过程一般是POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
     
  2. 服务端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT。形成的JWT就是一个形lll.zzz.xxx的字符串,并设置有效时间。
     
  3. 服务端将JWT字符串作为登录成功的返回结果返回给客户端。
     
  4. 客户端将返回的JWT以cookie的形式保存在浏览器上,并设置cookie的有效时间(建议客户端cookie和服务端JWT的有效时间设置为一致),用户登出时客户端需删除cookie。
     
  5. 客户端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题)
     
  6. 服务端对收到的JWT进行解密和校验,如检查签名是否正确、Token是否过期、Token的接收方是否是自己等。
     
  7. 验证通过后服务端使用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,以jjt-0.7.0.jar为例

1.如果签名认证失败会抛出如下的异常

        io.jsonebtoken.SignatureException

即签名错误,JWT的签名与本地计算机的签名不匹配

2.JWT过期异常

        io.jsonebtoken.ExpiredJtException

 就是令牌超过了过期时间

五、JWT令牌刷新思路

1.前后端跨域配置

CorsFilter:

package .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;


public class CorsFilter implements Filter {

	@Override
	public void init(FilterConfig filterConfig) thros ServletException {
	}

	// @Override
	// public void doFilter(ServletRequest servletRequest, ServletResponse
	// servletResponse, FilterChain filterChain)
	// thros IOException, ServletException {
	// HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
	//
	// // Aess-Control-Allo-Origin就是我们需要设置的域名
	// // Aess-Control-Allo-Headers跨域允许包含的头。
	// // Aess-Control-Allo-Methods是允许的请求方式
	// httpResponse.addHeader("Aess-Control-Allo-Origin", "");// ,任何域名
	// httpResponse.setHeader("Aess-Control-Allo-Methods", "POST, GET, PUT,
	// DELETe");
	// // httpResponse.setHeader("Aess-Control-Allo-Headers", "Origin,
	// // X-Requested-With, Content-Type, Aept");
	//
	// // 允许请求头Token
	// httpResponse.setHeader("Aess-Control-Allo-Headers",
	// "Origin,X-Requested-With, Content-Type, Aept, Token");
	// HttpServletRequest req = (HttpServletRequest) servletRequest;
	// System.out.println("Token=" + req.getHeader("Token"));
	// if("OPTIONS".equals(req.getMethod())) {
	// return;
	// }
	//
	//
	// filterChain.doFilter(servletRequest, servletResponse);
	// }

	@Override
	public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
			thros IOException, ServletException {
		HttpServletResponse resp = (HttpServletResponse) servletResponse;
		HttpServletRequest req = (HttpServletRequest) servletRequest;

		// Aess-Control-Allo-Origin就是我们需要设置的域名
		// Aess-Control-Allo-Headers跨域允许包含的头。
		// Aess-Control-Allo-Methods是允许的请求方式
		resp.setHeader("Aess-Control-Allo-Origin", "");// ,任何域名
		resp.setHeader("Aess-Control-Allo-Methods", "POST, GET, PUT, DELETE");
		// resp.setHeader("Aess-Control-Allo-Headers", "Origin,X-Requested-With,
		// Content-Type, Aept");
		// 允许客户端,发一个新的请求头jt
		resp.setHeader("Aess-Control-Allo-Headers", "Origin,X-Requested-With, Content-Type, Aept, jt");

		// 允许客户端,处理一个新的响应头jt
		resp.setHeader("Aess-Control-Expose-Headers", "jt");
		// String sss = resp.getHeader("Aess-Control-Expose-Headers");
		// System.out.println("sss=" + sss);

		// 允许请求头Token
		// httpResponse.setHeader("Aess-Control-Allo-Headers","Origin,X-Requested-With,
		// Content-Type, Aept, Token");
		// System.out.println("Token=" + req.getHeader("Token"));

		if ("OPTIONS".equals(req.getMethod())) {// axios的ajax会发两次请求,第一次提交方式为option,直接返回即可
			return;
		}
		filterChain.doFilter(servletRequest, servletResponse);
	}

	@Override
	public void destroy() {

	}
}

2.JWT配置

JtFilter

package .zking.vue.jt;

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.jsonebtoken.Claims;


public class JtFilter implements Filter {

	// 排除的URL,一般为登陆的URL(请改成自己登陆的URL)
	private static String EXCLUDE = "^/userAction\.action?.$";

	private static Pattern PATTERN = Pattern.pile(EXCLUDE);

	private boolean OFF = false;// true关闭jt令牌验证功能

	@Override
	public void init(FilterConfig filterConfig) thros ServletException {
	}

	@Override
	public void destroy() {
	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			thros IOException, ServletException {
		HttpServletRequest req = (HttpServletRequest) request;
		HttpServletResponse resp = (HttpServletResponse) response;
		String path = req.getServletPath();
		if (OFF || isExcludeUrl(path)) {// 登陆直接放行
			chain.doFilter(request, response);
			return;
		}

		// 从客户端请求头中获得令牌并验证
		String jt = req.getHeader(JtUtils.JWT_HEADER_KEY);
		Claims claims = this.validateJtToken(jt);
		if (null == claims) {
			// resp.setCharacterEncoding("UTF-8");
			resp.sendError(403, "JWT令牌已过期或已失效");
			return;
		} else {
			String neJt = JtUtils.copyJt(jt, JtUtils.JWT_WEB_TTL);
			resp.setHeader(JtUtils.JWT_HEADER_KEY, neJt);
			chain.doFilter(request, response);
		}
	}

	
	private Claims validateJtToken(String jt) {
		Claims claims = null;
		try {
			if (null != jt) {
				claims = JtUtils.parseJt(jt);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return claims;
	}

	
	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&passord=123";
	// Matcher matcher = PATTERN.matcher(path);
	// boolean b = matcher.matches();
	// System.out.println(b);
	// }

}

 JtUtils

package .zking.vue.jt;

import java.util.Date;
import java.util.Map;
import java.util.UUID;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

import .apache.mons.codec.binary.Base64;

import io.jsonebtoken.Claims;
import io.jsonebtoken.JtBuilder;
import io.jsonebtoken.Jts;
import io.jsonebtoken.SignatureAlgorithm;


public class JtUtils {
	
	public static final long JWT_WEB_TTL = 30  60  1000;

	
	public static final String JWT_HEADER_KEY = "jt";

	// 指定签名的时候使用的签名算法,也就是header那部分,jjt已经将这部分内容封装好了。
	private static final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;
	private static final String JWT_SECRET = "f356cdce935c42328ad2001d7e9552a3";// JWT密匙
	private static final SecretKey JWT_KEY;// 使用JWT密匙生成的加密key

	static {
		byte[] encodedKey = Base64.decodeBase64(JWT_SECRET);
		JWT_KEY = ne SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
	}

	private JtUtils() {
	}

	
	public static Claims parseJt(String jt) {
		Claims claims = Jts.parser().setSigningKey(JWT_KEY).parseClaimsJs(jt).getBody();
		return claims;
	}

	
	public static String createJt(Map claims, long ttlMillis) {
		// 生成JWT的时间,即签发时间
		long noMillis = System.currentTimeMillis();

		// 下面就是在为payload添加各种标准声明和私有声明了
		// 这里其实就是ne一个JtBuilder,设置jt的body
		JtBuilder builder = Jts.builder()
				// 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
				.setClaims(claims)
				// 设置jti(JWT ID)是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
				// 可以在未登陆前作为身份标识使用
				.setId(UUID.randomUUID().toString().replace("-", ""))
				// iss(Issuser)签发者,写死
				// .setIssuer("zking")
				// iat: jt的签发时间
				.setIssuedAt(ne Date(noMillis))
				// 代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可放数据{"uid":"zs"}。此处没放
				// .setSubject("{}")
				// 设置签名使用的签名算法和签名使用的秘钥
				.signWith(SIGNATURE_ALGORITHM, JWT_KEY)
				// 设置JWT的过期时间
				.setExpiration(ne Date(noMillis + ttlMillis));

		return builder.pact();
	}

	
	public static String copyJt(String jt, Long ttlMillis) {
		Claims claims = parseJt(jt);

		// 生成JWT的时间,即签发时间
		long noMillis = System.currentTimeMillis();

		// 下面就是在为payload添加各种标准声明和私有声明了
		// 这里其实就是ne一个JtBuilder,设置jt的body
		JtBuilder builder = Jts.builder()
				// 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
				.setClaims(claims)
				// 设置jti(JWT ID)是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
				// 可以在未登陆前作为身份标识使用
				//.setId(UUID.randomUUID().toString().replace("-", ""))
				// iss(Issuser)签发者,写死
				// .setIssuer("zking")
				// iat: jt的签发时间
				.setIssuedAt(ne Date(noMillis))
				// 代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可放数据{"uid":"zs"}。此处没放
				// .setSubject("{}")
				// 设置签名使用的签名算法和签名使用的秘钥
				.signWith(SIGNATURE_ALGORITHM, JWT_KEY)
				// 设置JWT的过期时间
				.setExpiration(ne Date(noMillis + ttlMillis));
		return builder.pact();
	}
}

3.配置JWT验证过滤器


  j2eeVue
  
 index.html
 index.htm
 index.jsp
 default.html
 default.htm
 default.jsp
  
  
  
  
  	corsFilter
  	.zking.vue.util.CorsFilter
  
  
  	corsFilter
  	
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 jt =indo.vm.$store.getters.getJt;
  if(null!=jt)
 config.headers['jt']=jt;
	return config;
}, function(error) {
	return Promise.reject(error);
});

// 响应拦截器
axios.interceptors.response.use(function(response) {
  console.log(response);
  //获取jt令牌存入vuex
  let jt=response.headers['jt']
  console.log(jt);
  indo.vm.$store.mit('setJt',{
 jt:jt
  });
	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")
// 			//   indo.location.href='http://login.'
// 			return
// 		} else {
// 			return response;
// 		}
// 	},
// 	error => {
// 		return Promise.reject(error.response) // 返回接口返回的错误信息
// 	});
export default axios;

Copyright © 2016-2025 www.caominkang.com 曹敏电脑维修网 版权所有 Power by