手把手带你使用JWT实现单点登录

news/2024/10/6 3:03:22/文章来源:https://www.cnblogs.com/dxflqm/p/18281013

JWT(英文全名:JSON Web Token)是目前最流行的跨域身份验证解决方案之一,今天我们一起来揭开它神秘的面纱!

一、故事起源

说起 JWT,我们先来谈一谈基于传统session认证的方案以及瓶颈。

传统session交互流程,如下图:

当浏览器向服务器发送登录请求时,验证通过之后,会将用户信息存入seesion中,然后服务器会生成一个sessionId放入cookie中,随后返回给浏览器。

当浏览器再次发送请求时,会在请求头部的cookie中放入sessionId,将请求数据一并发送给服务器。

服务器就可以再次从seesion获取用户信息,整个流程完毕!

通常在服务端会设置seesion的时长,例如 30 分钟没有活动,会将已经存放的用户信息从seesion中移除。

session.setMaxInactiveInterval(30 * 60);//30分钟没活动,自动移除

同时,在服务端也可以通过seesion来判断当前用户是否已经登录,如果为空表示没有登录,直接跳转到登录页面;如果不为空,可以从session中获取用户信息即可进行后续操作。

在单体应用中,这样的交互方式,是没啥问题的。

但是,假如应用服务器的请求量变得很大,而单台服务器能支撑的请求量是有限的,这个时候就容易出现请求变慢或者OOM

解决的办法,要么给单台服务器增加配置,要么增加新的服务器,通过负载均衡来满足业务的需求。

如果是给单台服务器增加配置,请求量继续变大,依然无法支撑业务处理。

显而易见,增加新的服务器,可以实现无限的水平扩展。

但是增加新的服务器之后,不同的服务器之间的sessionId是不一样的,可能在A服务器上已经登录成功了,能从服务器的session中获取用户信息,但是在B服务器上却查不到session信息,此时肯定无比的尴尬,只好退出来继续登录,结果A服务器中的session因为超时失效,登录之后又被强制退出来要求重新登录,想想都挺尴尬~~

面对这种情况,几位大佬于是合起来商议,想出了一个token方案。

将各个应用程序与内存数据库redis相连,对登录成功的用户信息进行一定的算法加密,生成的ID被称为token,将token还有用户的信息存入redis;等用户再次发起请求的时候,将token还有请求数据一并发送给服务器,服务端验证token是否存在redis中,如果存在,表示验证通过,如果不存在,告诉浏览器跳转到登录页面,流程结束。

token方案保证了服务的无状态,所有的信息都是存在分布式缓存中。基于分布式存储,这样可以水平扩展来支持高并发。

当然,现在springboot还提供了session共享方案,类似token方案将session存入到redis中,在集群环境下实现一次登录之后,每个服务器都可以获取到用户信息。

二、JWT是什么

上文中,我们谈到的session还有token的方案,在集群环境下,他们都是靠第三方缓存数据库redis来实现数据的共享。

那有没有一种方案,不用缓存数据库redis来实现用户信息的共享,以达到一次登录,处处可见的效果呢?

答案肯定是有的,就是我们今天要介绍的JWT

JWT全称JSON Web Token,实现过程简单的说就是用户登录成功之后,将用户的信息进行加密,然后生成一个token返回给客户端,与传统的session交互没太大区别。

交互流程如下:

唯一的不同点就是token存放了用户的基本信息,更直观一点就是将原本放入redis中的用户数据,放入到token中去了!

这样一来,客户端、服务端都可以从token中获取用户的基本信息,既然客户端可以获取,肯定是不能存放敏感信息的,因为浏览器可以直接从token获取用户信息。

JWT具体长什么样呢?

JWT是由三段信息构成的,将这三段信息文本用.链接一起就构成了JWT字符串。就像这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
  • 第一部分:我们称它为头部(header),用于存放token类型和加密协议,一般都是固定的;
  • 第二部分:我们称其为载荷(payload),用户数据就存放在里面;
  • 第三部分:是签证(signature),主要用于服务端的验证;
1、header

JWT的头部承载两部分信息:

  • 声明类型,这里是JWT;
  • 声明加密的算法,通常直接使用 HMAC SHA256;

完整的头部就像下面这样的JSON:

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

使用base64加密,构成了第一部分。

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
2、playload

