分布式日志(三)OAuth2安全控制
ShiJh Lv3

分布式日志第三篇,基于SpringSecurity的分布式系统的安全管理方案

分布式安全协议

微服务架构下的安全方案有许多种,如

  1. 共享Session (最容易实现)
  2. Token凭证 (最高可用性)
  3. SSO单点登录 (折中方案)

共享Session

由认证服务器认证后将本地Session保存在Redis中,其他服务器读取Redis中的Session对请求进行身份认证,最大的优点是不需要多余的代码,可以像开发单体应用一样开发安全管理系统

缺点:Redis服务或认证服务不可用时 将无法访问所有的资源服务器。

基于OAuth2协议的Token认证方案

Token也分为两种模式:

  1. 短Token

    认证服务器颁发access_token,与之对应的是储存在Redis或数据库中的权限信息,资源服务通过此凭证向认证服务器请求认证,从而获取用户的权限信息

  2. 长Token(JWT)

    认证服务器直接颁发包含权限信息的Token,使用JWT的形式做签名加密,资源服务器可直接验证用户身份,不需要再次与认证服务器交互(除了使用非对称加密需要请求公钥)

短Token的优缺点:

  • 字段较短,网络传输更快
  • 回收Token更容易,只需对数据库或Redis进行删除
  • 依赖于认证服务,认证服务器不可用时 其他服务将无法访问
  • 每次请求都需要与认证服务器交互 增加了网络通信代价

JWT优缺点:

  • Token可本地解析,获得Token后不再依赖认证服务器,具有高可用性
  • 本地解析速度更快,没有网络通信代价
  • 字段较长,会增加网络传输时间
  • 回收Token困难 可能造成一定程度上的安全问题

SpringSecurity构建认证服务器

SpringSecurityOAuth2认证流程为:

TokenEndPoint->TokenGranter->AuthenticationManager->ProviderManager->Provider

  • TokenEndPoint

    其实就是一个内置的 Controller ,用于接收认证请求,内置异常处理,如果想要使用自己的异常处理,建议重写此类。

  • TokenGranter

    一个验证方式管理器,根据 grant_type 选择验证方式,每个验证方式对应一个TokenGranter,但是我们需要多种验证方式,于是乎SpringSecurity内部采用了CompositeTokenGranter(可以放入多个TokenGranter,并根据grant_type选择)并包装成委托对象。如果想要自己添加新的认证方式,需要自己写该委托类并进行配置,可以参考AutoConfiguration进行编写和配置

    public class CustomTokenGranterDelegator implements TokenGranter {
        private CompositeTokenGranter delegate;
        private AuthenticationManager authenticationManager;
        private OAuth2RequestFactory OAuth2RequestFactory;
        private AuthorizationCodeServices authorizationCodeServices;
        private AuthorizationServerTokenServices tokenServices;
        private ClientDetailsService clientDetailsService;
        //可以通过该属性在配置文件中添加新的TokenGranter
        private Collection<TokenGranter> additionalTokenGranters;
    
        public CustomTokenGranterDelegator(AuthenticationManager authenticationManager, OAuth2RequestFactory OAuth2RequestFactory, AuthorizationCodeServices authorizationCodeServices, AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService) {
            this.authenticationManager = authenticationManager;
            this.OAuth2RequestFactory = OAuth2RequestFactory;
            this.authorizationCodeServices = authorizationCodeServices;
            this.tokenServices = tokenServices;
            this.clientDetailsService = clientDetailsService;
            this.additionalTokenGranters = new ArrayList<>(3);
        }
    
        @Override
        public OAuth2AccessToken grant(String s, TokenRequest tokenRequest) {
            if (this.delegate == null) {
                this.delegate = new CompositeTokenGranter(this.getDefaultTokenGranters());
            }
            return this.delegate.grant(s, tokenRequest);
        }
    
        public void addTokenGranter(TokenGranter tokenGranter) {
            this.additionalTokenGranters.add(tokenGranter);
        }
    
        private List<TokenGranter> getDefaultTokenGranters() {
            ClientDetailsService clientDetails = getClientDetailsService();
            AuthorizationServerTokenServices tokenServices = getTokenServices();
            AuthorizationCodeServices authorizationCodeServices = getAuthorizationCodeServices();
            OAuth2RequestFactory requestFactory = getOAuth2RequestFactory();
            List<TokenGranter> tokenGranters = new ArrayList<>(this.additionalTokenGranters);
            tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetails, requestFactory));
            tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetails, requestFactory));
            ImplicitTokenGranter implicit = new ImplicitTokenGranter(tokenServices, clientDetails, requestFactory);
            tokenGranters.add(implicit);
            tokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetails, requestFactory));
            if (getAuthenticationManager() != null) {
                tokenGranters.add(new ResourceOwnerPasswordTokenGranter(getAuthenticationManager(), tokenServices, clientDetails, requestFactory));
                //也可以直接写在这个方法内 不推荐
                tokenGranters.add(new ResourceOwnerPhoneTokenGranter(getAuthenticationManager(), tokenServices, clientDetails, requestFactory));
            }
    
            return tokenGranters;
        }
        //... getter setter
    }
  • AuthenticationManager

    委托Provider对参数进行验证,本身用于执行验证前后的一些预处理

  • ProviderManager

    用于选择合适的Provider进行验证,并举行投票机制

  • Provider

    对具体的参数进行验证,需要自定义时请重写此类

    由于内部的AbstractUserDetailsAuthenticationProvider把Token类型写死了为UsernamePasswordToken,个人认为是非常不明智的设定,简直不把拓展考虑进来,所以可以复制其代码并改为泛型类作为以后拓展的通用继承类 参考

