Spring Security Oauth2 源码解析(一)

 2019-11-23 11:10  阅读(1192)
文章分类:Spring Cloud

更新注意:

—————————————————————————–

springboot 2.0使用spring-security-oauth2的迁移指南

有朋友使用了 springboot2.0 之后发现原来的demo不能用了,我调试了下,发现springboot2.0和spring5的改动还是挺多的,帮大家踩下坑

改动一 暴露AuthenticationManager

springboot2.0 的自动配置发生略微的变更,原先的自动配置现在需要通过@Bean暴露,否则你会得到AuthenticationManager找不到的异常

@Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            AuthenticationManager manager = super.authenticationManagerBean();
            return manager;
        }

[https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.0-Migration-Guide][https_github.com_spring-projects_spring-boot_wiki_Spring-Boot-2.0-Migration-Guide] 查看 Security 相关的改动

改动二 添加PasswordEncoder

如果你得到这个异常 java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id “null” springboot2.0 需要增加一个加密器,原来的 plainTextPasswordEncoder 新版本被移除

方法一

@Bean
        PasswordEncoder passwordEncoder(){
            return NoOpPasswordEncoder.getInstance();
        }

如上是最简单的修改方式,但缺点很明显,是使用明文存储的密码

方法二

@Bean
        PasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }

        String finalPassword = new BCryptPasswordEncoder().encode("123456");
        String finalSecret = new BCryptPasswordEncoder().encode("123456");

配置具体的 BCryptPasswordEncoder,别忘了存储的密码和oauth client 的 secret 也要存储对应的编码过后的密码,而不是明文!

方法三

@Bean
        PasswordEncoder passwordEncoder(){
            return PasswordEncoderFactories.createDelegatingPasswordEncoder();
        }

        String finalPassword = "{bcrypt}"+new BCryptPasswordEncoder().encode("123456");
        String finalSecret = "{bcrypt}"+new BCryptPasswordEncoder().encode("123456");

使用 PasswordEncoderFactories.createDelegatingPasswordEncoder() 创建一个 DelegatingPasswordEncoder,这个委托者通过密码的前缀来 区分应该使用哪一种编码器去校验用户登录时的密码,文档中推荐使用这种方式,并且推荐使用 Bcrypt 而不是 md5,sha256 之类的算法,具体原因看文档

