当前位置: 首页 > news >正文

JWT——跨域认证解决方案

文章目录

  • JWT学习笔记
    • 一、什么是JWT
    • 二、JWT能做什么
      • 2.1、授权
      • 2.2、信息交换
    • 三、为什么是JWT
      • 3.1、传统的Session认证
      • 3.2、基于JWT的认证
      • 3.3、JWT的优势
    • 四、JWT的结构是什么
      • 4.1、令牌构成
      • 4.2、header 标头
      • 4.3、Payload 负载
      • 4.4、Signature 签名
      • 4.5、常见的异常信息
    • 五、JWT的第一个程序
      • 5.1、JWT整合SpringBoot
        • 5.1.1、封装工具类
        • 5.1.2、创建数据库
        • 5.1.3、引入依赖
        • 5.1.4、创建接口
    • 六、优化程序


JWT学习笔记

一、什么是JWT

官网:JSON Web Tokens - jwt.io
请添加图片描述

官方定义:

在这里插入图片描述

其大致意思就是:JWT是一个以JSON格式传输信息,且传输过程中是安全的,因为他有数字签名,所以可以做验证(其中的加密或者签名算法有RSA/CDSA)

自我理解就是:通过以JSON的形式把数据封装成一个令牌,用于保证数据交互过程中的安全性(在传输过程中可以进行加密和签名)。

二、JWT能做什么

2.1、授权

这是使用JWT常见方案,一旦用户登录,每个后续请求将包含JWT,从而允许用户访问令牌允许的路由,服务和资源。单点登录是当今广泛使用JWT的一项功能。因为它开销小并且可以在不同的域中使用。

2.2、信息交换

JSON Web Token是在各方面之间安全的传输信息的方法。因为可以对JWT进行签名(例如公钥/私钥),所以可以确保发件人是他们所说的人,此外,由于签名时使用的标头和有效负载计算的,因此还可以验证内容是否遇到篡改。

三、为什么是JWT

3.1、传统的Session认证

在这里插入图片描述

传统的Session认证流程:不同的客户端去访问服务器,由于发送到是http请求(无状态的),所以服务器端不知道谁发的,为了解决引入了Session(服务器端的对象,存储访问服务器的用户信息),但是Session知识解决了http请求是无状态的,也就是说Session只是起到一个标识作用,让服务器知道有人请求我了,但是具体是谁还不清楚。所以这是又出现了一个cookie的东西。当客户端第一次访问服务器的时候,服务器会给发送的客户端返回一个sessionId,而在客户端是以cookie的形式存在。这样就达到了确认的作用。

但是缺点也显而易见,当访问量上来后,服务器因为存储过多的session而造成运行效率降低,服务质量下降的不良影响。

3.2、基于JWT的认证

在这里插入图片描述

基于JWT的认证是保存在客户端的,所以不会造成服务端的资源浪费。

认证流程

  • 首先,前端将通过Web表单将自己的用户名和密码发送给后端的接口,这一过程一般是一个Http POST请求。建议的方式是通过SSL加密传输(Https),从而避免敏感信息被嗅探。
  • 后端核对用户名和密码成功后,将用户的id等其他信息作为JWT的palyload(负载),将其与头部分别进行Base64的编码拼接后签名,形成一个JWT(Token)。生成的JWT就是一个形态xxx.xxx.xxx的字符串。
  • 后端将JWT字串作为登陆成功的返回结果返回给前端。前端将返回的结果保存在LocalStorage或者SessionStorage上,退出登录是前端删除JWT即可。
  • 前端每次请求是将JWT保存在Http Header中的Authorization位(解决SXX和XSRF问题)
  • 后端检查是否存在,如果存在验证JWT有效性;Token是否过期;Token是否是自己的Token。
  • 验证通过后后端使用JWT中包含的用户信息机型逻辑处理,返回相应的结果。

3.3、JWT的优势

  • 简介(通过URL,POST参数或者在HTTP Header中发送,数据量小,传输速度快)
  • 自包含(负载中包含了所需要的用户信息)
  • 跨语言的(基于JSON加密形式保存在客户端)
  • 不需要在服务器端保存会话,特别适用于微服务分布式

四、JWT的结构是什么

4.1、令牌构成

token —> String —> x.y.z

我们看JWT是由三部分组成的,大致分为x,y,z

  • 第一部分(x):表示header,标头
  • 第二部分(y):表示payload,负载
  • 第三部分(z):表示signature,签名

所以token的结构式标头.负载.签名

4.2、header 标头

标头通常由两部分组成,令牌的类型(JWT)和所使用的签名算法(RSA、SHA),他会使用Base64编码组成JWT的第一部分

