分布式日志第三篇,基于SpringSecurity的分布式系统的安全管理方案
分布式安全协议
微服务架构下的安全方案有许多种,如
- 共享Session (最容易实现)
 - Token凭证 (最高可用性)
 - SSO单点登录 (折中方案)
 
○共享Session
由认证服务器认证后将本地Session保存在Redis中,其他服务器读取Redis中的Session对请求进行身份认证,最大的优点是不需要多余的代码,可以像开发单体应用一样开发安全管理系统
缺点:Redis服务或认证服务不可用时 将无法访问所有的资源服务器。
○基于OAuth2协议的Token认证方案
Token也分为两种模式:
- 
短Token
认证服务器颁发access_token,与之对应的是储存在Redis或数据库中的权限信息,资源服务通过此凭证向认证服务器请求认证,从而获取用户的权限信息
 - 
长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
待补充