决定放弃 JWT 了!

向量数据库大模型数据库

大家好,我是苏三~

JWT相信大家都有所了解,一种无状态的认证方式,因为JWT本身就能存储一些非敏感的身份信息,这种方式目前也被广泛使用,在苏三之前的项目中使用的就是JWT。

但是JWT虽好,使用过程中还是要依赖缓存,比如退出登录,JWT唯一的失效途径就是等待过期时间失效,因此在退出登录时必须借助外力Redis才能达到效果。这个在之前的文章中也有介绍。

既然都要用Redis,为什么不采用Redis+Spring Security+OAuth2的认证方式呢?这种方式也是企业中经常采用的方案。

今天就介绍一下如何将利用Redis和Spring Security 整合实现分布式统一认证登录的。

  1. 实现的效果

既然是直接使用Redis+Spring Security,身份信息肯定是存储在Redis中且token也不是JWT生成的令牌,如下图:

picture.image

可以看到令牌和刷新令牌以及身份信息都存储在Redis中。其中9d22b664-8540-48d1-98ed-4df1ce90b74f就是生成的令牌,无任何特殊含义,只是随机生成的UUID,相较于JWT短小了很多。

  1. 登录的客户端有哪些?

项目中需要登录的客户端如下:

  1. WEB端
  2. PDA端
  3. PAD端
  4. 患者端
  5. 小程序

今天先来介绍前三种,后面的两种后文介绍。

  1. WEB端

登录页面如下:

picture.image web端登录

三个参数:

  1. 用户名
  2. 密码
  3. 医院ID

请求的报文如下:


        
          
POST /auth/oauth2/token?grant_type=password&scope=server HTTP/1.1  
Host: codeape-gateway:9999  
Authorization: Basic dGVzdDp0ZXN0  
Content-Type: application/x-www-form-urlencoded  
Content-Length: 32  
username=admin&password=YehdBPev&hosId=1659018792143663105  

      

因为是多租户的模式,所以在登录中做了医院的选择,这点也是对代码改造的一部分,下文介绍如何改造。

  1. PDA端

PDA是护士的手持设备,用于采集数据,因此也是需要认证才能上传、查看数据。

PDA端登录只需要护士输入如下两个参数:

  1. 用户名
  2. 密码

为什么呢?不需要选择医院吗?

前面的文章中也有介绍过,PDA这种手持设备只有在平台上录入了才能使用,录入的地方:设备管理->设备列表->新增

picture.image

设备SN号是设备的唯一识别号,在设备取得注册证书后颁发的,所以可以作为唯一识别标志。

这里就是根据根据SN号去唯一关联这台设备,这也就是为什么PDA登录不用选择医院的原因。

PDA在发出登录请求时只需要携带这个SN号,请求报文如下:


        
          
POST /auth/oauth2/token?grant_type=password&scope=server HTTP/1.1  
Host: codeape-gateway:9999  
Authorization: Basic dGVzdDp0ZXN0  
Content-Type: application/x-www-form-urlencoded  
Content-Length: 32  
username=admin&password=YehdBPev&sn=3981293B102  

      

  1. PAD端

平板一般是医生查房时作为移动端使用,住院医生每天都需要去病房查看病人病情,需要结合测量的数据才能了解患者的病情,因此PAD也是需要医生认证登录。

PAD端登录其实有两种方案:

  1. 和WEB端相同,选择医院登录
  2. 通过设备MAC地址绑定登录

项目中采用的第一种方案,需要选择医院,请求报文如下:


        
          
POST /auth/oauth2/token?grant_type=password&scope=server HTTP/1.1  
Host: codeape-gateway:9999  
Authorization: Basic dGVzdDp0ZXN0  
Content-Type: application/x-www-form-urlencoded  
Content-Length: 32  
username=admin&password=YehdBPev&hosId=1659018792143663105  

      
  1. 密码模式登录

上面介绍的WEB端、PDA端、PAD端都是基于密码模式改造的,在介绍认证流程之前需要将登录接口给导入接口工具,这里使用的是Apifox,下载下方密码模式脚本,直接导入Apifox。

导入成功后,你将会得到一个接口,如下图:

picture.image

点击运行,发出请求登录,返回的信息如下图:

picture.image

上述返回信息几个比较重要的属性如下:

  1. access_token

这个则是认证成功生成token,后续请求资源时只需要携带这个token则能通过认证

PS:这里的token似乎很短小,其实并不是JWT生成token,而是UUID。

  1. refresh_token

这个是token过期后的刷新令牌,当token过期后则拿着这个refresh_token即可重新获取新的access_token,无需再次认证登录

  1. user_info