注意:Base64是一种编码,也就是说,他是可以被翻译回原来的样子的,他并不是一种加密过程

{
    "alg":"HS256",
    "typ":"JWT"
}

4.3、Payload 负载

令牌的第二部分是有效负载,其中包含声明,声明是有关实体(通常使用户或者其他实体的声明),同样的,也是用Base64编码组成JWT的第二部分

{
    "name":"zhangsan",
    "sub":"123456789",
    "admin":"true"
}

但是官方建议不要放用户私密信息放到Payload,因为Base64可以被翻译。🔥🔥🔥

4.4、Signature 签名

前面两部分都是使用Base64进行编码的,及前端可以被解开token里的信息,Signature需要使用编码后的header和payload以及我们提供的一个密钥,然后使用header所声明的签名算法进行签名。作用是保证JWT没被篡改

【未被Base64编码的】

在这里插入图片描述

【被Base64编码的】

在这里插入图片描述

4.5、常见的异常信息

  • SignatureVerifyException:签名不一致异常
  • TokenExpiredException:令牌过期异常
  • AlgorithmMismatchaException:算法不匹配异常
  • InvalidClaimException:失效的payload异常

五、JWT的第一个程序

由于JWT可以支持和各种程序集成,所以我先以一个Java程序

引入依赖

<!-- 引入JWT -->
<dependency>
	<groupId>com.auth0</groupId>
	<artifactId>java-jwt</artifactId>
	<version>3.4.0</version>
</dependency>
public class TestGenerateJwt {

	/**
	* 描述:Token的获取
	* @Title: test1
	* @author weiyongpeng
	* @date  2022年10月5日 上午8:26:35
	 */
	@Test
	public void test1() {
		HashMap<String, Object> headerMap = new HashMap<>();
		Calendar d =Calendar.getInstance();
		d.add(Calendar.MINUTE, 20);
		// 设置头信息
		String token = JWT.create()
			.withHeader(headerMap) 					// header
			.withClaim("username", "zhangsan")   	// payload 默认是只能放一个 放第二个会把第一个给覆盖掉 放多个可以使用Array
			.withClaim("userId", 21)				// payload payload里面放的是什么类型,那边获取asxxx就比要与者的类型对应
			.withExpiresAt(d.getTime())				// 过期时间
			.sign(Algorithm.HMAC256("AWEDSRF"));	// signature
			
		System.out.println(token);
	}
	
	/**
	* 描述:令牌的验证
	* @Title: test2
	* @author weiyongpeng
	* @date  2022年10月5日 上午8:27:07
	 */
	@Test
	public void test2() {
		JWTVerifier require = JWT.require(Algorithm.HMAC256("AWEDSRF")).build();
		DecodedJWT verify = require.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NjQ5MzExNTksInVzZXJJZCI6MjEsInVzZXJuYW1lIjoiemhhbmdzYW4ifQ.c7NTuFiAZw45aCYYwUSo7zac3oNStkNVoasBSeFp1Wk");
		System.out.println(verify.getClaim("username").asString());
		System.out.println(verify.getClaim("userId").asInt());
		System.out.println(verify.getClaims().get("username").asString());
		System.out.println(verify.getClaims().get("userId").asInt());
		System.out.println("过期时间:"+verify.getExpiresAt());
		
	}
}

在这里插入图片描述

5.1、JWT整合SpringBoot

5.1.1、封装工具类

经过上述的使用,我们不难发现,当我们一旦生成JWT,就需要验证JWT是否是我们的,再加上如果设置了过期时间,还需要考虑是否需要续签。所以经常要频繁的操作JWT。

为了方便使用,我们需要封装JWT的工具类用于生成和验证等方法

【JWTUtils】

public class JWTUtils {
	
	private static final String SECRTEKEY = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9";
 
	/**
	* 描述:生成Token
	* @Title: getToken
	* @param map
	* @return
	* @author weiyongpeng
	* @date  2022年10月5日 上午8:58:32
	 */
	public static String getToken(Map<String,String> map) {
		Calendar d =Calendar.getInstance();
		d.add(Calendar.DATE, 7); // 默认是7天过期
		Builder builder = JWT.create();
		
		// 设置头信息
		map.forEach((key,value)->{
			builder.withClaim(key, value);
		});
		String token = builder
			.withExpiresAt(d.getTime())				// 过期时间
			.sign(Algorithm.HMAC256(SECRTEKEY));	// signature
			
		System.out.println(token);
		return token;
	}
	
