给 Spring Security OAuth 增加新的授权模式

495 阅读3分钟

这是我参与2022首次更文挑战的第11天,活动详情查看:2022首次更文挑战

由于内容相关联,我先列几篇之前的文章,欢迎阅读:

以上三篇文章分别分析了,Spring Security 是怎么完成用户认证的、如何给 Spring Security 增加新的认证逻辑(手机验证码)、Spring Security OAuth 密码模式(资源所有者模式)是如何完成认证的。

这篇我们来试着参考密码模式,给 Spring Security OAuth 增加一个手机验证码模式。以下内容会与之前的文章内容有关联,因此在此之前,可以先阅读之前的几篇文章,回顾一下这些内容。

本文有大量内容会用到 Spring Security 自定义认证逻辑 这篇文章里创建的 Java 类,强烈建议你先读一下。

实现 Granter

首先需要实现一个与 ResourceOwnerPasswordTokenGranter 类似的、手机验证码模式对应的 Granter 类,继承 AbstractTokenGranter

我们根据 ResourceOwnerPasswordTokenGranter 照猫画虎,写一个 SmsTokenGranter

public class SmsTokenGranter extends AbstractTokenGranter {

    private static final String GRANT_TYPE = "sms";

    private final AuthenticationManager authenticationManager;

    public SmsTokenGranter(AuthenticationManager authenticationManager,
                           AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) {
        this(authenticationManager, tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
    }

    protected SmsTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices,
                                                ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, String grantType) {
        super(tokenServices, clientDetailsService, requestFactory, grantType);
        this.authenticationManager = authenticationManager;
    }

    @Override
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {

        Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters());
        String username = parameters.get("mobile");
        String password = parameters.get("smsCode");

        Authentication userAuth = new SmsCodeAuthenticationToken(username, password);
        ((AbstractAuthenticationToken) userAuth).setDetails(parameters);
        try {
            userAuth = authenticationManager.authenticate(userAuth);
        }
        catch (AccountStatusException ase) {
            //covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)
            throw new InvalidGrantException(ase.getMessage());
        }
        catch (BadCredentialsException e) {
            // If the username/password are wrong the spec says we should send 400/invalid grant
            throw new InvalidGrantException(e.getMessage());
        }
        if (userAuth == null || !userAuth.isAuthenticated()) {
            throw new InvalidGrantException("Could not authenticate user: " + username);
        }

        OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
        return new OAuth2Authentication(storedOAuth2Request, userAuth);
    }
}

这是已完成的代码,根据 ResourceOwnerPasswordTokenGranter 修改而来,需要关注的地方有两个:

  1. 代码第 3 行,GRANT_TYPE = "sms",认证类型为 sms。OAuth 2.0 共包含四种模式,分别是authorization_codeimplicitpasswordclient_credential。我们现在为其添加 sms 模式。
  2. 代码第 22 到 25 行,从请求中获取手机号和验证码参数,然后封装成一个 SmsCodeAuthenticationToken 对象。SmsCodeAuthenticationToken 也是我们为手机验证码模式创建的类,在 Spring Security 自定义认证逻辑 中已经写过了,具体的写法和作用,可以参考这篇文章。

Authentication 和 AuthenticationProvider

SmsCodeAuthenticationToken 是我们为了手机验证码模式创建的 Authentication 实现类。另外,在 SmsTokenGranter 的 getOAuth2Authentication 方法中,还有一行关键的代码:

userAuth = authenticationManager.authenticate(userAuth);

如果你阅读了文章开头的关联文章,你应该了解这句代码调用的方法背后,实现了认证的逻辑,并且,这里需要实现一个与手机验证码模式对应的 AuthenticationProvider 类,这个类我们在之前的文章里也是先过了,参考 Spring Security 自定义认证逻辑SmsCodeAuthenticationProvider 实现的部分。

配置

以上就是所有需要创建的类,接下来完成配置。

这里有个前提,就是已经配置好了 Spring Security OAuth 的功能,以下内容直接少我们需要在此基础上需要增加那些配置。

首先,在 Spring Security OAuth 的授权服务器配置类,也就是 AuthorizationServerConfigurerAdapter 的子类中的 configure(AuthorizationServerEndpointsConfigurer endpoints) 方法中,添加如下配置:

List<TokenGranter> tokenGranters = new ArrayList<>();

tokenGranters.add(new AuthorizationCodeTokenGranter(endpoints.getTokenServices(), endpoints.getAuthorizationCodeServices(), clientDetailsService,
        endpoints.getOAuth2RequestFactory()));
tokenGranters.add(new RefreshTokenGranter(endpoints.getTokenServices(), clientDetailsService, endpoints.getOAuth2RequestFactory()));
ImplicitTokenGranter implicit = new ImplicitTokenGranter(endpoints.getTokenServices(), clientDetailsService, endpoints.getOAuth2RequestFactory());
tokenGranters.add(implicit);
tokenGranters.add(new ClientCredentialsTokenGranter(endpoints.getTokenServices(), clientDetailsService, endpoints.getOAuth2RequestFactory()));
if (authenticationManager != null) {
    tokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, endpoints.getTokenServices(), clientDetailsService, endpoints.getOAuth2RequestFactory()));
}
tokenGranters.add(new SmsTokenGranter(authenticationManager, endpoints.getTokenServices(),
        clientDetailsService, endpoints.getOAuth2RequestFactory()));

endpoints.tokenGranter(new CompositeTokenGranter(tokenGranters));

以上代码中,就是将 Spring Security OAuth 内置的四种授权模式的 Granter 添加到一个列表中,再将我们刚才创建的 SmsTokenGranter 添加进去,最后在封装成一个 CompositeTokenGranter 并配置到 endpoints 中。

这里可以参考下之前的这篇文章:Spring Security OAuth 之 @EnableAuthorizationServer 干了啥?

然后,在 Spring Security 的配置类,也就是 WebSecurityConfigurerAdapter 的子类中,添加 SmsCodeAuthenticationProvider 的 Bean:

@Bean
public SmsCodeAuthenticationProvider smsCodeAuthenticationProvider() {
    SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
    smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
    return smsCodeAuthenticationProvider;
}

再在 configure(HttpSecurity http) 将其配置到 http 中:

http.authenticationProvider(smsCodeAuthenticationProvider())

其余的诸如发送验证码接口等内容,参考之前的文章。

测试