一套系统多套用户安全体系该怎么办

网友投稿 659 2022-05-30

在业务系统中很可能遇到两个或者用户体系,比如后台管理用户和前台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 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订阅。

Spring 网络

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:初识GaussDB A(初识GaussDB(for Influx))
下一篇:Excel工作表的复制与移动技巧是什么(Excel中工作表的移动和复制可以怎样实现?)
相关文章