Spring Security如何优雅的增加OAuth2协议授权模式

99封情书 提交于 2020-11-03 07:50:52

一、什么是OAuth2协议?

OAuth 2.0 是一个关于授权的开放的网络协议,是目前最流行的授权机制。

数据的所有者告诉系统,同意授权第三方应用进入系统,获取这些数据。系统从而产生一个短期的进入令牌(token),用来代替密码,供第三方应用使用。

由于授权的场景众多,OAuth 2.0 协议定义了获取令牌的四种授权方式,分别是:

  • 「授权码模式」:授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与"服务提供商"的认证服务器进行互动。

  • 「简化模式」:简化模式(implicit grant type)不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了"授权码"这个步骤,因此得名。所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。

  • 「密码模式」:密码模式(Resource Owner Password Credentials Grant)中,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向"服务商提供商"索要授权。

  • 「客户端模式」:客户端模式(Client Credentials Grant)指客户端以自己的名义,而不是以用户的名义,向"服务提供商"进行认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求"服务提供商"提供服务,其实不存在授权问题。

四种授权模式分别使用不同的 grant_type 来区分

 

二、为什么要自定义授权类型?

虽然 OAuth2 协议定义了4种标准的授权模式,但是在实际开发过程中还是远远满足不了各种变态的业务场景,需要我们去扩展。

例如增加图形验证码、手机验证码、手机号密码登录等等的场景

 

而常见的做法都是通过增加 过滤器Filter 的方式来扩展 Spring Security 授权,但是这样的实现方式有两个问题:

  1. 脱离了 OAuth2 的管理

  2. 不灵活:例如系统使用 「密码模式」 授权,网页版需要增加图形验证码校验,但是手机端APP又不需要的情况下,使用增加 Filter 的方式去实现就比较麻烦了。

 

所以目前在 Spring Security 中比较优雅和灵活的扩展方式就是通过自定义 「grant_type」 来增加授权模式。

 

三、实现思路

在扩展之前首先需要先了解 Spring Security 的整个授权流程,我以 「密码模式」 为例去展开分析,如下图所示

3.1. 流程分析

整个授权流程关键点分为以下两个部分:

「第一部分」:关于授权类型 grant_type 的解析

  1. 每种 grant_type 都会有一个对应的 TokenGranter 实现类。

  2. 所有 TokenGranter 实现类都通过 CompositeTokenGranter 中的 tokenGranters 集合存起来。

  3. 然后通过判断 grantType 参数来定位具体使用那个 TokenGranter 实现类来处理授权。

 

「第二部分」:关于授权登录逻辑

  1. 每种 授权方式 都会有一个对应的 AuthenticationProvider 实现类来实现。

  2. 所有 AuthenticationProvider 实现类都通过 ProviderManager 中的 providers 集合存起来。

  3. TokenGranter 类会 new 一个 AuthenticationToken 实现类,如 UsernamePasswordAuthenticationToken 传给 ProviderManager 类。

  4.  ProviderManager 则通过 AuthenticationToken 来判断具体使用那个 AuthenticationProvider 实现类来处理授权。

  5. 具体的登录逻辑由 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

 

「场景二」:新加一种授权方式,如:手机号加密码登录。

该场景需要实现以下内容:

  1. 定义一个新的 grantType 类型,并新增对应的 TokenGranter 实现类加到 CompositeTokenGranter 中的 tokenGranters 集合里

  2. 新增一个 AuthenticationToken 实现类,用于存放该授权所需的信息。

  3. 新增一个 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<String, String> 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}

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!