这部分是当前用户登录成功后返回一些个人信息,比如权限、医院ID、所属的科室/病区ID等,详细信息如下图:

  • username:用户名
  • authorities:权限
  • id:主键ID
  • deptId:科室/病区ID
  • hosId:医院ID
  • deptAuths:科室/病区权限
  • roleCodes:角色编码
  • phone:手机号
  • clientId:客户端ID
  • sn:登录的PDA的SN号
  • name:姓名

picture.image

  1. scope

对应的资源的权限

  1. 密码模式登录字段加密

密码模式的登录有两个点比较重要,以WEB端登录报文为例:


        
          
POST /auth/oauth2/token?grant_type=password&scope=server HTTP/1.1  
Host: codeape-gateway:9999  
Authorization: Basic dGVzdDp0ZXN0  
Content-Type: application/x-www-form-urlencoded  
Content-Length: 32  
username=admin&password=YehdBPev&hosId=1659018792143663105  

      

从上面的报文可以看到有两处进行了加密,如下:

  1. Authorization:这里是对client_id:client_secret,这里采用的是base64编码,比如WEB端的原始Authorization为:Basic web:web

  2. password:这里也对密码进行了AES加密处理

  3. 服务端认证的流程


先上一张整体的流程图,如下:

picture.image

按照Apifox的密码模式登录接口发出登录请求后,将会按照上方的流程图逐一处理,流程解析如下:

  1. 网关前置处理

网关的前置处理分为两个部分:

  1. 验证码校验
  2. 密码解密

这两个功能都是使用过滤器处理的,在网关的配置文件中可以看到对认证中心codeape-auth配置了两个过滤器,如下:

picture.image

关于网关的过滤器不理解的请看知识星球中《精尽Spring Cloud Alibaba》专栏网关的部分。

1. 验证码校验

在前面文章中介绍了项目中是对WEB端、PDA端、PAD端将验证码关闭的,但是对于院外患者端,比如患者APP端还是需要验证码的。

验证码对应的代码在com.code.ape.codeape.gateway.filter.ValidateCodeGatewayFilter中,里面的逻辑在前文介绍过,这里就不再详细说了,有一行代码需要注意一下,代码如下:


        
          
//解析请求头中的ClientId,和配置文件configProperties中的比较,忽略不需要校验clientId  
boolean isIgnoreClient =configProperties.getIgnoreClients().contains(WebUtils.getClientId(request));  

      

为什么需要注意呢?

上文说过,客户端ID和客户端秘钥是放在Authorization中经过base64编码后发送给服务端,因此后端取client_id是不是也要经过解码,WebUtils.getClientId(request)这个方法就是对Authorization解码获取client_id,代码如下:


        
          
 /**  
  * 从request 获取CLIENT\_ID  
  * com.code.ape.codeape.common.core.util.WebUtils#getClientId  
  */  
 @SneakyThrows  
 public String getClientId(ServerHttpRequest request) {  
  String header = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);  
  return splitClient(header)[0];  
 }  
  
 /**  
  * 对请求头中的Authorization拆分且解码  
  * com.code.ape.codeape.common.core.util.WebUtils#splitClient  
  */  
 @NotNull  
 private static String[] splitClient(String header) {  
  if (header == null || !header.startsWith(BASIC_)) {  
   throw new CheckedException("请求头中client信息为空");  
  }  
  byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8);  
  byte[] decoded;  
  try {  
            //解码  
   decoded = Base64.decode(base64Token);  
  }  
  catch (IllegalArgumentException e) {  
   throw new CheckedException("Failed to decode basic authentication token");  
  }  
  
  String token = new String(decoded, StandardCharsets.UTF_8);  
  
  int delim = token.indexOf(":");  
  
  if (delim == -1) {  
   throw new CheckedException("Invalid basic authentication token");  
  }  
  return new String[] { token.substring(0, delim), token.substring(delim + 1) };  
 }  
  

      

2. 密码解密

密码解密对应的过滤器:com.code.ape.codeape.gateway.filter.PasswordDecoderFilter,逻辑很简单:

  1. 校验是否是登录请求
  2. 校验授权类型,如果是刷新令牌则直接放行
  3. 解密

代码很简单,注释很清楚,这里就不再详细贴出来了。

注意:客户端和服务端的加密因子需要保持一致才能正确加解密。

  1. OAuth2ClientAuthenticationFilter

这个过滤器的作用是用于 OAuth2 的客户端身份验证,主要用于处理客户端使用客户端凭证(client credentials)访问受保护资源的情况。

整体的逻辑如下图:

picture.image

代码①

