飞道的博客

SpringSecurityOauth2自定义授权模式

274人阅读  评论(0)

前言

在我们实际开发过程中SpringSecurityOAuth2默认提供的5种授权模式不够用,那么就需要我们自己来定义授权模式,可能有人不是真的很了解SpringSecurityOauth2框架的使用,比如我们要开发一种短信验证码授权登录的场景,可能有些程序员直接就编写一个controller然后自己在controller中组装token,或者是沿用SpringSecurity的过滤器思维,编写一个短信验证么的过滤器,配置在过滤器链上,按照SpringSecurity认证的思维,加上一个登录成功处理器,在登录成功处理器生成Token,这两种写法其实不是很正规,正规的写法应该是按照SpringSecurityOAuth2框架的设计思想来完成自定义授权模式。SpringSecurityOAuth2的授权思想和SpringSecurity有点区别,区别就再授权认证的时机不同,SpringSecurityOAuth2是在请求进入controller后得到请求参数,然后很久SpringSecurityOAuth2提供的一些内置组件完成Token的生成,而SpringSecurity则是依赖过滤器链来实现的,SpringSecurity是在请求未到达controller时,被匹配的过滤器拦截,然后进行认证,生成Token的,所以这是两种框架设计实现,不过SpringSecurityOAuth沿用了SpringSecurity的认证流程,注意二者是认证时机不同!

在写SpringSecurityOAuth2自定义授权模式的时候,建议先查看这几篇文章SpringSecurityOAuth2授权流程源码分析SpringSecurityOAuth2授权流程加载源码分析,不然涉及到授权模式中一些概念,以及一些组件就有点搞不清楚了,本文将以最常见的短信登录这种模式,完成短信验证码自定义授权模式

编码

当我们需要自定义授权模式之前,请先完成SpringSecurityOAuth2默认的几种模式。也就是配置好AuthorizationServerConfig、WebSecurityConfig,确保SpringSecurityOAuth2默认几种模式能跑起来。

1.短信登录用户验证信息封装类

/**
 * @author TAO
 * @description: 短信登录用户验证信息封装类
 * @date 2021/4/17 17:01
 */
@Slf4j
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();
    }
}

2.短信验证码校验逻辑的类


/**
 * @author TAO
 * @description: 提供短信验证码校验逻辑的类
 * @date 2021/4/17 17:02
 */
@Slf4j
public class SmsVerificationCodeAuthenticationProvider implements AuthenticationProvider {
   

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

    public void setUserDetailsService(UserDetailsService userDetailsService) {
   
        this.userDetailsService = userDetailsService;
    }
	
	//此方法就是通过请求参数查询数据库用户信息,进行匹配
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
   
        SmsVerificationCodeAuthenticationToken authenticationToken = (SmsVerificationCodeAuthenticationToken) authentication;
        log.info("进行短信身份验证的逻辑");
        log.info("(JSONObject) authenticationToken.getPrincipal()===>" + authenticationToken.getPrincipal());
        UserDetails user = new YYExpandUser(2, 3, "YYYYYY", "111111111", true, true, true, true, AuthorityUtils.commaSeparatedStringToAuthorityList("sys_menu_edit"));

        /*if( authenticationToken.getPrincipal().toString().equals("100")) {
            throw new InternalAuthenticationServiceException("无法获取用户信息");
        }
*/
        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);
    }
}

3.短信验证么登录复合配置

