Spring Security如何优雅的增加OAuth2协议授权模式
转自:陶陶技术笔记,作者:zlt2000
一、什么是OAuth2协议?
OAuth 2.0 是一个关于授权的开放的网络协议,是目前最流行的授权机制。
数据的所有者告诉系统,同意授权第三方应用进入系统,获取这些数据。系统从而产生一个短期的进入令牌(token),用来代替密码,供第三方应用使用。
由于授权的场景众多,OAuth 2.0 协议定义了获取令牌的四种授权方式,分别是:
「授权码模式」:授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与"服务提供商"的认证服务器进行互动。
「简化模式」:简化模式(implicit grant type)不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了"授权码"这个步骤,因此得名。所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。
「密码模式」:密码模式(Resource Owner Password Credentials Grant)中,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向"服务商提供商"索要授权。
「客户端模式」:客户端模式(Client Credentials Grant)指客户端以自己的名义,而不是以用户的名义,向"服务提供商"进行认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求"服务提供商"提供服务,其实不存在授权问题。
四种授权模式分别使用不同的
grant_type来区分
友情推荐下欢哥的开源项目:https://github.com/yinjihuan/kitty
Spring Cloud & Spring Cloud Alibaba 基础框架,内置了 Cat 监控,互联网公司落地 Spring Cloud 架构必备。
二、为什么要自定义授权类型? 
虽然 OAuth2 协议定义了4种标准的授权模式,但是在实际开发过程中还是远远满足不了各种变态的业务场景,需要我们去扩展。
例如增加图形验证码、手机验证码、手机号密码登录等等的场景
而常见的做法都是通过增加 过滤器Filter 的方式来扩展 Spring Security 授权,但是这样的实现方式有两个问题:
脱离了 OAuth2的管理不灵活:例如系统使用 「密码模式」 授权,网页版需要增加图形验证码校验,但是手机端APP又不需要的情况下,使用增加 Filter的方式去实现就比较麻烦了。
所以目前在 Spring Security 中比较优雅和灵活的扩展方式就是通过自定义 「grant_type」 来增加授权模式。
三、实现思路
在扩展之前首先需要先了解 Spring Security 的整个授权流程,我以 「密码模式」 为例去展开分析,如下图所示