	/**
	* 描述:验证并返回DecoderJWT
	* @Title: verifyToken
	* @param token
	* @return
	* @author weiyongpeng
	* @date  2022年10月5日 上午9:04:53
	 */
	public static DecodedJWT verifyToken(String token) {
		DecodedJWT verify = null;
		try {
			verify = JWT.require(Algorithm.HMAC256(SECRTEKEY))
						.build()
						.verify(token);
		} catch (Exception e) {
			// TODO: handle exception
			return verify;
		}
		return verify;
	}
	
}

5.1.2、创建数据库

请添加图片描述

5.1.3、引入依赖

<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		
		<!-- 引入JWT -->
		<dependency>
			<groupId>com.auth0</groupId>
			<artifactId>java-jwt</artifactId>
			<version>3.4.0</version>
		</dependency>
		
		<!-- 引入Mybatis -->
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>2.1.3</version>
		</dependency>
		
		<!-- 引入druid -->
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid</artifactId>
			<version>1.1.19</version>
		</dependency>
		
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
		</dependency>
	</dependencies>

5.1.4、创建接口

在这里省略包的创建以及配置的相关设置,只展示关键的接口演示

在控制层创建两个接口。一个用于登录生成token,一个用于校验token。在实际开发过程中,一般会把校验放在登陆的接口里。

@GetMapping("/user/login")
	public Map<String,Object> login(User user) {
		System.out.println("用户名:"+user.getName());
		System.out.println("密码:"+user.getPassword());
		
		HashMap<String, Object> map = null;
		try {
			User u = userService.login(user);
			if (u!=null) {
				map = new HashMap<>();
				// JWT生成令牌
				HashMap<String, String> params = new HashMap<>();
				params.put("userId", u.getId().toString());
				params.put("username", u.getName());
				String token = JWTUtils.getToken(params);
				
				// 设置验证用户成功的map
				map.put("state", true);
				map.put("msg", "登陆成功");
				map.put("token", token);
			}
		} catch (Exception e) {
			// TODO: handle exception
			map.put("state", false);
			map.put("msg", e.getMessage());
		}
		
		return map;
	}
	
	@PostMapping("/user/verify")
	public Map<String,Object> verifyToken(String token){
		
		Map<String, Object> map = new HashMap<>();
		try {
			DecodedJWT verifyToken = JWTUtils.verifyToken(token);
			// 处理业务逻辑
			map.put("state", true);
			map.put("msg", "处理成功");
			return map;
		} catch (SignatureVerificationException e) {
			// TODO: handle exception
			e.printStackTrace();
			map.put("msg", "无效签名");
		}catch (TokenExpiredException e) {
			// TODO: handle exception
			e.printStackTrace();
			map.put("msg", "token过期");
		}catch (AlgorithmMismatchException e) {
			// TODO: handle exception
			e.printStackTrace();
			map.put("msg", "算法不一致");
		}catch (InvalidClaimException e) {
			// TODO: handle exception
			e.printStackTrace();
			map.put("msg", "payload失效");
		}catch (Exception e) {
			// TODO: handle exception
			e.printStackTrace();
			map.put("msg", "登陆失败");
		}
		map.put("state", false);
		return map;
	}

请添加图片描述

请添加图片描述

六、优化程序

上述的程序,会有很高的冗余,如果在真正的开发中。因为,每次登录访问资源都要去校验Token。所以,如果在单体架构中,我们可以使用拦截器去操作,如果实在分布式我们可以采用网关拦截操作。

public class JWTInterceptor implements HandlerInterceptor{

	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		// TODO Auto-generated method stub
		return HandlerInterceptor.super.preHandle(request, response, handler);
	}
	
}

这里由于官方建议我们把token放到请求头里面,所以前端在发送登录请求的时候,还是可以把token放到header里面

public class JWTInterceptor implements HandlerInterceptor {

	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		// TODO Auto-generated method stub
		Map<String, Object> map = new HashMap<>();
		// 获取请求头中的token
		String token = request.getHeader("token");
		try {
			DecodedJWT verifyToken = JWTUtils.verifyToken(token);
			// 处理业务逻辑
			return true;
		} catch (SignatureVerificationException e) {
			// TODO: handle exception
			e.printStackTrace();
			map.put("msg", "无效签名");
		} catch (TokenExpiredException e) {
			// TODO: handle exception
			e.printStackTrace();
			map.put("msg", "token过期");
		} catch (AlgorithmMismatchException e) {
			// TODO: handle exception
			e.printStackTrace();
			map.put("msg", "算法不一致");
		} catch (InvalidClaimException e) {
			// TODO: handle exception
			e.printStackTrace();
			map.put("msg", "payload失效");
		} catch (Exception e) {
			// TODO: handle exception
			e.printStackTrace();
			map.put("msg", "登陆失败");
		}
		map.put("state", false);
		String json = new ObjectMapper().writeValueAsString(map);
		response.setContentType("application/json;charset=UTF-8");
		response.getWriter().println(json);
		return false;
	}

}