@Component
public class SmsVerificationCodeAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
   

    @Autowired
    protected AuthenticationSuccessHandler successAuthenticationHandler;//表单登录成功处理器

    @Autowired
    protected AuthenticationFailureHandler failureAuthenticationHandler;//表单登录失败处理器

    @Autowired
    private UserDetailsService userDetailsService;//又来得到用户信息的

    @Override
    public void configure(HttpSecurity http) {
   
        SmsVerificationCodeAuthenticationFilter smsVerificationCodeAuthenticationFilter=new SmsVerificationCodeAuthenticationFilter();
        smsVerificationCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));//得到认证管理器
        smsVerificationCodeAuthenticationFilter.setAuthenticationSuccessHandler(successAuthenticationHandler);
        smsVerificationCodeAuthenticationFilter.setAuthenticationFailureHandler(failureAuthenticationHandler);

        SmsVerificationCodeAuthenticationProvider smsVerificationCodeAuthenticationProvider=new SmsVerificationCodeAuthenticationProvider();
        smsVerificationCodeAuthenticationProvider.setUserDetailsService(userDetailsService);

        http
        		/**
                 * 将短信验证码校验Provider绑定到authenticationProvider中,就是为了在
                 * SpringSecurity的SmsVerificationCodeAuthenticationFilter中return this.getAuthenticationManager().authenticate(authRequest);处找到当前过滤器的Provider
                 * SpringSecurityOAuth2的SmsVerificationCodeTokenGranter中userAuth = authenticationManager.authenticate(userAuth);处找到当前授权策略的Provider
                 */
                .authenticationProvider(smsVerificationCodeAuthenticationProvider)
                .addFilterAfter(smsVerificationCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

    }
}

上面两个是不是很熟悉,对没错,就和SpringSecurity中的写法一样,文章开头前言部分已经提到过了,认证流程是一样的,所以和SpringSecurity同样需要SmsVerificationCodeAuthenticationTokenSmsVerificationCodeAuthenticationProviderSmsVerificationCodeAuthenticationConfig,我这里的SmsVerificationCodeAuthenticationConfig配置类中是有SmsVerificationCodeAuthenticationFilter ,你们写的时候可以完全去掉,我这里是故意没去掉的,就是为了给你们体现SpringSecurityOAuth2和SpringSecurity的认证逻辑是一样的,所以我这里才没有去掉!当然不去掉也没关系,这里如果去掉是可以不用编写SmsVerificationCodeAuthenticationFilter这个玩意的。

4.SecurityConfig配置挂载一下

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
   
	
	@Autowired
    private SmsVerificationCodeAuthenticationConfig smsVerificationCodeAuthenticationConfig;//短信安全验证配置

	@Override
    protected void configure(HttpSecurity http) throws Exception {
   
    	 .apply(smsVerificationCodeAuthenticationConfig);//关键代码
	}
	
}

以上4步操作和SpringSecurity配置差不多,等下有个报错,下面会演示,没别的意思,就是让各位知道每个配置是干嘛的!

5.短信验证码授权


/**
 * @author TAO
 * @description: 短信验证码授权
 * 实际上就是复制ResourceOwnerPasswordTokenGranter中的代码,进行getOAuth2Authentication方法部分修改,
 * 这个类有点类似SpringSecurity中的各种AuthenticationFilter,也就是类似过滤器的作用
 * @date 2021/4/16 21:28
 */
@Slf4j
public class SmsVerificationCodeTokenGranter extends AbstractTokenGranter {
   

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

    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<String, String>(tokenRequest.getRequestParameters());//得到请求参数
        String phone = parameters.get(SecurityConstants.PHONE_PARAMETER);//得到手机号
        String verification_code = parameters.get(SecurityConstants.VERIFICATION_CODE);//得到手机验证码
        String client_id = parameters.get(SecurityConstants.CLIENT_ID);//得到client_id
        String client_secret = parameters.get(SecurityConstants.CLIENT_SECRET);//得到client_secret
        log.info("phone===>" + phone);
        log.info("verification_code===>" + verification_code);
        log.info("client_id===>" + client_id);
        log.info("client_secret===>" + client_secret);
        Authentication userAuth= new SmsVerificationCodeAuthenticationToken(parameters);
        ((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,phone number is: " + phone);
        }
        OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
        return new OAuth2Authentication(storedOAuth2Request, userAuth);
    }

}