载荷就是存放有效信息的地方,这些有效信息包含三个部分:

  • 标准中注册的声明;
  • 公共的声明;
  • 私有的声明;

其中,标准中注册的声明 (建议但不强制使用)包括如下几个部分

  • iss: jwt签发者;
  • sub: jwt所面向的用户;
  • aud: 接收jwt的一方;
  • exp: jwt的过期时间,这个过期时间必须要大于签发时间;
  • nbf: 定义在什么时间之前,该jwt都是不可用的;
  • iat: jwt的签发时间;
  • jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击;

公共的声明部分
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息,但不建议添加敏感信息,因为该部分在客户端可解密。

私有的声明部分
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

定义一个payload:

{"sub": "1234567890","name": "John Doe","admin": true
}

然后将其进行base64加密,得到Jwt的第二部分:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
3、signature

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

  • header (base64后的);
  • payload (base64后的);
  • secret (密钥);

这个部分需要base64加密后的headerbase64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

//javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);var signature = HMACSHA256(encodedString, '密钥');

加密之后,得到signature签名信息。

TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

将这三部分用.连接成一个完整的字符串,就构成了最终的jwt:

//jwt最终格式
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

这个只是通过javascript实现的一个演示,JWT的签发和密钥的保存都是在服务端来完成。

secret用来进行jwt的签发和jwt的验证,所以,在任何场景都不应该流露出去

三、实战

介绍了这么多,怎么实现呢?废话不多说,下面我们直接开撸!

  • 创建一个springboot项目,添加JWT依赖库
<!-- jwt支持 -->
<dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>3.4.0</version>
</dependency>
  • 然后,创建一个用户信息类,将会通过加密存放在token
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class UserToken implements Serializable {private static final long serialVersionUID = 1L;/*** 用户ID*/private String userId;/*** 用户登录账户*/private String userNo;/*** 用户中文名*/private String userName;
}
  • 接着,创建一个JwtTokenUtil工具类,用于创建token、验证token
public class JwtTokenUtil {//定义token返回头部public static final String AUTH_HEADER_KEY = "Authorization";//token前缀public static final String TOKEN_PREFIX = "Bearer ";//签名密钥public static final String KEY = "q3t6w9z$C&F)J@NcQfTjWnZr4u7x";//有效期默认为 2hourpublic static final Long EXPIRATION_TIME = 1000L*60*60*2;/*** 创建TOKEN* @param content* @return*/public static String createToken(String content){return TOKEN_PREFIX + JWT.create().withSubject(content).withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME)).sign(Algorithm.HMAC512(KEY));}/*** 验证token* @param token*/public static String verifyToken(String token) throws Exception {try {return JWT.require(Algorithm.HMAC512(KEY)).build().verify(token.replace(TOKEN_PREFIX, "")).getSubject();} catch (TokenExpiredException e){throw new Exception("token已失效,请重新登录",e);} catch (JWTVerificationException e) {throw new Exception("token验证失败!",e);}}
}
  • 编写配置类,允许跨域,并且创建一个权限拦截器
@Slf4j
@Configuration
public class GlobalWebMvcConfig implements WebMvcConfigurer {/*** 重写父类提供的跨域请求处理的接口* @param registry*/@Overridepublic void addCorsMappings(CorsRegistry registry) {// 添加映射路径registry.addMapping("/**")// 放行哪些原始域.allowedOrigins("*")// 是否发送Cookie信息.allowCredentials(true)// 放行哪些原始域(请求方式).allowedMethods("GET", "POST", "DELETE", "PUT", "OPTIONS", "HEAD")// 放行哪些原始域(头部信息).allowedHeaders("*")// 暴露哪些头部信息(因为跨域访问默认不能获取全部头部信息).exposedHeaders("Server","Content-Length", "Authorization", "Access-Token", "Access-Control-Allow-Origin","Access-Control-Allow-Credentials");}/*** 添加拦截器* @param registry*/@Overridepublic void addInterceptors(InterceptorRegistry registry) {//添加权限拦截器registry.addInterceptor(new AuthenticationInterceptor()).addPathPatterns("/**").excludePathPatterns("/static/**");}
}
  • 使用AuthenticationInterceptor拦截器对接口参数进行验证
@Slf4j
public class AuthenticationInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 从http请求头中取出tokenfinal String token = request.getHeader(JwtTokenUtil.AUTH_HEADER_KEY);//如果不是映射到方法,直接通过if(!(handler instanceof HandlerMethod)){return true;}//如果是方法探测,直接通过if (HttpMethod.OPTIONS.equals(request.getMethod())) {response.setStatus(HttpServletResponse.SC_OK);return true;}//如果方法有JwtIgnore注解,直接通过HandlerMethod handlerMethod = (HandlerMethod) handler;Method method=handlerMethod.getMethod();if (method.isAnnotationPresent(JwtIgnore.class)) {JwtIgnore jwtIgnore = method.getAnnotation(JwtIgnore.class);if(jwtIgnore.value()){return true;}}LocalAssert.isStringEmpty(token, "token为空,鉴权失败!");//验证,并获取token内部信息String userToken = JwtTokenUtil.verifyToken(token);//将token放入本地缓存WebContextUtil.setUserToken(userToken);return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {//方法结束后,移除缓存的tokenWebContextUtil.removeUserToken();}
}
  • 最后,在controller层用户登录之后,创建一个token,存放在头部即可
