【JAVA】spring cloud微服务中使用spring security auth2登录认证流程和自定义手机号认证(1)

hey-girl东拼西凑原创文章,若有歧义可留言,若需转载需标明出处。

1.概括:本文主要根据微服务pig<v3.3.3>项目pig码云地址,学习安全登录流程。通过扩展登录需求,自定义其他登录模式,比如常见的手机号登录或者微信登录等。不熟悉项目没有问题,因为文章重点还是介绍spring security auth2登录流程。

2.涉及服务介绍:
gateway:9999(网关服务)
auth:3000(认证服务)
upms:4000(资源服务)
common-security(安全模块)

3.先看看pig项目中登录大致流程图:

9999/auth/aouth/token
api
gateway服务
auth认证服务
UPMS资源服务

接下来详细讲解登录流程,因为只有了解清楚了流程和其原理,才能更加方便我们扩展自己的需求。

  1. 首先使用postman发起一个post请求。
    login登录

    Q1-为什么请求地址是 http://localhost:9999/auth/oauth/token
    A1-/auth是因为在gateway服务中,配置了路由如下图,/oauth/token是auth2默认的端点。更多的endpoint可参考官方文档传送门
    gateway配置文件
    Q2-请求参数讲解
    A2-username用户名;password是AES作为对称加密加密后的密码。加解密网站地址:加解密工具网站。红框中的key后端配置在yml中。也就是传的密码参数是加密后的密文。grant_type的值有五种。Oauth2.0定义了4种模式分别是:authorization_code(授权码模式)、password(密码模式)、client_credentials(客户端模式)、implicit(隐式授权模式)。加上一个refresh_token(令牌刷新)模式。scope作用域范围。
    加解密操作
    Q3- 参数client_id和client_secret
    A3- 我用的是postman.所以直接把参数写上就会自动生成类似这样的字符串Basic cGlnOnBpZw==。其实就是请求头是一个"Basic"加一个空格加"clientId:clientSecret"base64化的一个Authorization字段。这里的pig和数据库表数据统一既可
    在这里插入图片描述

  2. 请求首先会来到GirlRequestGlobalFilter过滤器(自定义的过滤器)。

这个过滤器主要就是2个作用。作用一清洗去除请求头中的的from参数,作用二重写StripPrefix,也就是/auth/oauth/token经过这个过滤器后会变成/oauth/token
GirlRequestGlobalFilter
Q1- 为什么要清洗去除from参数?
A1- 因为在整个项目中,把请求按分为了如下情况
在这里插入图片描述
为了实现内部的调用。采取了@Inner注解方式实现。仔细想想其实这个实现思想很简单。当我们单个服务引入security的时候。服务里面的接口是不是都被保护起来了。你去请求就得验证了。这个时候,如果说我想不登录就访问服务中的A接口。我得怎么做。我是不是的配置ignoring,把不需要鉴权的url给放行下。那说白了。我内部之前的调用就是这么个道理。我得放行这个url。但是这就有个问题,我如何知道你是内部调用了。万一你是外部调用。我不就GG了。所以pig作者给内部请求头加上了from字段。为了防止外部调用的时候冒充。所以在网关进来第一步就把请求头全部清洗一遍。思想就是这么个思想。具体实现代码可以看源代码。
Q2- order为啥是-1000
A2- 过滤器的执行顺序是根据order值决定,值越低,执行的优先顺序越高。如下图。可以看见自定义的过滤器顺序
过滤器的执行顺序

  1. 在执行PasswordDecoderFilter过滤器(也是自定义过滤器)。主要就是判断请求路径是不是登录请求oauth/token。是得话就进行password字段解密操作。这里解密就是对应前面的AES加密。解密后拿到密码明文继续往下走。
    在这里插入图片描述
    注意:这里去除了验证码过滤器,所以不说验证码这part

  2. 经过了gateway之后,服务会被分发到auth认证服务。也就是我们熟悉的Security流程。不熟悉流程的兄弟,请移步Security工作原理这位作者大大解释的还是很清楚的,有图有真相。这里我就流程的几步关键点进行分析。

  3. 请求先来到LogoutFilter过滤器,执行doFilter()判断当前是不是登出请求。如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理,如果登出失败则由 ExceptionTranslationFilter ;如果不是登出路径则直接进入下一个过滤器。
    LogoutFilter

  4. 再会来到BasicAuthenticationFilter过滤器。主要看doFilterInternal()方法。这个过滤器主要检测和处理 http basic 认证。
    BasicAuthenticationFilter
    Q1-代码解析

