日常bb

日常bb

Spring Security OAuth2自定义授权模式

2022-10-06
Spring Security OAuth2自定义授权模式

Spring Security OAuth2自定义授权模式

在实际开发过程中Spring Security OAuth2默认提供的五种授权模式不够用的情况下,就需要我们自己来定义授权模式。

例如国内比较常见的应用场景:短信验证码授权登录。

SpringSecurity-demo Github地址

验证码登录

其实实现这个需求有多种实现方式:

第一种:写一个controller接收登录,逻辑通过则在controller中组装token

第二种:以Spring Security认证的思维,编写一个短信验证码的过滤器,配置在过滤器链上,加一个登录成功处理器,在登录成功后组装token

这两种写法都可以实现短信验证码登录的这个需求,但是都不太规范。

规范写法:按照Spring Security OAuth2框架的的设计思想来完成自定义授权模式。

Spring Security OAuth2的授权思想和Spring Security有点区别:授权认证的时机不同。

Spring Security OAuth2是在请求进入controller后得到请求参数,然后使用Spring Security OAuth2提供的一些内置组件完成token的生成,而Spring Security则是依赖过滤器链来实现的,Spring Security是在请求未到达controller时,被匹配的过滤器拦截,然后进行认证,生成token

实现思路

密码模式的授权流程:

密码模式的授权流程

重要知识点:

  • 每种授权类型都对应一个实现类TokenGranter
  • 所有TokenGranter实现类都通过CompositeTokenGranter中的tokenGranters集合存储
  • 通过判断grantType参数来定位具体使用哪个TokenGranter实现类来处理
  • 每种授权方式都对应一个AuthenticationProvider
  • TokenGranter类会new一个AuthenticationToken实现类传递给ProviderManager

所以自定义一个授权类型,必须构建自己的TokenGranterAuthenticationProviderAuthenticationToken

验证码模式

1、实现UserDetailsService

后面主要填充的逻辑就是根据phone查询用户信息。

@Service
public class SmsUserDetailsServiceImpl implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return null;
    }
}

2、自定义AuthenticationToken

SmsVerificationCodeAuthenticationToken

public class SmsVerificationCodeAuthenticationToken extends AbstractAuthenticationToken {
    private final Object principal;

    public SmsVerificationCodeAuthenticationToken(Object principal) {
        super(null);
        this.principal = principal;
        setAuthenticated(false);
    }


    public SmsVerificationCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        // 设置为已认证
        super.setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        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();
    }
}

3、自定义TokenGranter

SmsVerificationCodeTokenGranter

@Slf4j
@Slf4j
public class SmsVerificationCodeTokenGranter extends AbstractTokenGranter {

    /**
     * 授权类型,和password是一样的作用
     */
    private static final String grantType = "sms_code";

    private final AuthenticationManager authenticationManager;

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

    protected SmsVerificationCodeTokenGranter(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<>(tokenRequest.getRequestParameters());
        // 手机号
        String phone = parameters.get("phone");
        // 验证码
        String verificationCode = parameters.get("verification_code");
        log.info("[登录请求 phone<{}> verificationCode<{}>]", phone, verificationCode);
        
        Authentication userAuth = new SmsVerificationCodeAuthenticationToken(parameters);
        ((AbstractAuthenticationToken) userAuth).setDetails(parameters);
        try {
            // 调用authenticationManager进行认证,会根据SmsVerificationCodeAuthenticationToken找到对应的Provider
            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,phone number is: " + phone);
        }
        OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
        return new OAuth2Authentication(storedOAuth2Request, userAuth);
    }
}

4、自定义AuthenticationProvider

这个类就是真正的处理类,经过TokenGranter后,会找到对应的AuthenticationProvider

SmsVerificationCodeAuthenticationProvider

@Slf4j
public class SmsVerificationCodeAuthenticationProvider implements AuthenticationProvider {