/*** 登录* @param userDto* @return*/
@JwtIgnore
@RequestMapping(value = "/login", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"})
public UserVo login(@RequestBody UserDto userDto, HttpServletResponse response){//...参数合法性验证//从数据库获取用户信息User dbUser = userService.selectByUserNo(userDto.getUserNo);//....用户、密码验证//创建token,并将token放在响应头UserToken userToken = new UserToken();BeanUtils.copyProperties(dbUser,userToken);String token = JwtTokenUtil.createToken(JSONObject.toJSONString(userToken));response.setHeader(JwtTokenUtil.AUTH_HEADER_KEY, token);//定义返回结果UserVo result = new UserVo();BeanUtils.copyProperties(dbUser,result);return result;
}

到这里基本就完成了!

其中AuthenticationInterceptor中用到的JwtIgnore是一个注解,用于不需要验证token的方法上,例如验证码的获取等等。

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface JwtIgnore {boolean value() default true;
}

WebContextUtil是一个线程缓存工具类,其他接口通过这个方法即可从token中获取用户信息。

public class WebContextUtil {//本地线程缓存tokenprivate static ThreadLocal<String> local = new ThreadLocal<>();/*** 设置token信息* @param content*/public static void setUserToken(String content){removeUserToken();local.set(content);}/*** 获取token信息* @return*/public static UserToken getUserToken(){if(local.get() != null){UserToken userToken = JSONObject.parseObject(local.get() , UserToken.class);return userToken;}return null;}/*** 移除token信息* @return*/public static void removeUserToken(){if(local.get() != null){local.remove();}}
}

最后,启动项目,我们来用postman测试一下,看看头部返回结果。

我们把返回的信息提取处理,使用浏览器的base64对前两个部分进行解密。

  • 第一部分,也就是header,结果如下:

  • 第二部分,也就是playload,结果如下:

可以很清晰的看到,头部、载荷的信息都可以通过base64解密出来。

所以,一定别在token中存放敏感信息

当我们需要请求其它服务接口时,只需要在请求头部headers中加入Authorization参数即可。

当权限拦截器验证通过之后,在接口方法中只需要通过WebContextUtil工具类就可以获取用户信息。

//获取用户token信息
UserToken userToken = WebContextUtil.getUserToken();

四、总结

JWT相比session方案,因为json的通用性,所以JWT是可以进行跨语言支持的,像JAVAJavaScriptPHP等很多语言都可以使用,而session方案只针对JAVA

因为有了payload部分,所以JWT可以存储一些其他业务逻辑所必要的非敏感信息。

同时,保护好服务端secret私钥非常重要,因为私钥可以对数据进行验证、解密。如果可以,请使用https协议!

项目源代码地址如下!

https://gitee.com/pzblogs/spring-boot-example-demo

五、参考

1、简书 - 什么是 JWT -- JSON WEB TOKEN

2、博客园 - 基于session和token的身份认证方案

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/736687.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何通过文档外发管控系统,实现重要数据的高效流转?

文档外发管控是指企业或组织对其内部文档的外发流程进行管理和控制的一系列措施,目的是为了保护企业的知识产权、商业秘密和客户信息等敏感数据。通常需要注意以下几个方面: 1.权限管理:确保只有授权人员可以访问和外发文档; 2.审批流程:外发文档前需要经过一定的审批流程…

人工智能产业综合标准化体系

人工智能产业综合标准化体系 https://mp.weixin.qq.com/s/dRjOjag2fKww1Trn8tJR7A参考文献链接 https://mp.weixin.qq.com/s/dRjOjag2fKww1Trn8tJR7A人工智能芯片与自动驾驶

免费ACME证书申请接口:可以简化SSL/TLS证书申请

ACME证书申请接口:简化SSL/TLS证书获取的利器在当今互联网安全日益受到重视的背景下,HTTPS已成为网站安全通信的标配。而HTTPS的核心在于SSL/TLS证书,它能够确保数据传输的完整性和安全性。然而,传统的手动创建和安装证书流程复杂且耗时,不利于网站的快速部署和安全更新。…

VuePress 的更多配置

除了插件,VuePress 自身也有很多有用的配置,这里简单说明下。08.更多配置 现在,读者应该对 VuePress、主题和插件等有了基本的认识,除了插件,VuePress 自身也有很多有用的配置,这里简单说明下。 ‍ ‍ VuePress 的介绍 在介绍了 VuePress 的基本使用、主题和插件的概念之…

如何解决大文件传输存在的痛点,实现高效流转?

在当代的数字化时代,数据资产在各行各业中扮演着举足轻重的角色,而数据的流通与交换则是其价值得以实现的关键。企业在进行大文件传输时,都面临着诸多挑战,比如网络延迟、大小受限、安全风险等。因此,如何高效安全的进行大文件传输,成为企业需要迫切解决的难题。在选择大…

VMware ESXi 8.0U3 macOS Unlocker OEM BIOS ConnectX-3 网卡定制版 (集成驱动版)

VMware ESXi 8.0U3 macOS Unlocker & OEM BIOS ConnectX-3 网卡定制版 (集成驱动版)VMware ESXi 8.0U3 macOS Unlocker & OEM BIOS ConnectX-3 网卡定制版 (集成驱动版) VMware ESXi 8.0U3 macOS Unlocker & OEM BIOS 集成网卡驱动和 NVMe 驱动 (集成驱动版) 发布 …

7.1 ~ 7.7

本部7.1 搬了校区。 发现我们虽然是在西扩上课,但宿舍还是老校区的 \(12\) 人宿舍,输。 不过教学楼好玩的东西还是挺多的。 本来我们是和化奥组一个班,但因为物奥集训 && 我们班人数过多 (\(69\)) 把我们和生奥放在了一起; 然后我们名义上的班主任还是张华,各种老…

发布构件到Maven中央仓库(2024-06更新版 - 解决2024年6月后发布报 status code 401 错误)

之前很久没发布Maven中央仓库了,2024年6月发布突然报 status code 401 错误,一顿查询后发现仓库发布改到中央门户网站了 https://central.sonatype.com/。报错如下:

本地资源(local resource)与项目资源文件(project resource)的区别

导入“本地资源”的图片,会在Form文件下面的Form.resx文件里面,不可以在多个Form界面引用,不可以在里面修改图片的名称; 导入“项目资源文件”的图片,会保存在Properties文件夹下面的Resources.resx文件夹里面,可以在多个form界面引用,可以在里面修改图片的名称。注意:…

Qt/C++编写地图应用/离线地图下载/路径规划/轨迹回放/海量点/坐标转换

一、前言说明 这个地图组件写了很多年了,最初设计的比较粗糙,最开始只是为了满足项目需要,并没有考虑太多拓展性,比如最初都是按照百度地图写死在代码中,经过这几年大量的现场实际应用,以及大量的用户提出的改进意见,逐渐萌生了彻底重新编写对应地图相关的代码,比如基类…

go语言的结构体标签tag 介绍

https://juejin.cn/post/7005465902804123679博客中所涉及到的图片都有版权,请谨慎使用

COOIS/COHV增强

1、文档说明 本文档介绍COOIS/COHV事务码中常用的选择屏幕增强和ALV增强 2、选择屏幕增强 COOIS生产订单抬头选择屏幕添加筛选条件,并将自定义数据添加到报表选择屏幕新增筛选字段函数模块中,将选择屏幕筛选条件抛到内存。此处可以优化,将不属于自定义删选条件去掉,只抛自定…