UsernamePasswordAuthenticationToken authRequest = this.authenticationConverter.convert(request);

A1-convert()作用是去请求头获取Authorization,然后解码(想想我们请求的时候不就是把client_id和秘钥进行base64编码的么,这里其实就是获取它啦)。拿到以后并生成了UsernamePasswordAuthenticationToken。UsernamePasswordAuthenticationToken就很好理解了,所有提交给AuthenticationManager的认证请求都会被封装成一个Token的实现。
convert方法
Q2-代码解析

Authentication authResult = this.authenticationManager.authenticate(authRequest);

A2-封装好的UsernamePasswordAuthenticationToken,也就是上面代码中的变量authResult会交个AuthenticationManager,AuthenticationManager是用户认证的管理类,所有的认证请求都会通过提交一个token(也就是封装好的token,比如常见的UsernamePasswordAuthenticationToken)给AuthenticationManager的authenticate()方法来实现。说白了AuthenticationManager是一个接口。默认实现类ProviderManager。当然ProviderManager也不是干活的人。具体校验动作不是它完成,它只是转发。
我们仔细看看providerManager.authenticate(authRequest)的作用。这里的authenticate(authRequest) 我贴下部分重要代码:
providerManager.authenticate()
上图代码,我把重要的拿出来唠唠

 // 获取当前的authentication对象的类
 // authentication就是传入的UsernamePasswordAuthenticationToken
 Class<? extends Authentication> toTest = authentication.getClass();
 // Provider认证的具体实现类 迭代器
 Iterator var9 = this.getProviders().iterator();
 // 循环,找到支持当前token的Provider
 if (provider.supports(toTest)) {
 }

这里就涉及到Security的另外一个概念AuthenticationProvider,认证的具体实现类,一个provider是一种认证方式的实现。前面讲了AuthenticationManager只是一个代理接口,真正的认证就是由AuthenticationProvider来做的。一个AuthenticationManager可以包含多个Provider,每个provider通过实现一个support方法来表示自己支持那种Token的认证。也就是说上面代码,会根据Provider的具体实现类的supports方法,找到匹配的Provider。然后再调用Provider的authenticate()方法。所以我们接着往下看。

  1. 这里调用的Provider是DaoAuthenticationProvider,它继承了AbstractUserDetailsAuthenticationProvider。他的主要2个方法我们来看看。
    DaoAuthenticationProvider的authenticate

Q1-supports方法方法做了什么?

   public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }

A1-表示自己支持那种Token的认证。
Q2-authenticate方法做了什么?
A2-最主要的就是去查数据库或者缓存中查询和校验用户信息.详细解释看看下面的代码注释

/**这里的getUserDetailsService是ClientDetailsUserDetailsService类,然后调用他的方法loadUserByUsername(),loadUserByUsername里面会去调用ClientDetailsService的实现类loadClientByClientId方法,已有实现一个是基于内存,一个是基于Jdbc。我们可以自定义*/
// username就是client_id
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
// 方法最后就是直接生成UsernamePasswordAuthenticationToken返回
// token的authentication为true
return this.createSuccessAuthentication(principalToReturn, authentication, user);

截止到这里,花里胡哨走完以后。我们进入到TokenEndPoint

8.TokenEndPoint其实说白了就是auth2内置的controller,package:org.springframework.security.oauth2.provider.endpoint。他支持/oauth/token的get和post2种请求方式。氮素,看代码你会发现get方法还是调用的post方法。
先看看TokenEndPoint的post请求代码全貌:
TokenEndPoint
针对这个代码,我们掰开了说。看看它做了啥。
Q1- 代码解析

 if (!(principal instanceof Authentication)) {
            throw new InsufficientAuthenticationException("There is no client authentication. Try adding an appropriate authentication filter.");
        } else {*********
        }

