使用‘过时的’spring-security-oauth2实现oauth2授权服务器及自定义认证方式

4,694 阅读8分钟

前言

最近正在重构一个项目,包含许多客户端,如浏览器,移动端app,移动端微信公众号等,在老大的设计下,它们各自有自己的后台服务,但有一些公有的服务被抽取出来做一个核心服务被各客户端的后台服务共享,也使用spring cloud + consul + feign来实现服务的注册发现和服务间调用

老大决定把之前的安全与权限控制也直接做一个修改,决定使用oauth2的方式控制服务间的权限控制,大概意思就是各客户端服务去请求核心服务做认证成功后返回一个access_token,以后每次进行服务接口调用时带上这个access_token,核心服务根据access_token获得认证信息。

本篇文章要介绍的是oauth2的授权服务器实现,使用spring-security-oauth2实现一个oauth2授权服务器(Authorization Server),它将负责拿到客户端认证信息执行认证过程,成功后授权签发access_token。至于客户端如何去调用获得access_token,不在本篇范围内,看情况下一篇写喽~

oauth2的简介

关于oauth2授权认证,网络上有许多介绍的博客和资料,这里简单提一下。

首先实现完整的oauth2过程应该包括以下几个部分:

  • Resource Server,资源服务器,第三方,如被微信保护着的微信用户信息
  • Resource Owner,资源拥有者,如微信用户
  • Authorization Server,授权服务器,第三方,如微信在用户同意授权给我们后,提供给我们的访问资源服务器权限的开放平台
  • Client,客户端,如微信开发者,微信用户在登录微信后,被微信开发者 ‘诱导’ 点击请求授权服务器授权给我们开发者,微信开发者就可以去资源服务器获得用户的一些信息了

oauth2定义了四种授权方式:

  • 授权码模式(authorization code),常用,如微信用户先登录微信,请求授权页面,同意授权后会给微信开发者一个code,通过这个code开发者可以再获得access_token,再去请求资源服务器获得用户信息
  • 简化模式(implicit)
  • 密码模式(resource owner password credentials),第三方认证时不常用,如微信用户直接输入微信用户名密码给开发者,开发者再拿着用户名,密码去微信那边获得权限,当然,微信不可能这么干的,但是因为我们是同一家公司服务间调用,可以使用这个模式。
  • 客户端模式(client credentials),与用户无关的一些认证服务用。

关于更多oauth2的详细流程,不在本文范围,可以参考文末参考中关于oauth2的认证流程。

总结来说,我们这里并不是标准的oauth2实践,因为一般情况下需要第三方认证授权才会用到。但是这里老大想把核心服务当做一个第三方的资源服务器,来使各客户端后台服务和核心服务间的授权调用过程用户认证信息的传递更简单。

介绍完了背景和理论,下面开始使用spring提供的oauth2支持实现以下吧~

关于spring-security-oauth2的过时

上面提到本文主要来做oauth2中的一部分,也就是Authorization Server授权服务器的实现过程。首先我就去大名鼎鼎的spring security中看看它是怎么实现oauth2的授权服务器的,然后得到了一个无情的结果,最新的spring security模块已经不提供授权服务器的实现了。

其实spring security oauth2模块是实现了oauth2的授权服务器的,但是官方又说spring security oauth2模块已全部过时,迁移到spring security模块提供,还‘贴心’地提供了迁移指南

点开一看

This document contains guidance for moving OAuth 2.0 Clients and Resource Servers from Spring Security OAuth 2.x to Spring Security 5.2.x. Since Spring Security doesn’t provide Authorization Server support, migrating a Spring Security OAuth Authorization Server is out of scope for this document.

大意就是,我们把提供了oauth2授权服务器实现的spring-security-oauth2过时了哦,但是我们在新的方案里不提供oauth2授权服务器地实现哈~

没错,这里是在吐槽spring,当然因为大家吐槽的不在少数,spring官方也正在建设新的oauth2授权服务器实现模块,未来可期。

可以看看spring oauth2授权服务器的历程

stackoverflow.com/questions/5…

总结来说,我们需要实现oauth2授权服务器,不用其它新技术的情况下,暂时需要使用一个过时的模块spring-security-oauth,相关的代码也都会有一个删除线

客户端信息配置

这里的客户端是oauth2中的概念,类似与微信开发中的appid和appsecret信息,在我们的需求这里指的就是各个不同用户客户端对应的的后台服务,如web端,移动app端,移动微信公众号端。配置它们的信息后,对应它们的后台服务拿着他们的信息才能授权服务器这里来请求access_token等授权信息。

spring-security-oauth模块提供了一个方便的注解@EnableAuthorizationServer和适配器AuthorizationServerConfigurerAdapter让我们配置Authorization Server,代码如下:

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients
            .inMemory()
            // 配置web客户端
            .withClient("web")
            .secret(passwordEncoder.encode("**"))
            // web端通过password形式认证
            .authorizedGrantTypes("password")
            .and()
            // 配置微信公众号客户端
            .withClient("wx_subscription")
            .secret(passwordEncoder.encode("**"))
            // 微信公众号客户端需要通过微信提供的code进行认证,所有自定义了认证方式,需要后面的支持
            .authorizedGrantTypes("wx_subscription")
    }
}

