01-Spring Security Oauth2源码解析

当你掌握某个技术,每次再重新去研究的时候都会有不一样的感悟。随的时间的推移每个人的能力都有不一样的提高,这时候再去回头看就会有更清晰的感觉。本文主要就是对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有本质区别!

  1. AuthorizationRequest
    向授权服务器AuthorizationEndPoint(/oauth/authorize)请求授权,AuthorizationRequest作为载体存储state,redirect_uri等参数,生命周期很短且不能长时间存储信息,可用OAuth2Request代替存储信息。

  2. TokenRequest
    向授权服务器TokenEndPoint(/oauth/token)发送请求获得access_token时,tokenRequest作为载体存储请求中grantType等参数。常和tokenGranter.grant(grantType,tokenRequest)结合起来使用。
    TokenRequest携带了新属性grantType,和createOAuth2Request(用于持久化)

  3. 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。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342