A1-principal 作为参数,这里就是传入的UsernamePasswordAuthenticationToken。(不多解释前面的7小part说的很清楚了哈)。
UsernamePasswordAuthenticationToken继承抽象类 AbstractAuthenticationToken ,AbstractAuthenticationToken 实现了Authentication。所以程序这里会执行else。
Q2- (else分支里面的代码啦)代码解析

String clientId = this.getClientId(principal)

A2- 看看这方法名字,不用多说了吧。造型然后就是去给看principal的authenticated是不是true。完事给把clientId取出来getClientId方法
Q3- 代码继续看

 ClientDetails authenticatedClient = this.getClientDetailsService().loadClientByClientId(clientId);

A3- 根据clientId(这里传入的值为pig)去获取第三方应用的详细信息封装在ClientDetails里
Q4- 代码继续看看

TokenRequest tokenRequest = this.getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);

A4- 这里就是拿请求传递进来的参数和查询出来的第三方应用信息构建TokenRequest。理解下parameters就是我们在请求中写的username,password,scope等,authenticatedClient就是上一步取到的客户端信息咯。看看创建TokenRequest的代码。就是看请求的参数里面有没有client_id,没有这个参数就直接取authenticatedClient的clientId赋值给它。有的话就比较这2个id是不是相等的。完事就创建TokenRequest并返回
我在最开始发请求的时候,并没有带clientId这个参数哈。
createTokenRequest方法
Q5- 代码继续看看

  if (authenticatedClient != null) {
  	// this.validateScope(tokenRequest.getScope(), client.getScope());
      this.oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
   }

A5- 这就是校验Scope,看你传进来的Scope值是不是和客户端详细信息匹配(这里就是和数据库里scope字段值匹配),scope请求的一些授权内容,所请求的授权必须是第三方应用可以发送的授权集合的子集,否则无法通过校验。
Q6- 代码继续看看
在这里插入图片描述
A6- 这里的一串就是校验,依次是
grant_type:必须显式指定按照哪种授权模式获取令牌;
判断传递的授权模式是否是简化模式,如果是简化模式也会抛异常。因为简化模式其实是对授权码模式的一种简化:在用户的第一步的授权行为的时候就直接返回令牌,所以是不会有调用请求令牌服务的机会的;
判断是不是授权码模式,因为授权码模式包含两个步骤,在授权码模式中发出的令牌中拥有的权限不是由发令牌的请求决定的,而是在发令牌之前的授权的请求里就已经决定好了。因此它会对请求过来的scope进行置空操作,然后根据之前发出去的授权码里的权限重新设置你的scope,因此它根本不会使用请求令牌的这个请求中携带的scope参数。
之后判断是不是刷新令牌的请求,应为刷新令牌的请求有自己的scope,所以也会进行重新设置scope的操作。

  1. 经过一系列的校验完成后。来执行令牌生成的操作了
OAuth2AccessToken token = this.getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);

Q1- OAuth2AccessToken类型是什么?
A1- OAuth2Request(实际上是之前的ClientDetails和TokenRequests这两个对象的一个整合)和Authorization(封装的实际上是当前授权用户的一些信息)这两个对象组合起来,会形成一个OAuth2Authorization对象,拿到OAuth2Authorization中所有的信息之后最终会生成一个OAuth2的令牌OAuth2AccessToken。
Q2- this.getTokenGranter()是啥?
A2- TokenGranter是一个接口,定义了一个方法grant().下图就是他的实现类。我们看看CompositeTokenGranter,Composite合成的意思。
在这里插入图片描述
Q3- CompositeTokenGranter做了什么?
CompositeTokenGranter
A3- 首先他有个属性tokenGranters里面存放了五种令牌模式,也就是我们文章前面说的那五种。
在扯扯他的grant()方法,它会对遍历这五种情况。

 TokenGranter granter = (TokenGranter)var3.next();
  grant = granter.grant(grantType, tokenRequest);

拿到这5种的具体实现,去调用他们自己的grant()方法,进行比较。比较的方法在AbstractTokenGranter类中。判断当前携带的授权类型和这个类所支持的授权类型是否匹配,如果不匹配就返回空值,如果匹配的话就进行令牌的生成操作。
AbstractTokenGranter
也可以看出这个抽象类被其他5种模式继承
AbstractTokenGranter的五种子类

