分布式日志第三篇,基于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
待补充