spring securityJava web

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

2020-09-02  本文已影响0人  zlt2000
file

一、什么是OAuth2协议?

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

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

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

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

 

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

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

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

 

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

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

 

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

 

三、实现思路

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

file

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 集合里即可。

参考代码:PwdImgCodeGranter.java

 

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

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

  1. 定义一个新的 grantType 类型,并新增对应的 TokenGranter 实现类添加到 CompositeTokenGranter 中的 tokenGranters 集合里
  2. 新增一个 AuthenticationToken 实现类,用于存放该授权所需的信息。
  3. 新增一个 AuthenticationProvider 实现类 实现授权的逻辑,并重写 supports 方法绑定步骤二的 AuthenticationToken 实现类

参考代码: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_typemobile_password 进行授权获取 access_token

/oauth/token?grant_type=mobile_password&mobile={mobile}&password={password}

file

 

五、参考样例

详细的代码实现可以参考

https://gitee.com/zlt2000/microservices-platform/tree/master/zlt-uaa

 
扫码关注有惊喜!

file
上一篇下一篇

猜你喜欢

热点阅读