这个的作用有点类似SpringSecurity中的SmsVerificationCodeAuthenticationFilterSmsVerificationCodeAuthenticationFilter说白了就是根据配置好要拦截的请求URL进行拦截,然后引导到对应的检验用户身份的SmsVerificationCodeAuthenticationProvider上,那么SmsVerificationCodeTokenGranter 其实就是SpringSecurityOAuth2中通过授权模式的不同将请求中携带的grant_type引导到对应的SmsVerificationCodeAuthenticationProvider中的作用

6.授权模式配置类

/**
 * @author TAO
 * @description: token授权模式配置类
 * 之所以下面这么多配置是因为AuthorizationServerEndpointsConfigurer为final类,无法继承,所以只能copy一些方法,保证流程走通
 * @date 2021/4/16 21:30
 */
@Configuration
public class TokenGranterConfig {
   

    @Autowired
    private ClientDetailsService clientDetailsService;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private TokenStore redisTokenStore;

    @Autowired
    private TokenEnhancer jwtTokenEnhancer;

    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;

    private AuthorizationCodeServices authorizationCodeServices;

    private boolean reuseRefreshToken = true;

    private AuthorizationServerTokenServices tokenServices;

    private TokenGranter tokenGranter;

    //默认写法
    @Bean
    public TokenGranter tokenGranter() {
   
        if (tokenGranter == null) {
   
            tokenGranter = new TokenGranter() {
   
                private CompositeTokenGranter delegate;
                @Override
                public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
   
                    if (delegate == null) {
   
                        delegate = new CompositeTokenGranter(getAllTokenGranters());
                    }
                    return delegate.grant(grantType, tokenRequest);
                }
            };
        }
        return tokenGranter;
    }

    //获取SpringSecurityOAuth2默认提供的5种授权模式+自定义模式
    private List<TokenGranter> getAllTokenGranters() {
   
        AuthorizationServerTokenServices tokenServices = tokenServices();
        AuthorizationCodeServices authorizationCodeServices = authorizationCodeServices();
        OAuth2RequestFactory requestFactory = requestFactory();
        List<TokenGranter> tokenGranters = getDefaultTokenGranters(tokenServices, authorizationCodeServices, requestFactory);//获取SpringSecurityOAuth2默认提供的5种授权模式
        if (authenticationManager != null) {
   //自定义授权模式
            // 添加自定义授权模式(实际是密码模式的复制)
            tokenGranters.add(new SmsVerificationCodeTokenGranter(authenticationManager, tokenServices, clientDetailsService, requestFactory));
        }
        return tokenGranters;
    }

    /**
     * 默认的授权模式
     */
    private List<TokenGranter> getDefaultTokenGranters(AuthorizationServerTokenServices tokenServices
            , AuthorizationCodeServices authorizationCodeServices, OAuth2RequestFactory requestFactory) {
   
        List<TokenGranter> tokenGranters = new ArrayList<>();
        // 添加授权码模式
        tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetailsService, requestFactory));
        // 添加刷新令牌的模式
        tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetailsService, requestFactory));
        // 添加隐士授权模式
        tokenGranters.add(new ImplicitTokenGranter(tokenServices, clientDetailsService, requestFactory));
        // 添加客户端模式
        tokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetailsService, requestFactory));
        if (authenticationManager != null) {
   
            // 添加密码模式
            tokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices, clientDetailsService, requestFactory));
        }
        return tokenGranters;
    }


    //默认写法
    private AuthorizationServerTokenServices tokenServices() {
   
        if (tokenServices != null) {
   
            return tokenServices;
        }
        this.tokenServices = createDefaultTokenServices();
        return tokenServices;
    }

    //默认写法
    private AuthorizationCodeServices authorizationCodeServices() {
   
        if (authorizationCodeServices == null) {
   
            authorizationCodeServices = new InMemoryAuthorizationCodeServices();
        }
        return authorizationCodeServices;
    }

    //默认写法
    private OAuth2RequestFactory requestFactory() {
   
        return new DefaultOAuth2RequestFactory(clientDetailsService);
    }

    //默认写法+JWT生成token、采用自定义JWT模板
    private DefaultTokenServices createDefaultTokenServices() {
   
        //使用JWT生成Token,设置自定义JWT模板
        List<TokenEnhancer> enhancers = new ArrayList<>();
        enhancers.add(jwtTokenEnhancer);
        enhancers.add(jwtAccessTokenConverter);
        TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
        enhancerChain.setTokenEnhancers(enhancers);

        //默认写法
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(redisTokenStore);
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setReuseRefreshToken(reuseRefreshToken);
        tokenServices.setClientDetailsService(clientDetailsService);
        tokenServices.setTokenEnhancer(enhancerChain);
        addUserDetailsService(tokenServices, this.userDetailsService);
        return tokenServices;
    }

    //默认写法
    private void addUserDetailsService(DefaultTokenServices tokenServices, UserDetailsService userDetailsService) {
   
        if (userDetailsService != null) {
   
            PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
            provider.setPreAuthenticatedUserDetailsService(new UserDetailsByNameServiceWrapper<PreAuthenticatedAuthenticationToken>(userDetailsService));
            tokenServices.setAuthenticationManager(new ProviderManager(Arrays.<AuthenticationProvider>asList(provider)));
        }
    }

}