这个很好理解,只有登录请求/oauth2/token才会校验客户端信息,其他的请求直接放行

代码②

这行代码是将请求头中客户端信息提取出来转换为Authentication客户端认证对象,这里用到了认证转换器AuthenticationConverter,在该过滤器构造时默认传入了四个,如下图:

picture.image

this.authenticationConverter.convert(request)该方法调用的是DelegatingAuthenticationConverter#convert方法,内部是循环调用上述的四个才转换器,如下:

picture.image

上述四个认证转换器比较重要的是其中两个:

  1. ClientSecretBasicAuthenticationConverter

这个是处理将客户端信息存放在请求头中转换器,在内部对请求头中的客户端信息进行base64解码,具体的代码逻辑如下:

picture.image

这个转换器正好是项目中的请求方式相匹配,因此走的则是这个逻辑。

  1. ClientSecretPostAuthenticationConverter

这个转换器是处理POST请求,且客户端信息通过Body传输的,里面逻辑也是非常简单,直接从请求参数中获取client_idclient_secret,具体的代码就不带大家看了,有兴趣可以看一下。

代码③

这里就是执行真正的校验逻辑了,内部调用的RegisteredClientRepository#findByClientId()方法校验。

对应的则是整体的流程图的第②部分,这里调用的则是自定义的CodeapeRemoteRegisteredClientRepository#findByClientId方法,内部逻辑非常简单:查询Redis缓存,存在缓存直接取,不存在则查数据库codeape/sys_oauth_client_details(通过feign接口远程调用服务查询)。

代码如下图:

picture.image

代码④

这部分是客户端认证成功的处理逻辑,是将客户端认证的信息存放到SecurityContext上下文中,方便后面流程获取,代码OAuth2ClientAuthenticationFilter#onAuthenticationSuccess如下:

picture.image

代码⑤

处理客户端认证失败的结果,这里最终执行的是自定义的失败处理器CodeapeAuthenticationFailureEventHandler#onAuthenticationFailure(),这个下文会介绍。

  1. RegisteredClientRepository

这个是客户端的持久层查询的类,在上文已经介绍过

  1. OAuth2TokenEndpointFilter

OAuth2TokenEndpointFilter 这个过滤器的作用是用于处理 OAuth2 认证和授权请求的。它会拦截所有请求,并根据请求的 URI 判断是否是授权请求(/oauth2/token)。

如果是授权请求,则它会根据请求的参数构造一个 OAuth2AuthenticationToken 对象,并将其交给 AuthenticationManager 进行身份认证。如果认证成功,则根据请求中携带的授权类型(grant_type)决定使用哪个 OAuth2 授权提供者来生成授权令牌(access_token),并将生成的授权令牌返回给请求方。

如果认证失败,则返回相应的错误信息。该过滤器通常用于实现 OAuth2 认证和授权功能的后端服务。

这个过滤器才是真正处理登录请求逻辑

整体的逻辑如下:

picture.image

  1. AuthenticationConverter

这个在第4步中的第②个步骤,会根据请求中的参数和授权类型组装成对应的授权认证对象。它的几个重要的实现类如下:

picture.image

先来看一下自定义的抽象类:OAuth2ResourceOwnerBaseAuthenticationConverter,三个抽象方法如下:

  • boolean support(String grantType):判断是否支持指定的授权类型
  • void checkParams(HttpServletRequest request):校验请求参数,比如密码模式下的username、password不能为空,手机验证码登录则手机号不能为空都是在这校验
  • T buildToken():这个是构建认证登录对象的方法

实现的convert()方法代码如下:


        
          
@Override  
 public Authentication convert(HttpServletRequest request) {  
  
  // grant\_type (REQUIRED) ① 校验授权类型,调用抽象方法support  
  String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);  
  if (!support(grantType)) {  
   return null;  
  }  
  
  //② 获取请求参数,比如密码模式:username、password、hosId,scope...  
  MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);  
  // scope (OPTIONAL)  ③ 提取出scope  
  String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);  
  if (StringUtils.hasText(scope) && parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {  
   OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE,  
     OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);  
  }  
  
  Set<String> requestedScopes = null;  
  if (StringUtils.hasText(scope)) {  
   requestedScopes = new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));  
  }  
  
  // ④ 校验个性化参数  
  checkParams(request);  
  
  // ⑤ 获取当前已经认证的客户端信息,这个是在OAuth2ClientAuthenticationFilter认证成功客户端认证对象  
  Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();  
  if (clientPrincipal == null) {  
   OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ErrorCodes.INVALID_CLIENT,  
     OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);  
  }  
  
  // ⑥ 扩展信息  
  Map<String, Object> additionalParameters = parameters.entrySet()  
   .stream()  
   .filter(e -> !e.getKey().equals(OAuth2ParameterNames.GRANT_TYPE)  
     && !e.getKey().equals(OAuth2ParameterNames.SCOPE))  
   .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));  
  
  // ⑦ 创建token 调用抽象方法buildToken()  
  return buildToken(clientPrincipal, requestedScopes, additionalParameters);  
  
 }  
  

      

