一套系统多套用户安全体系该怎么办
在业务系统中很可能遇到两个或者用户体系,比如后台管理用户和前台APP用户。很多时候这两种用户走的还是两种不同的体系,比如后台用户用有状态的Session,而前台用户用流行的无状态JWT,总之它们是两种完全不同的隔离体系。这种需求该怎么实现呢?其中有哪些坑要踩呢?本文将告诉你怎么做。
路径拦截策略
在Spring Security中当然是按照不同的请求路径规则定义专门的过滤器链,你可以通过三种方式来实现路径拦截。然后按照策略定义过滤器链即可:
-
@Bean
-
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
-
SecurityFilterChain systemSecurityFilterChain(HttpSecurity http) throws Exception {
-
// 省略
-
}
这三种策略介绍如下。
按照正则过滤
你可以通过HttpSecurity
提供的过滤器过滤URI,例如拦截请求中在query参数而且包含id
的URI:
http.regexMatcher("/(\\\\?|\\\\&)\" + id + \"=([^\\\\&]+)/")
这种常用来匹配一些带参数的URL。
按照Ant规则过滤
这种是我们常见的方式,例如拦截/system
开头的所有路径:
http.antMatcher("/system/**")
关于这种方式这里不再赘述,详细可以通过Ant规则详解这一篇来了解。
按照RequestMatcher过滤
一些复杂的组合可以通过定义RequestMatcher
接口来组合,例如这种复杂的规则:
-
RequestMatcher requestMatcher = new OrRequestMatcher(
-
new AntPathRequestMatcher(
-
providerSettings.getTokenEndpoint(),
-
HttpMethod.POST.name()),
-
new AntPathRequestMatcher(
-
providerSettings.getTokenIntrospectionEndpoint(),
-
HttpMethod.POST.name()),
-
new AntPathRequestMatcher(
-
providerSettings.getTokenRevocationEndpoint(),
-
HttpMethod.POST.name()));
-
-
http.requestMatcher(requestMatcher)
满足三个路径中的一个就行,这种组合方式能够实现最复杂的拦截策略。
配置隔离的一些要点
这里还要注意配置之间的隔离。
Session会话
默认情况下的Session依赖于cookie中设定的jsessionid
, 如果你使用会话模式,必须隔离多个过滤器链的会话存储,这样能够实现一个多个过滤器在同一个会话下不同的登录状态,否则它们共享配置就会发生错乱。
这是因为在一个会话下,默认的属性Key是SPRING_SECURITY_CONTEXT
,当在同一个会话下(同一个浏览器不同的tab页)获取当前上下文都是这样的:
-
// 默认 SPRING_SECURITY_CONTEXT
-
Object contextFromSession = httpSession.getAttribute(this.springSecurityContextKey);
这样登录一个,其它都认为是登录状态,这显然不符合预期。你需要在不同的过滤器中定义不同的会话属性Key。
-
final String ID_SERVER_SYSTEM_SECURITY_CONTEXT_KEY ="SOME_UNIQUE_KEY"
-
HttpSessionSecurityContextRepository hs = new HttpSessionSecurityContextRepository();
-
hs.setSpringSecurityContextKey(ID_SERVER_SYSTEM_SECURITY_CONTEXT_KEY);
-
-
http.securityContext().securityContextRepository(hs)
无状态Token
无状态Token相对简单一些,前端根据路径分开存储即可,而且Token中应该包含校验过滤器链的信息以方便后端校验,避免Token混用。
UserDetailsService
如果你的不同端的用户是独立的,你需要实现不同的UserDetailsService
,但是存在多个UserDetailsService
的话,
一定不要将它们直接注册到Spring IoC中!
一定不要将它们直接注册到Spring IoC中!
一定不要将它们直接注册到Spring IoC中!
如果你一定要注册到Spring IoC,你需要定义独立的接口,就像这样:
-
@FunctionalInterface
-
public interface OAuth2UserDetailsService {
-
-
UserDetails loadOAuth2UserByUsername(String username) throws UsernameNotFoundException;
-
}
然后实现该接口再注入Spring IoC,每个过滤器链配置的时候就可以这样写:
-
@Bean
-
@Order(Ordered.HIGHEST_PRECEDENCE + 2)
-
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http,
-
OAuth2UserDetailsService oAuth2UserDetailsService) throws Exception {
-
http.userDetailsService(oAuth2UserDetailsService::loadOAuth2UserByUsername)
-
}
但是Spring IoC中必须有一个UserDetailsService
,你得这样写:
-
@Bean
-
UserDetailsService notFoundUserDetailsService() {
-
return username -> {
-
throw new UsernameNotFoundException("用户未找到");
-
};
-
}
为啥不可用,因为注入Spring IoC的UserDetailsService
是一个兜底的实现,如果你只有一个实现,放入Spring IoC无可厚非,如果你想让多个各自走各自的就必须这样写最安全,不然还有一个默认的InMemoryUserDetailsManager
也会生效成为兜底的。
其它
其它配置按照各自的配置就行了,目前我还没有发现有冲突的地方。上面所讲的东西,在Id Server授权服务器中就是这样实现授权服务器过滤、后台管理用户和前台授权用户三者之间隔离的:
-
@EnableWebSecurity
-
@EnableGlobalMethodSecurity(prePostEnabled = true)
-
public class IdServerSecurityConfiguration {
-
-
private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent";
-
private static final String SYSTEM_ANT_PATH = "/system/**";
-
/**
-
* The constant ID_SERVER_SYSTEM_SECURITY_CONTEXT_KEY.
-
*/
-
public static final String ID_SERVER_SYSTEM_SECURITY_CONTEXT_KEY = "ID_SERVER_SYSTEM_SECURITY_CONTEXT";
-
-
/**
-
* 授权服务器配置
-
*
-
* @author felord.cn
-
* @since 1.0.0
-
*/
-
@Configuration(proxyBeanMethods = false)
-
public static class AuthorizationServerConfiguration {
-
-
/**
-
* Authorization server 集成 优先级要高一些
-
*
-
* @param http the http
-
* @return the security filter chain
-
* @throws Exception the exception
-
* @since 1.0.0
-
*/
-
@Bean("authorizationServerSecurityFilterChain")
-
@Order(Ordered.HIGHEST_PRECEDENCE)
-
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
-
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
-
new OAuth2AuthorizationServerConfigurer<>();
-
// 把自定义的授权确认URI加入配置
-
authorizationServerConfigurer.authorizationEndpoint(authorizationEndpointConfigurer ->
-
authorizationEndpointConfigurer.consentPage(CUSTOM_CONSENT_PAGE_URI));
-
-
RequestMatcher authorizationServerEndpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
-
-
// 拦截 授权服务器相关的请求端点
-
http.requestMatcher(authorizationServerEndpointsMatcher)
-
.authorizeRequests().anyRequest().authenticated()
-
.and()
-
// 忽略掉相关端点的csrf
-
.csrf(csrf -> csrf
-
.ignoringRequestMatchers(authorizationServerEndpointsMatcher))
-
.formLogin()
-
.and()
-
// 应用 授权服务器的配置
-
.apply(authorizationServerConfigurer);
-
return http.build();
-
}
-
-
/**
-
* 配置 OAuth2.0 provider元信息
-
*
-
* @param port the port
-
* @return the provider settings
-
* @since 1.0.0
-
*/
-
@Bean
-
public ProviderSettings providerSettings(@Value("${server.port}") Integer port) {
-
//TODO 配置化 生产应该使用域名
-
return ProviderSettings.builder().issuer("http://localhost:" + port).build();
-
}
-
}
-
-
/**
-
* 后台安全配置.
-
*
-
* @author felord.cn
-
* @since 1.0.0
-
*/
-
@Configuration(proxyBeanMethods = false)
-
public static class SystemSecurityConfiguration {
-
-
/**
-
* 管理后台以{@code /system}开头
-
*
-
* @param http the http
-
* @return the security filter chain
-
* @throws Exception the exception
-
* @see AuthorizationServerConfiguration
-
*/
-
@Bean
-
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
-
SecurityFilterChain systemSecurityFilterChain(HttpSecurity http, UserInfoService userInfoService) throws Exception {
-
SimpleAuthenticationEntryPoint authenticationEntryPoint = new SimpleAuthenticationEntryPoint();
-
AuthenticationEntryPointFailureHandler authenticationFailureHandler = new AuthenticationEntryPointFailureHandler(authenticationEntryPoint);
-
HttpSessionSecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
-
securityContextRepository.setSpringSecurityContextKey(ID_SERVER_SYSTEM_SECURITY_CONTEXT_KEY);
-
http.antMatcher(SYSTEM_ANT_PATH).csrf().disable()
-
.headers().frameOptions().sameOrigin()
-
.and()
-
.securityContext().securityContextRepository(securityContextRepository)
-
.and()
-
.authorizeRequests().anyRequest().authenticated()
-
/* .and()
-
.exceptionHandling()
-
.authenticationEntryPoint(authenticationEntryPoint)*/
-
.and()
-
.userDetailsService(userInfoService::findByUsername)
-
.formLogin().loginPage("/system/login").loginProcessingUrl("/system/login")
-
.successHandler(new RedirectLoginAuthenticationSuccessHandler("/system"))
-
.failureHandler(authenticationFailureHandler).permitAll();
-
return http.build();
-
}
-
-
}
-
-
/**
-
* 普通用户访问安全配置.
-
*
-
* @author felord.cn
-
* @since 1.0.0
-
*/
-
@Configuration(proxyBeanMethods = false)
-
public static class OAuth2SecurityConfiguration {
-
-
/**
-
* Default security filter chain security filter chain.
-
*
-
* @param http the http
-
* @param oAuth2UserDetailsService the oauth2 user details service
-
* @param securityFilterChain the security filter chain
-
* @return the security filter chain
-
* @throws Exception the exception
-
*/
-
@Bean
-
@Order(Ordered.HIGHEST_PRECEDENCE + 2)
-
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http,
-
OAuth2UserDetailsService oAuth2UserDetailsService,
-
@Qualifier("authorizationServerSecurityFilterChain") SecurityFilterChain securityFilterChain) throws Exception {
-
DefaultSecurityFilterChain authorizationServerFilterChain = (DefaultSecurityFilterChain) securityFilterChain;
-
SimpleAuthenticationEntryPoint authenticationEntryPoint = new SimpleAuthenticationEntryPoint();
-
AuthenticationEntryPointFailureHandler authenticationFailureHandler = new AuthenticationEntryPointFailureHandler(authenticationEntryPoint);
-
http.requestMatcher(new AndRequestMatcher(
-
new NegatedRequestMatcher(new AntPathRequestMatcher(SYSTEM_ANT_PATH)),
-
new NegatedRequestMatcher(authorizationServerFilterChain.getRequestMatcher())
-
)).authorizeRequests(authorizeRequests ->
-
authorizeRequests
-
.anyRequest().authenticated()
-
).csrf().disable()
-
.userDetailsService(oAuth2UserDetailsService::loadOAuth2UserByUsername)
-
.formLogin().loginPage("/login")
-
.successHandler(new RedirectLoginAuthenticationSuccessHandler())
-
.failureHandler(authenticationFailureHandler).permitAll()
-
.and()
-
.oauth2ResourceServer().jwt();
-
return http.build();
-
}
-
-
}
-
}
你可以通过https://github.com/NotFound403/id-server
下载源码进行改造学习,欢迎Star。
OAuth2教程可通过https://blog.csdn.net/qq_35067322/category_11691173.html
订阅。
文章来源: felord.blog.csdn.net,作者:码农小胖哥,版权归原作者所有,如需转载,请联系作者。
原文链接:felord.blog.csdn.net/article/details/124833765
- 点赞
- 收藏
- 关注作者
评论(0)