这里因为不需要动态修改,直接在内存中配置了。

通过以上配置,即可spring已经为我们提供了以下东西:

  • oauth2认证授权端点
  • 认证后token的生成、签发、保存

但是spring如何是根据passwordwx_subscription的code来认证呢,需要在以下认证管理器中配置。

认证管理器配置

首先完成password形式的认证,仍然在上面的适配器类中,配置AuthorizationServerEndpointsConfigurer的认证管理器端点,这个认证管理器authenticationManager与我们平常在使用spring security时所配置的认证管理器一样即可。

public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        // 设置这个开启password授权方式,通过spring security暴露
        endpoints.authenticationManager(authenticationManager);
    }
}

关于在spring securtiy的配置中暴露AuthenticationManager

@Configuration
@EnableWebSecurity
class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    //.. 省略用户名密码等认证配置
    
    /**
         * 暴露认证管理器给授权服务器使用
         * @return
         * @throws Exception
         */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

测试

经过以上配置之后,如果你的用户名密码等认证配置的没有问题,此时就可以启动项目测试以下oauth2的password认证方式是否好用了

@Test
void testWebClient() {
    ResourceOwnerPasswordResourceDetails resource = new ResourceOwnerPasswordResourceDetails();
    resource.setClientId("web");
    resource.setClientSecret("**");
    resource.setAccessTokenUri("http://**/oauth/token");// Oauth2.0 服务端链接
    resource.setScope(Arrays.asList(""));// 读写权限
    resource.setUsername("**");
    resource.setPassword("**");
    resource.setGrantType("password");// Oauth2.0 使用的模式 为密码模式
    AccessTokenRequest atr = new DefaultAccessTokenRequest();
    OAuth2RestTemplate template = new OAuth2RestTemplate(resource, new DefaultOAuth2ClientContext(atr));
    ResourceOwnerPasswordAccessTokenProvider provider = new ResourceOwnerPasswordAccessTokenProvider();
    template.setAccessTokenProvider(provider);
    System.out.println(template.getAccessToken());
}

微信公众号等自定义认证方式配置

以上配置完成了oauth2标准中定义的通过用户名密码直接获得授权的方式。

但我们有时不只通过用户名密码进行认证,如有是通过手机号 + 验证码进行认证;微信公众号的code进行认证。这里我们自定义配置一下通过微信公众号的code认证过程。

public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {@Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        // 设置这个开启password授权方式
        endpoints.authenticationManager(authenticationManager);
        // 增加自定义授权方式,这里可以增加新的认证方式,只要自定义TokenGranter即可
        endpoints.tokenGranter(
            new CompositeTokenGranter(
                Arrays.asList(endpoints.getTokenGranter(), customTokenGranter(endpoints))
            )
        );
    }
}

关于自定义TokenGranter的配置,这里因为AuthorizationServerEndpointsConfigurer的一些内部方法,使用了反射调用其必要方法,且使用了代理进行延迟加载,不然会有一些生命周期问题

private TokenGranter customTokenGranter(AuthorizationServerEndpointsConfigurer endpoints) {
    // 代理延迟加载
    return new TokenGranter() {
        private TokenGranter delegate;
        // 第一次使用时才创建
        @Override
        public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
            if (delegate == null) {
                try {
                    // private方法,需要使用getDeclaredMethod
                    Method tokenServicesMethod = endpoints.getClass().getDeclaredMethod("tokenServices"),
                    requestFactoryMethod = endpoints.getClass().getDeclaredMethod("requestFactory");
                    tokenServicesMethod.setAccessible(true);
                    requestFactoryMethod.setAccessible(true);
                    AuthorizationServerTokenServices tokenServices = (AuthorizationServerTokenServices) tokenServicesMethod.invoke(endpoints);
                    OAuth2RequestFactory requestFactory = (OAuth2RequestFactory) requestFactoryMethod.invoke(endpoints);
                    // 这里也加入了我们的认证管理器authenticationManager,所有后续通过code进行微信认证的过程也在spring security的AuthenticationManager中完成
                    delegate = new WxSubscriptionCodeTokenGranter(authenticationManager, tokenServices, clientDetailsService, requestFactory);
                }catch (Exception e){
                    logger.error("创建自定义授权方式失败", e);
                    return null;
                }
            }
            return delegate.grant(grantType, tokenRequest);
        }
    };
}

关于WxSubscriptionCodeTokenGranter的实现,基本参考spring提供的ResourceOwnerPasswordTokenGranter类实现即可,把认证方式改成我们的wx_subscription即可,这里就不赘述贴代码了。

至此,完成了oauth2自定义认证方式Authorization Server的配置。

总结

  • oauth2认证方式用在分布式服务间调用以避免传递用户认证信息,这个做法现在好像还不是太多,这里优缺点欢迎大家思考讨论
  • 通过代理的方式可以完成懒加载的创建,避免一些复杂的生命周期方法依赖调用

本文只简单介绍了oauth2标准中Authorization Server使用spring security oauth2如何实现,以后可能再写关于resource server和client的实现。

欢迎大家批评讨论~

参考

关于oauth2的认证流程 www.cnblogs.com/wudimanong/…

spring-security-oauth2的过时 stackoverflow.com/questions/5…

spring-security-oauth2的文档 projects.spring.io/spring-secu…