注释非常清晰了,这里不再详细解释了。

其中密码模式认证登录的实现类是:OAuth2ResourceOwnerPasswordAuthenticationConverter,里面的逻辑非常简单,这里不介绍了。

  1. AuthenticationToken

          
`AuthenticationToken`是登录认证对象,在第4步中的第②步组装,[码猿慢病云管理系统](https://mp.weixin.qq.com/s?__biz=MzU3MDAzNDg1MA==∣=2247526866&idx=1&sn=3820b44ff80c46749efa1a2c0b1f8aa7&chksm=fcf7b61fcb803f090688a542cfb766f5dc06385b5e6e04c2309b4af7b07c07580fb19ffc9d0a&scene=178&cur_album_id=2989600933141807115#rd)中对其进行了扩展,有如下三个类:  

      
  1. OAuth2ResourceOwnerBaseAuthenticationToken:抽象类
  2. OAuth2ResourceOwnerPasswordAuthenticationToken:密码模式的登录认证对象
  3. OAuth2ResourceOwnerSmsAuthenticationToken:短信验证码登录认证对象

后续如有其他授权模式,直接继承OAuth2ResourceOwnerBaseAuthenticationToken扩展

  1. AuthenticationProvider

AuthenticationProvider是Spring Security提供的一种机制,用于接收和验证用户名和密码等认证信息,并返回一个已认证的Authentication对象。其作用是封装了整个认证过程,包括认证用户的来源、密码的加密和解密、对用户账户状态的判断等。

AuthenticationProvider在第4步中的第③步中被调用,用于认证;项目中自定义了三个实现类,如下:

  1. OAuth2ResourceOwnerBaseAuthenticationProvider

抽象类,封装了具体的执行逻辑,有三个抽象方法供子类实现,如下:


        
          
 /**  
  * 构建登录认证对象  
  * @param reqParameters  
  * @return  
  */  
 public abstract UsernamePasswordAuthenticationToken buildToken(Map<String, Object> reqParameters);  
  
 /**  
  * 当前provider是否支持此令牌类型  
  * @param authentication  
  * @return  
  */  
 @Override  
 public abstract boolean supports(Class<?> authentication);  
  
 /**  
  * 当前的请求客户端是否支持此模式  
  * @param registeredClient  
  */  
 public abstract void checkClient(RegisteredClient registeredClient);  

      

具体的执行逻辑都在OAuth2ResourceOwnerBaseAuthenticationProvider#authenticate()方法中,关键逻辑如下:


        
          
//① 构建登录认证对象,交由子类实现  
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = buildToken(reqParameters);  
  
//② 交由Spring Security 认证  
Authentication usernamePasswordAuthentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);  
  
// ----- Access token ----- ③ 构建Access token  
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();  
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);  
  
// ----- Refresh token -----  ④ 认证成功后,构建刷新令牌  
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();  
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);  
  
//⑤ 存储令牌  
this.authorizationService.save(authorization);  

      

代码①:构建认证登录对象,提供了一个buildToken抽象方法交由子类实现

剩余代码下文介绍

  1. OAuth2ResourceOwnerPasswordAuthenticationProvider

密码模式的AuthenticationProvider,继承抽象类OAuth2ResourceOwnerBaseAuthenticationProvider实现三个抽象方法,逻辑很简单。

  1. OAuth2ResourceOwnerSmsAuthenticationProvider

短信验证码登录模式的AuthenticationProvider,继承抽象类OAuth2ResourceOwnerBaseAuthenticationProvider实现三个抽象方法。

  1. DaoAuthenticationProvider

DaoAuthenticationProvider这里就进入真正的认证逻辑了,从名字就可以看出涉及到数据库的操作了。内部的逻辑很简单,就是通过UserDetailService调用查询用户信息封装成UserDetails

在第7步中的第②步骤中则会进入:


        
          
//② 交由Spring Security 认证  
Authentication usernamePasswordAuthentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);  

      

项目中自定义了一个CodeapeDaoAuthenticationProvider,执行的逻辑将会在这个类中,先看下其中重载的两个重要的方法:


        
          
//方法一:feign远程调用根据username查询用户信息,组装成UserDetails  
UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication);  
  