3.1. 流程分析
整个授权流程关键点分为以下两个部分:
「第一部分」:关于授权类型 grant_type 的解析
每种 grant_type都会有一个对应的TokenGranter实现类。所有 TokenGranter实现类都通过CompositeTokenGranter中的tokenGranters集合存起来。然后通过判断 grantType参数来定位具体使用那个TokenGranter实现类来处理授权。
「第二部分」:关于授权登录逻辑
每种 授权方式都会有一个对应的AuthenticationProvider实现类来实现。所有 AuthenticationProvider实现类都通过ProviderManager中的providers集合存起来。TokenGranter类会 new 一个AuthenticationToken实现类,如UsernamePasswordAuthenticationToken传给ProviderManager类。而 ProviderManager则通过AuthenticationToken来判断具体使用那个AuthenticationProvider实现类来处理授权。具体的登录逻辑由 AuthenticationProvider实现类来实现,如DaoAuthenticationProvider。
3.2. 扩展分析
根据上面的流程,扩展分为以下两种场景
「场景一」:只对原有的授权逻辑进行增强或者扩展,如:用户名密码登录前增加图形验证码校验。
该场景需要定义一个新的 grantType 类型,并新增对应的 TokenGranter 实现类 「添加扩展内容」,然后加到 CompositeTokenGranter 中的 tokenGranters 集合里即可。
参考代码:https://gitee.com/zlt2000/microservices-platform/blob/master/zlt-uaa/src/main/java/com/central/oauth/granter/PwdImgCodeGranter.java
「场景二」:新加一种授权方式,如:手机号加密码登录。
该场景需要实现以下内容:
定义一个新的 grantType类型,并新增对应的TokenGranter实现类加到CompositeTokenGranter中的tokenGranters集合里新增一个 AuthenticationToken实现类,用于存放该授权所需的信息。新增一个 AuthenticationProvider实现类 「实现授权的逻辑」,并重写supports方法绑定步骤二的AuthenticationToken实现类
参考代码:https://gitee.com/zlt2000/microservices-platform/blob/master/zlt-uaa/src/main/java/com/central/oauth/granter/MobilePwdGranter.java
四、代码实现
下面以「场景二」新增手机号加密码授权方式为例,展示核心的代码实现
4.1. 创建 AuthenticationToken
创建 MobileAuthenticationToken 类,用于存储手机号和密码信息
public class MobileAuthenticationToken extends AbstractAuthenticationToken {
 private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
 private final Object principal;
 private Object credentials;
 public MobileAuthenticationToken(String mobile, String password) {
  super(null);
  this.principal = mobile;
  this.credentials = password;
  setAuthenticated(false);
 }
 public MobileAuthenticationToken(Object principal, Object credentials,
          Collection extends GrantedAuthority> authorities) {
  super(authorities);
  this.principal = principal;
  this.credentials = credentials;
  super.setAuthenticated(true);
 }
 @Override
 public Object getCredentials() {
  return this.credentials;
 }
 @Override
 public Object getPrincipal() {
  return this.principal;
 }
 @Override
 public void setAuthenticated(boolean isAuthenticated) {
  if (isAuthenticated) {
   throw new IllegalArgumentException(
     "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
  }
  super.setAuthenticated(false);
 }
 @Override
 public void eraseCredentials() {
  super.eraseCredentials();
 }
}
4.2. 创建 AuthenticationProvider
创建 MobileAuthenticationProvider 类,实现登录逻辑,并绑定 MobileAuthenticationToken 类
@Setter
public class MobileAuthenticationProvider implements AuthenticationProvider {
    private ZltUserDetailsService userDetailsService;
    private PasswordEncoder passwordEncoder;
    @Override
    public Authentication authenticate(Authentication authentication) {
        MobileAuthenticationToken authenticationToken = (MobileAuthenticationToken) authentication;
        String mobile = (String) authenticationToken.getPrincipal();
        String password = (String) authenticationToken.getCredentials();
        UserDetails user = userDetailsService.loadUserByMobile(mobile);
        if (user == null) {
            throw new InternalAuthenticationServiceException("手机号或密码错误");
        }
        if (!passwordEncoder.matches(password, user.getPassword())) {
            throw new BadCredentialsException("手机号或密码错误");
        }
        MobileAuthenticationToken authenticationResult = new MobileAuthenticationToken(user, password, user.getAuthorities());
        authenticationResult.setDetails(authenticationToken.getDetails());
        return authenticationResult;
    }
    @Override
    public boolean supports(Class> authentication) {
        return MobileAuthenticationToken.class.isAssignableFrom(authentication);
    }
}
4.3. 创建 TokenGranter
创建 MobilePwdGranter 类并定义 grant_type 的值为 mobile_password
public class MobilePwdGranter extends AbstractTokenGranter {
    private static final String GRANT_TYPE = "mobile_password";
    private final AuthenticationManager authenticationManager;
    public MobilePwdGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices
            , ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) {
        super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
        this.authenticationManager = authenticationManager;
    }
    @Override
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
        Map parameters = new LinkedHashMap<>(tokenRequest.getRequestParameters());
        String mobile = parameters.get("mobile");
        String password = parameters.get("password");
        parameters.remove("password");
        Authentication userAuth = new MobileAuthenticationToken(mobile, password);
        ((AbstractAuthenticationToken) userAuth).setDetails(parameters);
        userAuth = authenticationManager.authenticate(userAuth);
        if (userAuth == null || !userAuth.isAuthenticated()) {
            throw new InvalidGrantException("Could not authenticate mobile: " + mobile);
        }
        OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
        return new OAuth2Authentication(storedOAuth2Request, userAuth);
    }
}
 
4.4. 加到 CompositeTokenGranter 中的集合里
// 添加手机号加密码授权模式
tokenGranters.add(new MobilePwdGranter(authenticationManager, tokenServices, clientDetailsService, requestFactory));
4.5. 测试
使用以下地址,指定 grant_type 为 mobile_password 进行授权,获取 「access_token」
/oauth/token?grant_type=mobile_password&mobile={mobile}&password={password}

五、参考样例
详细的代码实现可以参考
https://gitee.com/zlt2000/microservices-platform/tree/master/zlt-uaa
我整理了一份很全的学习资料,感兴趣的可以微信搜索「猿天地」,回复关键字 「学习资料」获取我整理好了的 Spring Cloud,Spring Cloud Alibaba,Sharding-JDBC 分库分表,任务调度框架 XXL-JOB,MongoDB,爬虫等相关资料。
后台回复 学习资料 领取学习视频
如有收获,点个在看,诚挚感谢