    /**
     * 得到UserDetailsService对象用来获取用户信息
     */
    private UserDetailsService userDetailsService;

    public SmsVerificationCodeAuthenticationProvider(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    /**
     * 过请求参数查询数据库用户信息,进行匹配
     *
     * @param authentication the authentication request object.
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsVerificationCodeAuthenticationToken authenticationToken = (SmsVerificationCodeAuthenticationToken) authentication;
        Map<String, String> parameters = (Map<String, String>) authenticationToken.getPrincipal();
        log.info("[parameters <{}>]", parameters);
        // 手机号
        String phone = parameters.get("phone");
        // 验证码
        String verificationCode = parameters.get("verification_code");
        log.info("[登录请求 phone<{}> verificationCode<{}>]", phone, verificationCode);
        // 1.根据phone查询用户信息是否存在
        // 2.判断验证码 是否有效

        UserDetails user = new User("rcbb", "rcbb", true, true, true, true,
                AuthorityUtils.commaSeparatedStringToAuthorityList("user_add,user_update,ROLE_ADMIN"));

        SmsVerificationCodeAuthenticationToken smsVerificationCodeAuthenticationToken = new SmsVerificationCodeAuthenticationToken(user, user.getAuthorities());
        smsVerificationCodeAuthenticationToken.setDetails(authenticationToken.getDetails());
        return smsVerificationCodeAuthenticationToken;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        // 判断authentication是否是SmsCodeAuthenticationToken类型
        return SmsVerificationCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

5、将自定义的Provider注入容器

SmsVerificationCodeAuthenticationConfig

@Component
public class SmsVerificationCodeAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    public void configure(HttpSecurity builder) throws Exception {
        SmsVerificationCodeAuthenticationProvider provider = new SmsVerificationCodeAuthenticationProvider(userDetailsService);
        builder.authenticationProvider(provider);
    }
}

WebSecurityConfig中,注入自定义的授权配置类。

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    // 短信安全验证配置
    @Autowired
    private SmsVerificationCodeAuthenticationConfig smsVerificationCodeAuthenticationConfig;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.apply(smsVerificationCodeAuthenticationConfig);
    }
    
    ...
}

6、加到CompositeTokenGranter集合中

clients中的authorizedGrantTypes允许的授权类型添加sms_code

将自定义的授权类型加到集合CompositeTokenGranter中。

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

        // 基于内存便于测试,使用in-memory存储
        clients.inMemory()
                // client_id
                .withClient("rcbb")
                // 未加密
                //.secret("secret")
                // 加密
                .secret(passwordEncoder.encode("rcbb"))
                // 资源列表
                //.resourceIds("res1")
                // 该client允许的授权类型authorization_code,password,refresh_token,implicit,client_credentials
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token","sms_code")
                // 允许的授权范围
                .scopes("all", "ROLE_ADMIN", "ROLE_USER")
                // false跳转到授权页面
                //.autoApprove(false)
                //加上验证回调地址
                .redirectUris("http://rcbb.cc");
    }
    
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        List<TokenGranter> tokenGranters = new ArrayList<>(Collections.singleton(endpoints.getTokenGranter()));
        tokenGranters.add(new SmsVerificationCodeTokenGranter(authenticationManager, tokenService(), clientDetailsService, new DefaultOAuth2RequestFactory(clientDetailsService)));

        // 配置认证管理器
        endpoints.authenticationManager(authenticationManager)
                .tokenServices(tokenService())
                .tokenGranter(new CompositeTokenGranter(tokenGranters));
    }
}

7、测试

使用自定义grant_type=sms_code

测试

该请求curl

curl --location --request POST 'http://127.0.0.1:3000/oauth/token' \
--header 'Authorization: Basic cmNiYjpyY2Ji' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=sms_code' \
--data-urlencode 'phone=13838384388' \
--data-urlencode 'verification_code=123456'

上面例子仅为梳理整个流程,还缺少业务逻辑上数据的校验。