spring-cloud-starter-oauth2 密码模式认证过程

一、认证过程

上文讲到在密码模式下
OAuth2主要用于校验客户端合法性、产生token、校验token
Sercurity主要用于用户名密码校验、接口权限控制
OAuth2与Sercurity整合之后,校验顺序:
获取token请求/oauth/token:校验客户端合法性——校验用户名密码——产生token
受保护的接口请求/**:校验token——校验接口权限
下面就来看下,每个步骤在源码中是如何实现的

二、获取token请求

2.1 校验客户端合法性

客户端合法性校验,第一步校验client_id、client_secret,就是客户端id和密码;第二步校验grant_type;第三步校验scope

2.1.1 校验client_id、client_secret

校验client_id、client_secret使用的是过滤器,介绍两个过滤

  • ClientCredentialsTokenEndpointFilter提取表单中的client_id、client_secret做校验
  • BasicAuthenticationFilter提取请求头中的 Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==

后端接收到请求先要经过层层过滤器,执行过滤器链ApplicationFilterChain,从它又进入到security相关的过滤器链
在这里插入图片描述
springSecurityFilterChain对应的是DelegatingFilterProxyRegistrationBean的代理在这里插入图片描述
执行代理类的invokeDelegate,再去调用它装饰的过滤器链delegate
在这里插入图片描述
delegate里面有两个过滤器链,过滤不同的路由

  • /oauth/token 过滤获取token的端点
  • /** 过滤所有的路由

在这里插入图片描述
getFilters将判断使用哪个过滤器链,看代码逻辑被/oauth/token命中就不会被/**命中,命中到第一个过滤器链
在这里插入图片描述
两个都在这里了,分别看看

2.1.1.1 ClientCredentialsTokenEndpointFilter

在这里插入图片描述
ClientCredentialsTokenEndpointFilter的doFilter在它的父类里面进入

!requiresAuthentication(request, response)–>this.requiresAuthenticationRequestMatcher.matches(request)

在这里插入图片描述
如果表单中没有client_id会跳过该过滤器,回到doFilter

Authentication authenticationResult = attemptAuthentication(request, response);

在这里插入图片描述
可以看到这里从请求参数里拿到client_id、client_secret,然后判断安全上下文里的authentication是否被认证过(可以自定义过滤器做认证,然后将结果放入安全上下文,这里就不会再做认证了)。往下看代码
在这里插入图片描述

在另一篇博文Spring Security认证流程里讲到过:

UsernamePasswordAuthenticationFilter对于用户名密码的认证是从AuthenticationManager接口发起的,具体的验证逻辑与UsernamePasswordAuthenticationFilter一样,可以看下另一篇博文。

重点在红框中的三个对象,ClientCredentialsTokenEndpointFilter(Oauth2提供)在验证客户端时使用的是Oauth2自己的ClientDetailsUserDetailsService,它实现了UserDetailsService(这个接口属于Spring Security)。由它的loadUserByUsername去调ClientDetailsService的loadClientByClientId,ClientDetailsService可以自定义,也可以使用JdbcClientDetailsService(Oauth2提供)。

UsernamePasswordAuthenticationFilter(Security提供)在验证用户名密码时使用的是自定义UserDetailsService。只需实现UserDetailsService接口,Spring Security就会调用实现类的loadUserByUsername来获取用户信息。

返回到doFilter进入successfulAuthentication(request, response, chain, authenticationResult);
在这里插入图片描述
将认证结果放入安全上下文,在UsernamePasswordAuthenticationFilter验证用户名密码时,也会先判断安全上下文里是否有认证结果,所以后面对用户名密码的校验就不能使用UsernamePasswordAuthenticationFilter了。实际上Oauth2是在生成token的时候做的用户名密码校验

2.1.1.2 BasicAuthenticationFilter

进入BasicAuthenticationFilter的doFilterInternal
在这里插入图片描述
进入this.authenticationConverter.convert(request);
在这里插入图片描述
这里就是从请求头取出Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==做转换,回到doFilterInternal,进入authenticationIsRequired(username)
在这里插入图片描述
从安全上下文取出认证信息,如果当前客户端已经被验证则跳过。再回到doFilterInternal,还是从AuthenticationManager进入做验证,逻辑与ClientCredentialsTokenEndpointFilter一样。

2.1.2 校验scope

获取token的请求经过层层过滤器之后到达端点方法postAccessToken
在这里插入图片描述
进入validateScope
在这里插入图片描述
客户端的scope可以多个,请求用的scope(一个或多个)必须全部包含在原始scope(数据库表里)里面。

2.1.3 校验grant_type

校验完scope回到postAccessToken
在这里插入图片描述
红框部分简单校验了grant_type,grant_type和token的生成策略是对应的。Oauth2会根据grant_type来找token的生成策略
对应关系如下表

grant_type生成策略
client_credentialsClientCredentialsTokenGranter
authorization_codeAuthorizationCodeTokenGranter
implicitImplicitTokenGranter
refresh_tokenRefreshTokenGranter
passwordResourceOwnerPasswordTokenGranter
自定义自定义TokenGranter extends AbstractTokenGranter

可以自定义grant_type类型,需要自定义对应的生成策略继承至AbstractTokenGranter。如果提供的grant_type找不到对应的生成策略就会报错。进入getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);如果未自定义TokenGranter ,将进入CompositeTokenGranter的grant
在这里插入图片描述
遍历所有的TokenGranter寻找对应的对象,进入OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
在这里插入图片描述
当前TokenGranter的grantType为"password",请求参数的grantType也是"password"就匹配上了。但是还要继续验证咱们在数据库里的客户端信息支不支持。进入validateGrantType(grantType, client);
在这里插入图片描述
数据库里面配置的usercenter、refresh_token,不支持password就报错了。需要自定义一个grantType为usercenter的token生成策略,可以参照ResourceOwnerPasswordTokenGranter来实现。

2.2 校验用户名密码

修改请求参数的grantType改为password,回到AbstractTokenGranter.grant,进入getAccessToken(client, tokenRequest);
在这里插入图片描述
进入的是匹配到的ResourceOwnerPasswordTokenGranter的getAccessToken,进入getOAuth2Authentication(client, tokenRequest)
在这里插入图片描述
在token生成器里面持有一个AuthenticationManager,由他进入做账号密码校验。与前面校验客户端时用的逻辑是一样的,但是这个AuthenticationManager与前面那个是不一样的。在校验客户端信息时是Oauth2自己组装的,而这里这个是我们在SecurityConfig配置类里自定义的spring-cloud-starter-oauth2使用中有讲。仔细对比看两个AuthenticationManager的名称不一样。这里的AuthenticationManager,使用的获取用户名密码的UserDetailsService是我们自定义的,和单独使用Security的校验过程类似。

也就是说,Oauth2两次复用了Security提供的AuthenticationManager进行密码校验,一次是客户端校验,一次是用户名密码校验。

在这里插入图片描述
进入自定义UserDetailsService,看这个调用链

2.3 产生token

密码校验成功之后产生token,一路返回回到ResourceOwnerPasswordTokenGranter.getAccessToken,进入tokenServices.createAccessToken()在DefaultTokenServices里面
在这里插入图片描述
将在这里生成token之后返回,在浏览器得到token
请求地址:http://localhost:8080/oauth/token?username=admini&password=123456&grant_type=password&client_id=alarm&client_secret=alarm&scope=server
在这里插入图片描述
这里得到的token没有用户信息,回到createAccessToken方法里面,往下执行到OAuth2AccessToken accessToken = this.createAccessToken(authentication, refreshToken);跟进去
在这里插入图片描述
可以自定义增强器对token的内容进行填充,在Oauth2Config里面有配置TokenEnhancer

    @Bean
    public TokenEnhancer tokenEnhancer() {
        return (accessToken, authentication) -> {
            if ("client_credentials".equals(authentication.getOAuth2Request().getGrantType())) {
                return accessToken;
            } else {
                UserDetails userDetails = (UserDetails)authentication.getUserAuthentication().getPrincipal();
                 Map<String, Object> map =new HashMap<>();
                 map.put("username",userDetails.getUsername());
                ((DefaultOAuth2AccessToken)accessToken).setAdditionalInformation(map);
                return accessToken;
            }
        };
    }

再次访问,token信息里面多了username
在这里插入图片描述

三、受保护的接口请求

3.1 校验token

前文spring-cloud-starter-oauth2 使用,提供了一个过滤器AuthorizationFilter用来校验token
在这里插入图片描述
没带token,访问失败
在这里插入图片描述
带上刚得到的token之后
在这里插入图片描述
请求成功,拿到返回值

3.2 校验接口权限

3.2.1 添加注解@PreAuthorize

HelloController修改之后

@RestController
@RequestMapping("/mms")
public class HelloController {

    @GetMapping("/hello")
    public String hello(){
        return "hello!";
    }

    @GetMapping("/blog")
    @PreAuthorize("hasAnyRole('ROLE_ADMIN')")
    public Blog blog(){
        return new Blog("user1","dept1");
    }
}

开启方法权限,添加注解@EnableGlobalMethodSecurity(prePostEnabled=true)
在这里插入图片描述
再次访问,发现接口不允许访问
在这里插入图片描述
这是因为获取token的时候,授权信息是和token一起存在了redis。再次访问其他接口,不能自动拿到redis的授权信息,会被Security当作匿名用户处理,而接口又配置了权限,所以不能访问

3.2.2 配置用户权限

UserServiceImp添加授权

换个用户名重新获取token,该用户就有了"ROLE_ADMIN"权限,权限信息存在redis,需要在过滤器AuthorizationFilter中手动添加到安全上下文
在这里插入图片描述
这样在对方法鉴权时就能拿到这个授权信息,进行授权。代码在AbstractAccessDecisionManager的实现类AffirmativeBased
在这里插入图片描述
可以看到已经拿到了用户授权信息,之后就成功得到返回值

四、刷新token

最后来看下刷新token的过程
请求url:http://localhost:8080/oauth/token?username=fox1&password=123456&grant_type=refresh_token&client_id=alarm&client_secret=alarm&refresh_token=2e5d08a2-d592-47b1-a705-24b2e67ca53c
在这里插入图片描述
grant_type=refresh_token匹配到RefreshTokenGranter,进入 getAccessToken(client, tokenRequest);
在这里插入图片描述
执行refreshAccessToken方法重新生成token和refresh_token
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值