然后再声明一个配置类,用于配置拦截器。

@Configuration
public class InterceptorConfig implements WebMvcConfigurer{

	@Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new JWTInterceptor())
                .addPathPatterns("/**")
             	.excludePathPatterns("/jwt/user/login");
    }

}

最后修改控制器里面的校验接口,即在真正的业务操作中业务逻辑。这里只是简单的演示:

@PostMapping("/test/verify")
	public Map<String,Object> verifyToken(){
		Map<String, Object> map = new HashMap<>();
		// 处理业务逻辑
		map.put("state", true);
		map.put("msg", "处理成功");
		return map;
	}

相关文章:

  • python计算微积分
  • 吃灰树莓派应用之HomeAssistant安装与Tuya插件应用
  • 基于Springboot+Vue实现智能停车场管理系统
  • 【模型训练】YOLOv7行人摔倒检测
  • 基于JAVA校园外卖系统Web端计算机毕业设计源码+系统+数据库+lw文档+部署
  • 4_卷积神经网络
  • [C++基础]-入门知识
  • Java8-19新特性(附官网传送门)
  • 加解密相关
  • 【TypeScript教程】# 6:使用webpack打包ts代码
  • Pytorch环境配置(anaconda安装+独显+CUDA+cuDNN)
  • Incorrect string value: ‘\xE6\x9D\x91\xE4\xB8\x8A...‘ for column ‘name‘错误解决
  • git --- git撤销commit(未push)
  • python爬虫的防盗链
  • 【UI自动化】实现C站三连功能
  • canvas 五子棋游戏
  • Fabric架构演变之路
  • Java多线程(4):使用线程池执行定时任务
  • Java深入 - 深入理解Java集合
  • nginx(二):进阶配置介绍--rewrite用法,压缩,https虚拟主机等
  • supervisor 永不挂掉的进程 安装以及使用
  • 测试如何在敏捷团队中工作?
  • 今年的LC3大会没了?
  • 聊聊sentinel的DegradeSlot
  • 面试总结JavaScript篇
  • 它承受着该等级不该有的简单, leetcode 564 寻找最近的回文数
  • 学习Vue.js的五个小例子
  • “十年磨一剑”--有赞的HBase平台实践和应用之路 ...
  • shell使用lftp连接ftp和sftp,并可以指定私钥
  • ​插件化DPI在商用WIFI中的价值
  • $(function(){})与(function($){....})(jQuery)的区别
  • $jQuery 重写Alert样式方法
  • (C语言)编写程序将一个4×4的数组进行顺时针旋转90度后输出。
  • (蓝桥杯每日一题)平方末尾及补充(常用的字符串函数功能)
  • 、写入Shellcode到注册表上线
  • .[hudsonL@cock.li].mkp勒索加密数据库完美恢复---惜分飞
  • .NET : 在VS2008中计算代码度量值
  • .NET Core 控制台程序读 appsettings.json 、注依赖、配日志、设 IOptions
  • .net on S60 ---- Net60 1.1发布 支持VS2008以及新的特性
  • .NET 动态调用WebService + WSE + UsernameToken
  • .NET 使用 XPath 来读写 XML 文件
  • .NET 中使用 TaskCompletionSource 作为线程同步互斥或异步操作的事件
  • .NET6 开发一个检查某些状态持续多长时间的类
  • @Builder用法
  • @Not - Empty-Null-Blank
  • [2016.7.test1] T2 偷天换日 [codevs 1163 访问艺术馆(类似)]
  • [BZOJ1053][HAOI2007]反素数ant
  • [C/C++]_[初级]_[关于编译时出现有符号-无符号不匹配的警告-sizeof使用注意事项]
  • [Codeforces] probabilities (R1600) Part.1
  • [C语言]——C语言常见概念(1)
  • [EFI]Dell Latitude-7400电脑 Hackintosh 黑苹果efi引导文件
  • [FROM COM张]如何解决Nios II SBTE中出现的undefined reference to `xxx'警告
  • [HTML]Web前端开发技术18(HTML5、CSS3、JavaScript )HTML5 基础与CSS3 应用——喵喵画网页
  • [JavaEE系列] wait(等待) 和 notify(唤醒)
  • [java基础揉碎]关系运算符(比较运算符)逻辑运算符赋值运算符三元运算符运算符的优先级