当你掌握某个技术,每次再重新去研究的时候都会有不一样的感悟。随的时间的推移每个人的能力都有不一样的提高,这时候再去回头看就会有更清晰的感觉。本文主要就是对spring security oauth2的源码进行解读,希望能帮助到大家同时自已也做一下学习记录。
官方文档说明
spring security oauth2 中的 endpoint
/oauth/authorize(授权端,授权码模式使用)
/oauth/token(令牌端,获取 token)
/oauth/check_token(资源服务器用来校验token)
/oauth/confirm_access(用户发送确认授权)
/oauth/error(认证失败)
/oauth/token_key(如果使用JWT,可以获的公钥用于 token 的验签)
OAuth2Authentication
OAuth2Authentication是Authentication的子类,存储用户信息和客户端信息,但多了2个属性
private final OAuth2Request storedRequest;
private final Authentication userAuthentication;
这样OAuth2Authentication可以存储2个Authentication,一个给client(必要),一个给user(只是有些授权方式需要)。除此之外同样有principle,credentials,authorities,details,authenticated等属性。
OAuth2Request 用于存储request中的Authentication信息(grantType,responseType,resouceId,clientId,scope等),这里就引出了OAuth2 中的三大request。
BaseRequest
BaseRequest是抽象类,有3个属性:clienId、scope和requestParameters。
3个子类都在OAuth2包中,这些request都会存在于OAuth2的验证流程中,用于传递clientId,scope,requestParameters等属性,与HttpServletRequest有本质区别!
AuthorizationRequest
向授权服务器AuthorizationEndPoint(/oauth/authorize)请求授权,AuthorizationRequest作为载体存储state,redirect_uri等参数,生命周期很短且不能长时间存储信息,可用OAuth2Request代替存储信息。TokenRequest
向授权服务器TokenEndPoint(/oauth/token)发送请求获得access_token时,tokenRequest作为载体存储请求中grantType等参数。常和tokenGranter.grant(grantType,tokenRequest)结合起来使用。
TokenRequest携带了新属性grantType,和createOAuth2Request(用于持久化)OAuth2Request
用来存储TokenRequest或者AuthorizationRequest的信息,只有构造方法和getter方法,不提供setter方法。它作为OAuth2Authentication的一个属性(StoredRequest),存储request中的authentication信息(grantType,approved,responseTypes)。
OAuth2RequestFactory
工厂类生成OAuth2Request、TokenRequest、AuthenticationRequest。
OAuth2AccessToken
OAuth2AccessToken是一个接口,提供安全令牌token的基本信息,不包含用户信息,仅包含一些静态属性(scope,tokenType,expires_in等)和getter方法,如String getScope,OAuth2RefreshToken getRefreshToken,String getTokenType,String getValue()等。TokenGranter.grant()返回的值即OAuth2AccessToken。
TokenGranter
一般在用户请求TokenEndPoints中的路径/oauth/token时,根据请求参数中的grantType,username,password,client_id,client_secret等,调用TokenGranter给用户分发OAuth2AccessToken。
根据grantType(password,authorization-code)和TokenRequest(requestParameters,clientId,grantType)授予OAuth2AccessToken令牌。
实现AbstractTokenGranter的类有5种。如果用password的方式进行验证,那么TokenGranter类型是ResourceOwnerPasswordTokenGranter,该类中重写了getOAuth2Authentication方法,里面调用了authenticationManager.manage()方法。
用户可自行定义granter类继承AbstractTokenGranter,重写getOAuth2Authentication()方法,并将该granter类添加至CompositeTokenGranter中。
TokenStore
一般在TokenGranter执行grant方法完毕后,将OAuth2AccessToken和OAuth2Authentication存储起来,方便以后根据其中一个查询另外一个(如根据access_token查询获得OAuth2Authentication)
JwtTokenStore不存储token和authentication,直接根据token解析获得authentication
TokenExtractor (OAuth2AuthentiactionProcessingFilter)
用户携带token访问资源,过滤器进行到OAuth2AuthentiactionProcessingFilter时,从HttpServletRequest中获取access_token(可以从header或者params中获取),拼接成PreAuthenticatedAuthenticationToken(Authentication子类)
BearerTokenExtractor是TokenExtractor的实现类,实现了从request中获取Authentication的方法。
1.header中 Authentication:Bearer xxxxxxxx--xxx
2.request parameters中 access_token=xxxx-xxxx-xxxx
ResourceServerTokenServices
ResourceServerTokenServices有两重要继承类,DefaultTokenServices和RemoteTokenServices。
用户携access_token访问资源服务器时,资源服务器会将该字符串进行解析,获得OAuth2Authentication和OAuth2AccessToken。
1.loadAuthentication根据字符串accessToken获得OAuth2Authentication;
2.readAccessToken根据字符串accessToken获得OAuth2AccessToken。
DefaultTokenServices
实现了两个接口AuthorizationServerTokenServices和ResourceServerTokenServices。常在granter().grant()方法中调用tokenServices.createAccessToken()方法获得oauth2accesstoken。
RemoteTokenServices
当授权服务和资源服务不在一个应用程序的时候,资源服务可以把传递来的access_token递交给授权服务的/oauth/check_token进行验证,而资源服务自己无需去连接数据库验证access_token,这时就用到了RemoteTokenServices。
loadAuthentication方法,设置head表头Authorization 存储clientId和clientSecret信息,请求参数包含access_token字符串,向授权系统发送/oauth/check_token请求,详见CheckTokenEndpoint类,返回验证结果map(包含clientId,grantType,scope,username等信息),拼接成OAuth2Authentication。
授权系统默认
String checkTokenAccess = "denyAll()";
String tokenKeyAccess = "denyAll()";
需要配置
.tokenKeyAccess("isAuthenticated()").checkTokenAccess("isAuthenticated()"),否则请求访问/oauth/check_token会提示没权限。
@EnableAuthorizationServer 授权认证服务器核心注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({AuthorizationServerEndpointsConfiguration.class, AuthorizationServerSecurityConfiguration.class})
public @interface EnableAuthorizationServer {
}
从源码我们知道引入了AuthorizationServerEndpointsConfiguration和AuthorizationServerSecurityConfiguration这2个配置类。
@Configuration
@Import(TokenKeyEndpointRegistrar.class)
public class AuthorizationServerEndpointsConfiguration {
private AuthorizationServerEndpointsConfigurer endpoints = new AuthorizationServerEndpointsConfigurer();
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private List<AuthorizationServerConfigurer> configurers = Collections.emptyList();
@PostConstruct
public void init() {
for (AuthorizationServerConfigurer configurer : configurers) {
try {
configurer.configure(endpoints);
} catch (Exception e) {
throw new IllegalStateException("Cannot configure enpdoints", e);
}
}
endpoints.setClientDetailsService(clientDetailsService);
}
@Bean
public AuthorizationEndpoint authorizationEndpoint() throws Exception {
AuthorizationEndpoint authorizationEndpoint = new AuthorizationEndpoint();
FrameworkEndpointHandlerMapping mapping = getEndpointsConfigurer().getFrameworkEndpointHandlerMapping();
authorizationEndpoint.setUserApprovalPage(extractPath(mapping, "/oauth/confirm_access"));
authorizationEndpoint.setProviderExceptionHandler(exceptionTranslator());
authorizationEndpoint.setErrorPage(extractPath(mapping, "/oauth/error"));
authorizationEndpoint.setTokenGranter(tokenGranter());
authorizationEndpoint.setClientDetailsService(clientDetailsService);
authorizationEndpoint.setAuthorizationCodeServices(authorizationCodeServices());
authorizationEndpoint.setOAuth2RequestFactory(oauth2RequestFactory());
authorizationEndpoint.setOAuth2RequestValidator(oauth2RequestValidator());
authorizationEndpoint.setUserApprovalHandler(userApprovalHandler());
authorizationEndpoint.setRedirectResolver(redirectResolver());
return authorizationEndpoint;
}
@Bean
public TokenEndpoint tokenEndpoint() throws Exception {
TokenEndpoint tokenEndpoint = new TokenEndpoint();
tokenEndpoint.setClientDetailsService(clientDetailsService);
tokenEndpoint.setProviderExceptionHandler(exceptionTranslator());
tokenEndpoint.setTokenGranter(tokenGranter());
tokenEndpoint.setOAuth2RequestFactory(oauth2RequestFactory());
tokenEndpoint.setOAuth2RequestValidator(oauth2RequestValidator());
tokenEndpoint.setAllowedRequestMethods(allowedTokenEndpointRequestMethods());
return tokenEndpoint;
}
@Bean
public CheckTokenEndpoint checkTokenEndpoint() {
CheckTokenEndpoint endpoint = new CheckTokenEndpoint(getEndpointsConfigurer().getResourceServerTokenServices());
endpoint.setAccessTokenConverter(getEndpointsConfigurer().getAccessTokenConverter());
endpoint.setExceptionTranslator(exceptionTranslator());
return endpoint;
}
@Bean
public WhitelabelApprovalEndpoint whitelabelApprovalEndpoint() {
return new WhitelabelApprovalEndpoint();
}
@Bean
public WhitelabelErrorEndpoint whitelabelErrorEndpoint() {
return new WhitelabelErrorEndpoint();
}
查看AuthorizationServerEndpointsConfiguration源码, 我们知道这个配置类会创建AuthorizationEndpoint和TokenEndPoint、CheckTokenEndpoint、WhitelabelApprovalEndpoint、WhitelabelErrorEndpoint等。
查看AuthorizationServerSecurityConfiguration源码,主要的配置ClientDetailsService
UserDetailsService,内部仅有loadUserByUsername方法
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
ClientDetailsUserDetailsService是UserDetailsService的子类,内部维护了ClientDetailsService,loadUserByUsername()方法重写后调用了ClientDetailsService的loadClientByClientId()方法。
public class ClientDetailsUserDetailsService implements UserDetailsService {
private final ClientDetailsService clientDetailsService;
private String emptyPassword = "";
public ClientDetailsUserDetailsService(ClientDetailsService clientDetailsService) {
this.clientDetailsService = clientDetailsService;
}
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
this.emptyPassword = passwordEncoder.encode("");
}
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
ClientDetails clientDetails;
try {
clientDetails = clientDetailsService.loadClientByClientId(username);
} catch (NoSuchClientException e) {
throw new UsernameNotFoundException(e.getMessage(), e);
}
String clientSecret = clientDetails.getClientSecret();
if (clientSecret == null || clientSecret.trim().length() == 0) {
clientSecret = emptyPassword;
}
return new User(username, clientSecret, clientDetails.getAuthorities());
}
}
ClientDetailsService,内部仅有loadClientByClientId(),从方法名我们可以知道是通过clientId来获取client信息,官方提供俩个实现类,我们也可以像UserDetailsService一样自已编写实现类。
public interface ClientDetailsService {
ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException;
}
ClientCredentialsTokenEndPointFilter
1.拦截/oauth/token,获取到clientId和clientsecret信息
2.创建UsernamepasswordAutherticationToken
3.作为AuthenticationManager().authenticate(authRequest)参数调用认证过程
整个认证过程唯一最大的区别在于:
DaoAuthenticationProvider.retrieveUser() 获取认证用户信息时调用的是 ClientDetailsUserDetailsService,其内部其实是调用ClientDetailsService 获取到客户端信息。
@EnableResourceServer
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ResourceServerConfiguration.class)
public @interface EnableResourceServer {
}
从源码我们知道引入了ResourceServerConfiguration配置类,这个配置类是应用了ResourceServerSecurityConfigurer
ResourceServerSecurityConfigurer
1.创建OAuth2AuthenticationManager对象
2.创建OAuth2AuthenticationProcessingFilter 过滤器
3.将OAuth2AuthenticationProcessingFilter过滤器加载到过滤链上
AuthorizationEndPoint
AuthorizationEndpoint:用于服务授权请求。 请求地址:/oauth/authorize
1.getOAuth2RequestFactory根据参数创建AuthorizationRequest。
2.判断principal是否已授权,oauth/authorize 设置为无权限访问 ,如果 判断失败则抛出 InsufficientAuthenticationException异常会被 ExceptionTranslationFilter 处理 ,然后重定向到登录页。
3.通过getClientDetailsService()获取到ClientDetails客户端信息。
4.获取参数中的回调地址和系统配置的回调地址对比。
5.验证scope。
6.检测客户端是否设置自动授权,客户端配置autoApprove(true)。
7.如果设置 autoApprove(true) 则 调用 getAuthorizationCodeResponse() 方法生成code码并回调到设置的回调地址。
8.真实生成Code 的方法是generateCode(AuthorizationRequest authorizationRequest, Authentication authentication) 方法,其内部是调用authorizationCodeServices.createAuthorizationCode()方法生成code。
TokenEndPoint
TokenEndPoint: 用于服务访问令牌的请求。请求地址:/oauth/token
1.从principal中获取clientId,进而load client信息。
2.从paramters中拿clientid,scope,grantType组装tokenRequest。
3.校验client信息,确保从principal拿到的client信息与根据paramters得到的client信息一致。
4.根据grantType设置tokenRequest的scope,授权类型有password 模式、authorization_code 模式、refresh_token 模式、client_credentials 模式、implicit 模式。
5.如果是授权模式,则清空scope,因为授权请求会确定scope,所以没必要传。
6.如果是刷新Token模式,解析并设置scope。
7.通过令牌授予者获取token。
TokenGranter
官方默认调用CompositeTokenGranter的grant()方法,debug追踪知道默认有五个子类加上一个共同的父类(AbstractTokenGranter),然后遍历尝试看使用的是哪种授权方式,ClientCredentialsTokenGranter重写了父类的grant()方法,其余四种都是直接调用父类进行处理。
AbstractTokenGranter.grant()
1.判断grantType是否匹配。
2.获取client信息并验证grantType。
3.调用用getAccessToken()方法生成token返回。
getAccessToken()方法
1.通过getOAuth2Authentication()方法获取OAuth2Authentication对象。
2.调用tokenServices.createAccessToken()方法生成token
AuthorizationCodeTokenGranter.getOAuth2Authentication()方法实现
1.从tokenRequest中获取code和回调url
2.authorizationCodeServices.consumeAuthorizationCode(authorizationCode)通过code获取OAuth2Authentication对象
3.从OAuth2Authentication对象获取OAuth2Request对象并验证回调url和clientid
4.创建一个新的OAuth2Request,并从OAuth2Authentication中获取到Authentication对象
5.通过新的OAuth2Request对象和Authentication对象创建一个全新的OAuth2Authentication对象
生成OAuth2Authentication调用tokenServices.createAccessToken()
查看DefaultTokenServices的createAccessToken()方法
1.通过tokenStore获取之前存在的token并判断是否为空和是否过期,如果不为空且未过期就直接返回token(我们常用Jwt 这里是 JwtTokenStore ,且 existingAccessToken 永远为空,即每次请求获取token的值均不同,跟RedisTokenStore 是不一样的)。
2.调用createRefreshToken()生成refreshToken。
3.调用createAccessToken(authentication, refreshToken)生成accesstoken。
从源码看出token通过uuid生成的,生成过程相对简单,但是如果我们配置了token增强器(TokenEnhancer)jwtToken就是使用了增强器实现。
4.重新覆盖原有的刷新token(原有的 refreshToken 为UUID 数据,覆盖为 jwtToken)并返回token。
OAuth2AuthenticationProcessingFilter解析
我们获取到了token,就会通过token去拿到资源信息。那么资源服务器是如何通过传入的token去辨别用户并允许返回资源信息的。
1.创建OAuth2AuthenticationProcessingFilter
2.为OAuth2AuthenticationProcessingFilter提供固定的AuthenticationManager即OAuth2AuthenticationManager,它并没有将OAuth2AuthenticationManager添加到spring的容器中,不然可能会影响spring security的普通认证流程(非oauth2请求),只有被OAuth2AuthenticationProcessingFilter拦截到的oauth2相关请求才被特殊的身份认证器处理。
3.设置了TokenExtractor默认的实现—-BearerTokenExtractor
4.相关的异常处理器,可以重写实现
1.调用tokenExtractor.extract()从请求中解析出token信息并存放到authentication 的 principal 字段 中
2.调用 authenticationManager.authenticate() 认证过程:注意此时的 authenticationManager 是 OAuth2AuthenticationManager
authenticationManager.authenticate() 方法实现
1.从authentication获取token
2.调用tokenServices.loadAuthentication()方法通过token参数获取到OAuth2Authentication对象,tokenServices就是我们资源服务器所配置的。
3.checkClientDetails()检测客户端信息,由于授权服务器和资源服务器分离设计,这个检测方法实际没有进行检测
4.设置认证成功标识并返回,返回的是OAuth2Authentication
回顾:
创建token需要的几个必要类:
clientDetailsService,authorizationServerTokenServices,ClientDetails ,TokenRequest,OAuth2Request,authentication和OAuth2Authentication 。要了解这几个类直接的联系。
clientDetailsService和authorizationServerTokenServices可以直接从spring 容器获取,ClientDetails可以从请求参数中获取,有了ClientDetails 就有了TokenRequest,有了TokenRequest和authentication就有了OAuth2Authentication,有了OAuth2Authentication就能生成OAuth2AccessToken。