手把手带你使用JWT实现单点登录
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加密后的header和base64加密后的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";
//有效期默认为 2hour
public 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
*/
@Override
public 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
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
//添加权限拦截器
registry.addInterceptor(new AuthenticationInterceptor()).addPathPatterns("/**").excludePathPatterns("/static/**");
}
}
- 使用
AuthenticationInterceptor拦截器对接口参数进行验证
@Slf4j
public class AuthenticationInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 从http请求头中取出token
final 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;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//方法结束后,移除缓存的token
WebContextUtil.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 {
//本地线程缓存token
private 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是可以进行跨语言支持的,像JAVA、JavaScript、PHP等很多语言都可以使用,而session方案只针对JAVA。
因为有了payload部分,所以JWT可以存储一些其他业务逻辑所必要的非敏感信息。
同时,保护好服务端secret私钥非常重要,因为私钥可以对数据进行验证、解密。如果可以,请使用https协议!
项目源代码地址如下!
五、参考
1、简书 - 什么是 JWT -- JSON WEB TOKEN
2、博客园 - 基于session和token的身份认证方案
手把手带你使用JWT实现单点登录的更多相关文章
- springBoot整合spring security+JWT实现单点登录与权限管理--筑基中期
写在前面 在前一篇文章当中,我们介绍了springBoot整合spring security单体应用版,在这篇文章当中,我将介绍springBoot整合spring secury+JWT实现单点登录与 ...
- 9.springSecurity整合OAuth2结合Jwt实现单点登录
1.总结:昨天主要是继续完善OAuth2配合将问题完成单点登录,昨天也应证了一个认证服务,两个客户端服务在登陆一次后可以访问两个客户端的页面,也算是完成了单点登录 2.具体实现 首先是使用java ...
- 手把手教你学会 基于JWT的单点登录
最近我们组要给负责的一个管理系统 A 集成另外一个系统 B,为了让用户使用更加便捷,避免多个系统重复登录,希望能够达到这样的效果--用户只需登录一次就能够在这两个系统中进行操作.很明显这就是单点登 ...
- Spring Security构建Rest服务-1300-Spring Security OAuth开发APP认证框架之JWT实现单点登录
基于JWT实现SSO 在淘宝( https://www.taobao.com )上点击登录,已经跳到了 https://login.taobao.com,这是又一个服务器.只要在淘宝登录了,就能直接访 ...
- springboot+security+JWT实现单点登录
本次整合实现的目标:1.SSO单点登录2.基于角色和spring security注解的权限控制. 整合过程如下: 1.使用maven构建项目,加入先关依赖,pom.xml如下: <?xml v ...
- spring boot:spring security+oauth2+sso+jwt实现单点登录(spring boot 2.3.3)
一,sso的用途 ? 1,如果有多个应用系统,用户只需要登录一次就可以访问所有相互信任的应用系统. 不需要每次输入用户名称和用户密码, 也不需要创建并记忆多套用户名称和用户密码. 2,系统管理员只需维 ...
- 2.JWT实现单点登录的概念
1.总结: 昨天主要是了解了JWT的作用.构成以及RSA的作用和构成,再就是分布式认证的流程和集中式的差别 JWT的作用:JWT用于生成和校验token JWT的构成:头部.载荷以及签名 头部:设置规 ...
- Spring Boot 集成 JWT 实现单点登录授权
使用步骤如下:1. 添加Gradle依赖: dependencies { implementation 'com.auth0:java-jwt:3.3.0' implementation('org.s ...
- 看图理解JWT如何用于单点登录
单点登录是我比较喜欢的一个技术解决方案,一方面他能够提高产品使用的便利性,另一方面他分离了各个应用都需要的登录服务,对性能以及工作量都有好处.自从上次研究过JWT如何应用于会话管理,加之以前的项目中也 ...
- 手把手教做单点登录(SSO)系列之一:概述与示例
本系列将由浅入深的结合示例.源码以及演示视频,手把手的带大家深入最新的单点登录SSO方案选型与架构开发实战.文末附5个满足不同单点登录场景的gif动画演示(如果看不清请在图片上右键用新窗口打开),本系 ...
随机推荐
- IPD、CMMI、敏捷
华为公司早在2009年正式发文在全公司现在流程IPD.CMMI的基础上,所有产品线的软件开发团队全面推行敏捷开发.除了华为之外,不仅是互联网企业,现在凡是涉及到软件开发的企业对敏捷都不陌生,那么IPD ...
- WordPress函数小结
1.body_class()函数 为了区分不同的页面,可以用WordPress的body_class()函数 可以在head.php中给body添加:<body <?php body_cl ...
- PostgreSQL世界上最先进的开源关系型数据库
PostgreSQL 的 Slogan 是 "世界上最先进的开源关系型数据库". PostgreSQL是一个功能非常强大.源代码开放的对象关系数据库系统(ORDBMS),在灵活的B ...
- 九、.net core(.NET 6)添加通用的Redis功能
.net core 编写通用的Redis功能 在 Package项目里面,添加包:StackExchange.Redis: 在Common工具文件夹下,新建 Wsk.Core.Redis类库项目,并 ...
- QQ/钉钉远程控制和商业远程控制哪个好用
提到远程协助.远程控制,你会想到什么? 国内最古老.最被用户熟知的,大概就是QQ远程控制了.QQ远程控制,方便易用.打开聊天窗口,点窗口上方"-",再移动到如图所示的电脑-箭头图标 ...
- ollama 源代码中值得阅读的部分
阅读 Ollama 源代码以了解其内部工作机制.扩展功能或参与贡献,以下是一些值得重点关注的部分: 1. 核心服务模块: 查找负责启动和管理模型服务的主程序或类,这通常是整个项目的核心逻辑所在.关注如 ...
- SqlServer 死锁查询
use master go create procedure sp_who_lock as begin declare @spid int,@bl int, @intTransactionCountO ...
- Java——成语接龙——递归算法——SpringJDBC+c3p0
这个例子,是我练习SpringJdbc+c3p0时所写.实现了输入一个成语,得到一颗不完整的接龙树.因为我数据库里有3W多个成语,在排除了接龙树成语重复的情况下,依然不能得到全部的结果,我感觉应该 ...
- Sqlserver存储过程中使用try-catch和事务
BEGIN TRY BEGIN TRANSACTION --逻辑代码 COMMIT TRANSACTION --提交事务 END TRY BEGIN CATCH SELECT @Msg = ERROR ...
- Redis内存容量评估
业务侧申请redis服务器资源时,需要事先对redis容量做一个大致评估,之前的容量评估公式基本只是简单的 (key长度 value长度)* key个数,误差较大,后期经常需要进行缩扩容调整,因此提出 ...