其中标注默认写法的直接从AuthorizationServerEndpointsConfiguration类中copy出来,不用更改,但是最好知道是干什么用的。这里我就不过多解释了。不懂的可以自己调试或者看我往期文章,这里有两个点需要注意如下代码

//默认写法+JWT生成token、采用自定义JWT模板
    private DefaultTokenServices createDefaultTokenServices() {
   
        //使用JWT生成Token,设置自定义JWT模板
        List<TokenEnhancer> enhancers = new ArrayList<>();
        enhancers.add(jwtTokenEnhancer);
        enhancers.add(jwtAccessTokenConverter);
        TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
        enhancerChain.setTokenEnhancers(enhancers);

        //默认写法
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(redisTokenStore);
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setReuseRefreshToken(reuseRefreshToken);
        tokenServices.setClientDetailsService(clientDetailsService);
        tokenServices.setTokenEnhancer(enhancerChain);
        addUserDetailsService(tokenServices, this.userDetailsService);
        return tokenServices;
    }

我这里是在默认写法前面加了enhancers ,目的就是为了使用JWT+JWT自定义模板,另一个如下

//获取SpringSecurityOAuth2默认提供的5种授权模式+自定义模式
    private List<TokenGranter> getAllTokenGranters() {
   
        AuthorizationServerTokenServices tokenServices = tokenServices();
        AuthorizationCodeServices authorizationCodeServices = authorizationCodeServices();
        OAuth2RequestFactory requestFactory = requestFactory();
        List<TokenGranter> tokenGranters = getDefaultTokenGranters(tokenServices, authorizationCodeServices, requestFactory);//获取SpringSecurityOAuth2默认提供的5种授权模式
        if (authenticationManager != null) {
   //自定义授权模式
            // 添加自定义授权模式(实际是密码模式的复制)
            tokenGranters.add(new SmsVerificationCodeTokenGranter(authenticationManager, tokenServices, clientDetailsService, requestFactory));
        }
        return tokenGranters;
    }

这里就是将我们自定义的短信验证码授权模式加入到SpringSecurityOAuth2中的

7.认证授权服务配置
这个配置和基本配置差不多只是多了个TokenGranter的配置

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
   

 	@Autowired
    private TokenGranter tokenGranter;
    
    @Primary
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
   
        endpoints
                .authenticationManager(authenticationManager)//开启密码模式认证
                .tokenGranter(tokenGranter)
                .pathMapping("/oauth/confirm_access", "/custom/confirm_access")//替换/oauth/confirm_access为/custom/confirm_access
        ;
    }

}

启动测试
默认密码模式

自定义短信验证码模式

如果有碰到No AuthenticationProvider found for…报错那么请见SpringSecurityOAuth2自定义授权模式Handling error: ProviderNotFoundException, No AuthenticationProvider foun


转载:https://blog.csdn.net/CSDN877425287/article/details/115798342
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场