文章目录

  • 一、认证过程
  • 二、获取token请求
  • 2.1 校验客户端合法性
  • 2.1.1 校验client_id、client_secret
  • 2.1.1.1 ClientCredentialsTokenEndpointFilter
  • 2.1.1.2 BasicAuthenticationFilter
  • 2.1.2 校验scope
  • 2.1.3 校验grant_type
  • 2.2 校验用户名密码
  • 2.3 产生token
  • 三、受保护的接口请求
  • 3.1 校验token
  • 3.2 校验接口权限
  • 3.2.1 添加注解@PreAuthorize
  • 3.2.2 配置用户权限
  • 四、刷新token


一、认证过程

上文讲到在密码模式下
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相关的过滤器链

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_客户端


springSecurityFilterChain对应的是DelegatingFilterProxyRegistrationBean的代理

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_用户名_02


执行代理类的invokeDelegate,再去调用它装饰的过滤器链delegate

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_客户端_03


delegate里面有两个过滤器链,过滤不同的路由

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

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_java_04


getFilters将判断使用哪个过滤器链,看代码逻辑被/oauth/token命中就不会被/**命中,命中到第一个过滤器链

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_spring_05


两个都在这里了,分别看看

2.1.1.1 ClientCredentialsTokenEndpointFilter

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_spring_06


ClientCredentialsTokenEndpointFilter的doFilter在它的父类里面进入

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

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_用户名_07


如果表单中没有client_id会跳过该过滤器,回到doFilter

Authentication authenticationResult = attemptAuthentication(request, response);

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_用户名_08


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

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_spring_09

在另一篇博文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);

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_客户端_10


将认证结果放入安全上下文,在UsernamePasswordAuthenticationFilter验证用户名密码时,也会先判断安全上下文里是否有认证结果,所以后面对用户名密码的校验就不能使用UsernamePasswordAuthenticationFilter了。实际上Oauth2是在生成token的时候做的用户名密码校验

2.1.1.2 BasicAuthenticationFilter

进入BasicAuthenticationFilter的doFilterInternal

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_自定义_11


进入this.authenticationConverter.convert(request);

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_用户名_12


这里就是从请求头取出Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==做转换,回到doFilterInternal,进入authenticationIsRequired(username)

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_spring_13


从安全上下文取出认证信息,如果当前客户端已经被验证则跳过。再回到doFilterInternal,还是从AuthenticationManager进入做验证,逻辑与ClientCredentialsTokenEndpointFilter一样。

2.1.2 校验scope

获取token的请求经过层层过滤器之后到达端点方法postAccessToken

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_用户名_14


进入validateScope

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_java_15


客户端的scope可以多个,请求用的scope(一个或多个)必须全部包含在原始scope(数据库表里)里面。

2.1.3 校验grant_type

校验完scope回到postAccessToken

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_自定义_16


红框部分简单校验了grant_type,grant_type和token的生成策略是对应的。Oauth2会根据grant_type来找token的生成策略

对应关系如下表

grant_type

生成策略

client_credentials

ClientCredentialsTokenGranter

authorization_code

AuthorizationCodeTokenGranter

implicit

ImplicitTokenGranter

refresh_token

RefreshTokenGranter

password

ResourceOwnerPasswordTokenGranter

自定义

自定义TokenGranter extends AbstractTokenGranter

可以自定义grant_type类型,需要自定义对应的生成策略继承至AbstractTokenGranter。如果提供的grant_type找不到对应的生成策略就会报错。进入getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);如果未自定义TokenGranter ,将进入CompositeTokenGranter的grant

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_spring_17


遍历所有的TokenGranter寻找对应的对象,进入OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_用户名_18


当前TokenGranter的grantType为"password",请求参数的grantType也是"password"就匹配上了。但是还要继续验证咱们在数据库里的客户端信息支不支持。进入validateGrantType(grantType, client);

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_客户端_19


数据库里面配置的usercenter、refresh_token,不支持password就报错了。需要自定义一个grantType为usercenter的token生成策略,可以参照ResourceOwnerPasswordTokenGranter来实现。

2.2 校验用户名密码

修改请求参数的grantType改为password,回到AbstractTokenGranter.grant,进入getAccessToken(client, tokenRequest);

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_spring_20


进入的是匹配到的ResourceOwnerPasswordTokenGranter的getAccessToken,进入getOAuth2Authentication(client, tokenRequest)

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_java_21


在token生成器里面持有一个AuthenticationManager,由他进入做账号密码校验。与前面校验客户端时用的逻辑是一样的,但是这个AuthenticationManager与前面那个是不一样的。在校验客户端信息时是Oauth2自己组装的,而这里这个是我们在SecurityConfig配置类里自定义的spring-cloud-starter-oauth2使用中有讲。仔细对比看两个AuthenticationManager的名称不一样。这里的AuthenticationManager,使用的获取用户名密码的UserDetailsService是我们自定义的,和单独使用Security的校验过程类似。

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

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_客户端_22


进入自定义UserDetailsService,看这个调用链

2.3 产生token

密码校验成功之后产生token,一路返回回到ResourceOwnerPasswordTokenGranter.getAccessToken,进入tokenServices.createAccessToken()在DefaultTokenServices里面

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_自定义_23


将在这里生成token之后返回,在浏览器得到token

请求地址:http://localhost:8080/oauth/token?username=admini&password=123456&grant_type=password&client_id=alarm&client_secret=alarm&scope=server

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_客户端_24


这里得到的token没有用户信息,回到createAccessToken方法里面,往下执行到OAuth2AccessToken accessToken = this.createAccessToken(authentication, refreshToken);跟进去

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_客户端_25


可以自定义增强器对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

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_用户名_26

三、受保护的接口请求

3.1 校验token

前文spring-cloud-starter-oauth2 使用,提供了一个过滤器AuthorizationFilter用来校验token

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_spring_27


没带token,访问失败

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_spring_28


带上刚得到的token之后

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_java_29


请求成功,拿到返回值

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)

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_客户端_30


再次访问,发现接口不允许访问

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_自定义_31


这是因为获取token的时候,授权信息是和token一起存在了redis。再次访问其他接口,不能自动拿到redis的授权信息,会被Security当作匿名用户处理,而接口又配置了权限,所以不能访问

3.2.2 配置用户权限

UserServiceImp添加授权

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_java_32


换个用户名重新获取token,该用户就有了"ROLE_ADMIN"权限,权限信息存在redis,需要在过滤器AuthorizationFilter中手动添加到安全上下文

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_java_33


这样在对方法鉴权时就能拿到这个授权信息,进行授权。代码在AbstractAccessDecisionManager的实现类AffirmativeBased

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_java_34


可以看到已经拿到了用户授权信息,之后就成功得到返回值

四、刷新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

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_自定义_35


grant_type=refresh_token匹配到RefreshTokenGranter,进入 getAccessToken(client, tokenRequest);

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_java_36


执行refreshAccessToken方法重新生成token和refresh_token

Spring security oauth2 生成一个永久有效令牌 spring security oauth2认证流程_java_37