了解细节戳这里:[https://docs.spring.io/spring-security/site/docs/5.0.4.RELEASE/reference/htmlsingle/\#10.3.2][https_docs.spring.io_spring-security_site_docs_5.0.4.RELEASE_reference_htmlsingle_10.3.2]DelegatingPasswordEncoder

改动三 注意Redis的版本

如果你在使用新版本的oauth2时发现有redis的相关报错,如redis-client找不到相应的接口,序列化问题时,请注意下 spring-security-oauth2 的版本 务必高于 2.3.2.RELEASE,这是官方的一个bug,参考 [https://github.com/spring-projects/spring-security-oauth/issues/1335][https_github.com_spring-projects_spring-security-oauth_issues_1335]

<dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>2.3.2.RELEASE</version>
        </dependency>

更新變動結束

—————————————————————————–

关于oauth2,其实是一个规范,本文重点讲解spring对他进行的实现,如果你还不清楚授权服务器,资源服务器,认证授权等基础概念,可以移步[理解OAuth 2.0 – 阮一峰][OAuth 2.0 _],这是一篇对于oauth2很好的科普文章。

准备工作

首先开启debug信息:

logging:
      level:
        org.springframework: DEBUG

可以完整的看到内部的运转流程。

client模式稍微简单一些,使用client模式获取token
http://localhost:8080/oauth/token?client_id=client_1&client_secret=123456&scope=select&grant_type=client_credentials

由于debug信息太多了,我简单按照顺序列了一下关键的几个类:

ClientCredentialsTokenEndpointFilter
    DaoAuthenticationProvider
    TokenEndpoint
    TokenGranter

@EnableAuthorizationServer

上一篇博客中我们尝试使用了password模式和client模式,有一个比较关键的endpoint:/oauth/token。从这个入口开始分析,spring security oauth2内部是如何生成token的。获取token,与第一篇文章中的两个重要概念之一有关,也就是AuthorizationServer与ResourceServer中的AuthorizationServer。

在之前的配置中

@Configuration
    @EnableAuthorizationServer
    protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {}

出现了AuthorizationServerConfigurerAdapter 关键类,他关联了三个重要的配置类,分别是

public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {

        @Override
        public void configure(AuthorizationServerSecurityConfigurer security <1>) throws Exception {
        }

        @Override
        public void configure(ClientDetailsServiceConfigurer clients <2>) throws Exception {
        }

        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints <3>) throws Exception {
        }

    }

<1> 配置AuthorizationServer安全认证的相关信息,创建ClientCredentialsTokenEndpointFilter核心过滤器 <2> 配置OAuth2的客户端相关信息 <3> 配置AuthorizationServerEndpointsConfigurer众多相关类,包括配置身份认证器,配置认证方式,TokenStore,TokenGranter,OAuth2RequestFactory 我们逐步分析其中关键的类 ## 客户端身份认证核心过滤器ClientCredentialsTokenEndpointFilter(掌握) ## 截取关键的代码,可以分析出大概的流程 在请求到达/oauth/token之前经过了ClientCredentialsTokenEndpointFilter这个过滤器,关键方法如下 ``` public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { ... String clientId = request.getParameter("client_id"); String clientSecret = request.getParameter("client_secret"); ... clientId = clientId.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId, clientSecret); return this.getAuthenticationManager().authenticate(authRequest); } ``` ## 顶级身份管理者AuthenticationManager(掌握) ## 用来从请求中获取client\_id,client\_secret,组装成一个UsernamePasswordAuthenticationToken作为身份标识,使用容器中的顶级身份管理器AuthenticationManager去进行身份认证(AuthenticationManager的实现类一般是ProviderManager。而ProviderManager内部维护了一个List,真正的身份认证是由一系列AuthenticationProvider去完成。而AuthenticationProvider的常用实现类则是DaoAuthenticationProvider,DaoAuthenticationProvider内部又聚合了一个UserDetailsService接口,UserDetailsService才是获取用户详细信息的最终接口,而我们上一篇文章中在内存中配置用户,就是使用了UserDetailsService的一个实现类InMemoryUserDetailsManager)。UML类图可以大概理解下这些类的关系,省略了授权部分。 ![20191123100255\_1.png][20191123100255_1.png] 可能机智的读者会发现一个问题,我前面一篇文章已经提到了client模式是不存在“用户”的概念的,那么这里的身份认证是在认证什么呢?debug可以发现UserDetailsService的实现被适配成了ClientDetailsUserDetailsService,这个设计是将client客户端的信息(client\_id,client\_secret)适配成用户的信息(username,password),这样我们的认证流程就不需要修改了。 经过ClientCredentialsTokenEndpointFilter之后,身份信息已经得到了AuthenticationManager的验证。接着便到达了 TokenEndpoint。 ## Token处理端点TokenEndpoint(掌握) ## 前面的两个ClientCredentialsTokenEndpointFilter和AuthenticationManager可以理解为一些前置校验,和身份封装,而这个类一看名字就知道和我们的token是密切相关的。 ``` @FrameworkEndpoint public class TokenEndpoint extends AbstractEndpoint { @RequestMapping(value = "/oauth/token", method=RequestMethod.POST) public ResponseEntity postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException { ... String clientId = getClientId(principal); ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);//<1> ... TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);//<2> ... OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);//<3> ... return getResponse(token); } private TokenGranter tokenGranter; } ``` <1> 加载客户端信息 <2> 结合请求信息,创建TokenRequest <3> 将TokenRequest传递给TokenGranter颁发token 省略了一些校验代码之后,真正的/oauth/token端点暴露在了我们眼前,其中方法参数中的Principal经过之前的过滤器,已经被填充了相关的信息,而方法的内部则是依赖了一个TokenGranter 来颁发token。其中OAuth2AccessToken的实现类DefaultOAuth2AccessToken就是最终在控制台得到的token序列化之前的原始类: ``` public class DefaultOAuth2AccessToken implements Serializable, OAuth2AccessToken { private static final long serialVersionUID = 914967629530462926L; private String value; private Date expiration; private String tokenType = BEARER_TYPE.toLowerCase(); private OAuth2RefreshToken refreshToken; private Set scope; private Map<String, Object> additionalInformation = Collections.emptyMap(); //getter,setter } @org.codehaus.jackson.map.annotate.JsonSerialize(using = OAuth2AccessTokenJackson1Serializer.class) @org.codehaus.jackson.map.annotate.JsonDeserialize(using = OAuth2AccessTokenJackson1Deserializer.class) @com.fasterxml.jackson.databind.annotation.JsonSerialize(using = OAuth2AccessTokenJackson2Serializer.class) @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = OAuth2AccessTokenJackson2Deserializer.class) public interface OAuth2AccessToken { public static String BEARER_TYPE = "Bearer"; public static String OAUTH2_TYPE = "OAuth2"; /** * The access token issued by the authorization server. This value is REQUIRED. */ public static String ACCESS_TOKEN = "access_token"; /** * The type of the token issued as described in <a * href="http://tools.ietf.org/html/draft-ietf-oauth-v2-22#section-7.1">Section 7.1. Value is case insensitive. * This value is REQUIRED. */ public static String TOKEN_TYPE = "token_type"; /** * The lifetime in seconds of the access token. For example, the value "3600" denotes that the access token will * expire in one hour from the time the response was generated. This value is OPTIONAL. */ public static String EXPIRES_IN = "expires_in"; /** * The refresh token which can be used to obtain new access tokens using the same authorization grant as described * in Section 6. This value is OPTIONAL. */ public static String REFRESH_TOKEN = "refresh_token"; /** * The scope of the access token as described by <a * href="http://tools.ietf.org/html/draft-ietf-oauth-v2-22#section-3.3">Section 3.3 */ public static String SCOPE = "scope"; ... ``` 一个典型的样例token响应,如下所示,就是上述类序列化后的结果: ``` { "access_token":"950a7cc9-5a8a-42c9-a693-40e817b1a4b0", "token_type":"bearer", "refresh_token":"773a0fcd-6023-45f8-8848-e141296cb3cb", "expires_in":27036, "scope":"select" } ``` ## TokenGranter(掌握) ## 先从UML类图对TokenGranter接口的设计有一个宏观的认识 ![20191123100255\_2.png][20191123100255_2.png] TokenGranter的设计思路是使用CompositeTokenGranter管理一个List列表,每一种grantType对应一个具体的真正授权者,在debug过程中可以发现CompositeTokenGranter 内部就是在循环调用五种TokenGranter实现类的grant方法,而granter内部则是通过grantType来区分是否是各自的授权类型。 ``` public class CompositeTokenGranter implements TokenGranter { private final List tokenGranters; public CompositeTokenGranter(List tokenGranters) { this.tokenGranters = new ArrayList(tokenGranters); } public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) { for (TokenGranter granter : tokenGranters) { OAuth2AccessToken grant = granter.grant(grantType, tokenRequest); if (grant!=null) { return grant; } } return null; } } ``` 五种类型分别是: * ResourceOwnerPasswordTokenGranter ==> password密码模式 * AuthorizationCodeTokenGranter ==> authorization\_code授权码模式 * ClientCredentialsTokenGranter ==> client\_credentials客户端模式 * ImplicitTokenGranter ==> implicit简化模式 RefreshTokenGranter ==>refresh\_token 刷新token专用 以客户端模式为例,思考如何产生token的,则需要继续研究5种授权者的抽象类:AbstractTokenGranter ``` public abstract class AbstractTokenGranter implements TokenGranter { protected final Log logger = LogFactory.getLog(getClass()); //与token相关的service,重点 private final AuthorizationServerTokenServices tokenServices; //与clientDetails相关的service,重点 private final ClientDetailsService clientDetailsService; //创建oauth2Request的工厂,重点 private final OAuth2RequestFactory requestFactory; private final String grantType; ... public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) { ... String clientId = tokenRequest.getClientId(); ClientDetails client = clientDetailsService.loadClientByClientId(clientId); validateGrantType(grantType, client); logger.debug("Getting access token for: " + clientId); return getAccessToken(client, tokenRequest); } protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) { return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest)); } protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) { OAuth2Request storedOAuth2Request = requestFactory.createOAuth2Request(client, tokenRequest); return new OAuth2Authentication(storedOAuth2Request, null); } ... } ``` 回过头去看TokenEndpoint中,正是调用了这里的三个重要的类变量的相关方法。由于篇幅限制,不能延展太多,不然没完没了,所以重点分析下AuthorizationServerTokenServices是何方神圣。 ## AuthorizationServerTokenServices(了解) ## AuthorizationServer端的token操作service,接口设计如下: ``` public interface AuthorizationServerTokenServices { //创建token OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException; //刷新token OAuth2AccessToken refreshAccessToken(String refreshToken, TokenRequest tokenRequest) throws AuthenticationException; //获取token OAuth2AccessToken getAccessToken(OAuth2Authentication authentication); } ``` 在默认的实现类DefaultTokenServices中,可以看到token是如何产生的,并且了解了框架对token进行哪些信息的关联。 ``` @Transactional public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException { OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication); OAuth2RefreshToken refreshToken = null; if (existingAccessToken != null) { if (existingAccessToken.isExpired()) { if (existingAccessToken.getRefreshToken() != null) { refreshToken = existingAccessToken.getRefreshToken(); // The token store could remove the refresh token when the // access token is removed, but we want to // be sure... tokenStore.removeRefreshToken(refreshToken); } tokenStore.removeAccessToken(existingAccessToken); } else { // Re-store the access token in case the authentication has changed tokenStore.storeAccessToken(existingAccessToken, authentication); return existingAccessToken; } } // Only create a new refresh token if there wasn't an existing one // associated with an expired access token. // Clients might be holding existing refresh tokens, so we re-use it in // the case that the old access token // expired. if (refreshToken == null) { refreshToken = createRefreshToken(authentication); } // But the refresh token itself might need to be re-issued if it has // expired. else if (refreshToken instanceof ExpiringOAuth2RefreshToken) { ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken; if (System.currentTimeMillis() > expiring.getExpiration().getTime()) { refreshToken = createRefreshToken(authentication); } } OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken); tokenStore.storeAccessToken(accessToken, authentication); // In case it was modified refreshToken = accessToken.getRefreshToken(); if (refreshToken != null) { tokenStore.storeRefreshToken(refreshToken, authentication); } return accessToken; } ``` 简单总结一下AuthorizationServerTokenServices的作用,他提供了创建token,刷新token,获取token的实现。在创建token时,他会调用tokenStore对产生的token和相关信息存储到对应的实现类中,可以是redis,数据库,内存,jwt。 ## 总结 ## 本篇总结了使用客户端模式获取Token时,spring security oauth2内部的运作流程,重点是在分析AuthenticationServer相关的类。其他模式有一定的不同,但抽象功能是固定的,只是具体的实现类会被相应地替换。阅读spring的源码,会发现它的设计中出现了非常多的抽象接口,这对我们理清楚内部工作流程产生了不小的困扰,我的方式是可以借助UML类图,先从宏观理清楚作者的设计思路,这会让我们的分析事半功倍。 作者:[下一秒升华][Link 1] 原文链接:[https://blog.csdn.net/u013815546/article/details/76977239][https_blog.csdn.net_u013815546_article_details_76977239] **版权归作者所有,转载请注明出处** [https_github.com_spring-projects_spring-boot_wiki_Spring-Boot-2.0-Migration-Guide]: https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.0-Migration-Guide [https_docs.spring.io_spring-security_site_docs_5.0.4.RELEASE_reference_htmlsingle_10.3.2]: https://docs.spring.io/spring-security/site/docs/5.0.4.RELEASE/reference/htmlsingle/#10.3.2 [https_github.com_spring-projects_spring-security-oauth_issues_1335]: https://github.com/spring-projects/spring-security-oauth/issues/1335 [OAuth 2.0 _]: http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html [20191123100255_1.png]: https://gitee.com/chenssy/blog-home/raw/master/image/series-images/springcloud/20191123100255_1.png [20191123100255_2.png]: https://gitee.com/chenssy/blog-home/raw/master/image/series-images/springcloud/20191123100255_2.png [Link 1]: https://blog.csdn.net/u013815546 [https_blog.csdn.net_u013815546_article_details_76977239]: https://blog.csdn.net/u013815546/article/details/76977239 ------- 来源:[http://ddrv.cn/a/88268](http://ddrv.cn/a/88268)

点赞(0)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> Spring Security Oauth2 源码解析(一)

相关推荐