//方法二:校验用户信息、密码  
void additionalAuthenticationChecks(UserDetails userDetails,  
   UsernamePasswordAuthenticationToken authentication);  

      

1. retrieveUser 查询用户信息

retrieveUser这个方法逻辑很简单,则是调用UserDetailService查询用户信息,逻辑如下:

picture.image

代码①

从Request中获取相关参数:

  1. clientId:客户端ID,由于是base64编码传输,因此需要调用的convert方法解码
  2. hosId:医院ID,WEB、PAD登录所需参数
  3. sn:设备的唯一识别SN号,用于PDA登录

代码②

从IOC容器中获取UserDetailSevice,项目中目前实现类有三个:

  1. CodeapeAppUserDetailsServiceImpl:处理APP端的手机号登录
  2. CodeapePDAUserDetailsServiceImpl:处理PDA端登录
  3. CodeapeUserDetailsServiceImpl:处理PAD端和WEB端登录

代码③

调用UserDetailService中的loadUserByUsernameAndOther方法获取UserDetails

2. additionalAuthenticationChecks 密码校验

这个方法核心逻辑则是校验密码,项目中的密码校验是通过PasswordEncoder加密。

picture.image

3. 用户状态校验

核心逻辑在:AbstractUserDetailsAuthenticationProvider.DefaultPreAuthenticationChecks#check方法中,代码如下:

picture.image

  1. UserDetailService

在第8步中说到查询用户信息是通过UserDetailService查询,项目中目前内置三个实现类:

  1. CodeapeAppUserDetailsServiceImpl:处理APP端的手机号登录
  2. CodeapePDAUserDetailsServiceImpl:处理PDA端登录
  3. CodeapeUserDetailsServiceImpl:处理PAD端和WEB端登录

这里都是通过feign调用解耦,当然你也可以在auth模块嵌入数据库,从数据库查询

这里调用的方法是loadUserByUsernameAndOther,比如CodeapeUserDetailsServiceImpl实现如下:

picture.image

最终的组装UserDetails通过getUserDetails方法,如下:

picture.image

需要注意的是:项目中的用户信息是封装在CodeapeUser中,方便后续扩展,其中的属性如下:

picture.image

可以看到这里和登录返回的信息中user_info是对应的:

picture.image

  1. 生成OAuth2AccessToken

在第7步中的第③步中生成access_token,自定义的实现类为:CustomeOAuth2AccessTokenGenerator

picture.image

  1. OAuth2AuthorizationService 令牌持久化

在第7步中的第⑤步骤中执行了令牌的持久化,Spring Security 默认支持两种持久化方式:

  1. InMemoryOAuth2AuthorizationService:持久化在内存中
  2. JdbcOAuth2AuthorizationService:持久化在数据库中

项目中扩展了Redis中持久化,自定义的实现类:CodeapeRedisOAuth2AuthorizationService

picture.image

持久化成功后将会在Redis中看到对应的信息:

picture.image

  1. AuthenticationSuccessHandler 登录成功处理

在第4步中的第④步骤中认证成功,则调用AuthenticationSuccessHandler 处理登录成功的逻辑,将认证信息输出返回给客户端。

项目中自定义类:CodeapeAuthenticationSuccessEventHandlerpicture.image

总结

本节内容详细介绍了项目中完整的认证登录生成token的流程,相信你对整体的流程有了清晰的了解。

最后欢迎 加入苏三的星球 ,你将获得:苏三AI项目、 商城微服务实战、秒杀系统实战 、 商城系统实战、秒杀系统实战、代码生成工具、系统设计、性能优化、技术选型、底层原理、Spring源码解读、工作经验分享、痛点问题、面试八股文等多个优质专栏。

还有1V1答疑、修改简历、职业规划、送书活动、技术交流。

picture.image

目前星球已经更新了5200+篇优质内容,还在持续爆肝中.....

星球已经被官方推荐了3次,收到了小伙伴们的一致好评。戳我加入学习,已有1600+小伙伴加入学习。

0
0
0
0
关于作者
关于作者

文章

0

获赞

0

收藏

0

相关资源
KubeZoo: 轻量级 Kubernetes 多租户方案探索与实践
伴随云原生技术的发展,多个租户共享 Kubernetes 集群资源的业务需求应运而生,社区现有方案各有侧重,但是在海量小租户的场景下仍然存在改进空间。本次分享对现有多租户方案进行了总结和对比,然后提出一种基于协议转换的轻量级 Kubernetes 网关服务:KubeZoo,该方案能够显著降低多租户控制面带来的资源和运维成本,同时提供安全可靠的租户隔离性。
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论