文章目录
一、认证过程
上文讲到在密码模式下
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_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
遍历所有的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