Q4- 当上面匹配到合适的模式时候,就得进行生成令牌操作了。也就是执行代码

this.getAccessToken(client, tokenRequest);
// 就是调用下面这方法啦
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
 return this.tokenServices.createAccessToken(this.getOAuth2Authentication(client, tokenRequest));
    }

A4- this.tokenServices就是 AuthorizationServerTokenServices ,createAccessToken()是生成令牌的方法。这个方法需要接收OAuth2Authentication对象作为参数。所以就调用了this.getOAuth2Authentication(client, tokenRequest)

那就先唠唠getOAuth2Authentication这个方法。
在Spring Security OAuth核心类图解析中我们已经知道最终产生的Oauth2Authorization包含两部分信息,一部分是请求中的一些信息,另一部分是根据请求获取的授权用户的信息。而在不同的授权模式下获取授权用户的信息的方式是不同的,比如说pig所使用的密码模式就是使用请求中携带的用户名和密码来获取当前授权用户中的授权信息,而在授权码模式的两个步骤中是根据第一步发出授权码的同时会记录相关用户的信息,之后对第二步进行授权的时候根据第三方应用请求过来的授权码再读取该授权码对应的用户信息。所以getOAuth2Authentication对于不同的授权类型有不同的实现。
那就得看看ResourceOwnerPasswordTokenGranter的getOAuth2Authentication()方法。这个方法取用户名密码封装成UsernamePasswordAuthenticationToken对象传给manager.是不是很熟悉,就和之前2part一样,根据token,去找支持的provider验证。DaoAuthenticationProvider在验证的时候去调用UserDetailsServiceImpl实现loadUserByUsername方法。也会匹配密码啥的。具体可以看看DaoAuthenticationProvider的代码即可
验证完成没有问题以后。就生成OAuth2Authentication返回就好啦。
ResourceOwnerPasswordTokenGranter
那就先唠唠createAccessToken这个方法,上面我们已经得到了OAuth2Authentication对象啦。那不就开始干活啦。

  1. createAccessToken创建令牌啦。
    首先看看源代码createAccessToken方法
    Q1- 逐句看看代码
OAuth2AccessToken existingAccessToken = this.tokenStore.getAccessToken(authentication);

A1- 在tokenStore中获取accessToken,因为同一个用户只要令牌没过期那么再次请求令牌的时候会把之前发送的令牌再次发还。因此一开始就会找当前用户已经存在的令牌。
Q2- 当取到了accessToken,我们就看看if分支做什么了?
accessToken方法
A2- 首先看令牌过期没,如果令牌过期了,那么就会在tokenStore里把accessToken和refreshToken一起删掉,如果令牌没过期,那么就把这个没过期的令牌重新再存一下。因为可能用户是使用另外的方式来访问令牌的,比如说一开始用授权码模式,后来用密码模式,而这两种模式需要存的信息是不一样的,所以这个令牌要重新store一次。之后直接返回这个不过期的令牌。

Q3- 如果accessToken没有取到,为null,就会走下面的逻辑。说明是第一次请求或者是令牌过期了的情况
在这里插入图片描述

A3- 首先看看刷新的令牌有没有,如果刷新的令牌没有的话,那么创建一枚刷新的令牌,根据authentication, refreshToken创建accessToken。而这个创建accessToken的方法也非常简单,OAuth2AccessToken其实就是用UUID创建一个accessToken,然后把过期时间,刷新令牌和scope这些OAuth协议规定的必须要存在的参数设置上,设置完了以后它会判断是否存在tokenEnhancer,如果存在tokenEnhancer它就会按照定制的tokenEnhancer增强生成出来的token。
拿到返回的令牌之后,在122行tokenStore会把拿到的令牌存起来,然后拿refreshToken存起来,最后把生成的令牌返回回去。

至此我们就可以获取到类似这样的结果啦:
在这里插入图片描述
总结:至此登录的流程大抵就这样了,结合流程考虑考虑下,如何扩展就显得思路清晰很多了。自定义手机号登录,就放到下一篇文章详细聊了。
hey-girl东拼西凑原创文章,若有歧义可留言,若需转载需标明出处

  • 9
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值