还有一些基本的组件 ClientDetailsService UserDetailsService是必备的拓展点 也很简单。

自用配置文件参考(JWT版)

@Configuration
@EnableAuthorizationServer
public class AuthorizationConfig extends AuthorizationServerConfigurerAdapter {
    @Autowired
    @Qualifier("myAuthorizationTS")
    private AuthorizationServerTokenServices tokenServices;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    @Qualifier("myUserDetailService")
    private UserDetailsService userDetailsService;

    @Autowired
    @Qualifier("myClientDetailsService")
    private ClientDetailsService clientDetailsService;

    //授权码认证模式的拓展点 一般来说直接由内存记录就够了 不需要持久化
    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        return new InMemoryAuthorizationCodeServices();
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.checkTokenAccess("permitAll()")
                .tokenKeyAccess("isAuthenticated()");
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientDetailsService)
                .build();
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        Function<AuthorizationServerEndpointsConfigurer, TokenGranter> customTokenGranter = (config)-> new CustomTokenGranterDelegator(
                authenticationManager,
                config.getOAuth2RequestFactory(),
                authorizationCodeServices(),
                tokenServices,
                clientDetailsService
        );
        endpoints.userDetailsService(userDetailsService)
                .tokenGranter(customTokenGranter.apply(endpoints));
    }
}
@Configuration
public class TokenConfig {

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("test_sign");
        return converter;
    }

    @Bean("myTokenStore")
    public TokenStore myTokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }
	
    //由TokenGranter调用的服务 用于生成Token 也可以认证token
    @Bean("myAuthorizationTS")
    public AuthorizationServerTokenServices tokenServices(ClientDetailsService clientDetailsService) {
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setClientDetailsService(clientDetailsService);
        tokenServices.setTokenStore(myTokenStore());
        tokenServices.setTokenEnhancer(jwtAccessTokenConverter());
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setAccessTokenValiditySeconds(60 * 60 * 2);
        tokenServices.setRefreshTokenValiditySeconds(60 * 60 * 24 * 3);
        return tokenServices;
    }
}
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    @Qualifier("myUserDetailService")
    private UserDetailsService userDetailsService;

    @Autowired
    private RedisTemplate<Object,Object> redisTemplate;

    @Autowired
    private CacheManager cacheManager;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    //AuthenticationManager 必须使用该方式注入
    @Bean
    public AuthenticationManager authenticationManagerProvider() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public UserCache userCache() {
        return new SpringCacheBasedUserCache(cacheManager.getCache("security:user:cache"));
    }
	
    //可对Provider添加缓存器 优化性能
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        RedisPhoneAuthProvider phoneAuthProvider = new RedisPhoneAuthProvider(redisTemplate, s -> s.concat(":security:phone:code"));
        phoneAuthProvider.setUserCache(userCache());
        phoneAuthProvider.setUserDetailsService(userDetailsService);
        auth.userDetailsService(userDetailsService)
                .and()
                .authenticationProvider(phoneAuthProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().cors().disable()
                .antMatcher("/oauth/**")
                .authorizeRequests()
                .antMatchers("/oauth/**").permitAll();
    }
}

SpringSecurity构建资源服务器

鲁迅说过,所有的微服务都是资源服务器

资源服务器主要功能只有从access_token中获取用户信息 配置起来简单得多

@Configuration
@EnableResourceServer
//开启方法级别权限控制
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private AccessDeniedHandler securityAccessDeniedHandler;

    //配置当前资源服务器的ID
    private static final String RESOURCE_ID = "test";

    /**当前资源服务器的一些配置, 如资源服务器ID **/
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception{
        // 配置当前资源服务器的ID, 会在认证服务器验证(客户端表的resources配置了就可以访问这个服务)
        resources.resourceId(RESOURCE_ID)
                // 实现令牌服务, ResourceServerTokenServices实例
                .tokenServices(defaultTokenService())
                .accessDeniedHandler(securityAccessDeniedHandler);
    }
	
	//JWT本地认证
    @Bean
    public ResourceServerTokenServices defaultTokenService() throws Exception{
        DefaultTokenServices tk = new DefaultTokenServices();
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        //使用本地认证jwt,签名需在多个服务器上同步
        jwtAccessTokenConverter.setSigningKey("test_sign");
        //非Spring注入方式需手动调用来配置默认的 verifier
        jwtAccessTokenConverter.afterPropertiesSet();
        JwtTokenStore jwtTokenStore = new JwtTokenStore(jwtAccessTokenConverter);
        tk.setTokenEnhancer(jwtAccessTokenConverter);
        tk.setTokenStore(jwtTokenStore);
        return tk;
    }

    //远程认证 非 JWT Token 使用
    public ResourceServerTokenServices tokenService() {
        // 资源服务器去远程认证服务器验证 token 是否有效
        RemoteTokenServices service = new RemoteTokenServices();
        //负载均衡(使用自定义RestTemplate可以实现负载均衡和服务发现)
        service.setRestTemplate(restTemplate);
        // 请求认证服务器验证URL,注意:默认这个端点是拒绝访问的,要设置认证后可访问
        service.setCheckTokenEndpointUrl("http://auth-server/oauth/check_token");
        // 在认证服务器配置的客户端id
        service.setClientId("test_client");
        // 在认证服务器配置的客户端密码
        service.setClientSecret("test");
        return service;
    }
}

基于OAuth2协议的SSO

待补充

 评论