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

举报
码农小胖哥 发表于 2022/05/19 22:24:58 2022/05/19
【摘要】 在业务系统中很可能遇到两个或者用户体系,比如后台管理用户和前台APP用户。很多时候这两种用户走的还是两种不同的体系,比如后台用户用有状态的Session,而前台用户用流行的无状态JWT,总之它们是两种完全不同的隔离体系。这种需求该怎么实现呢?其中有哪些坑要踩呢?本文将告诉你怎么做。 路径拦截策略 在Spring Secur...

3b81412206e2c0fa1d2b21370f7d111c.gif

在业务系统中很可能遇到两个或者用户体系,比如后台管理用户和前台APP用户。很多时候这两种用户走的还是两种不同的体系,比如后台用户用有状态的Session,而前台用户用流行的无状态JWT,总之它们是两种完全不同的隔离体系。这种需求该怎么实现呢?其中有哪些坑要踩呢?本文将告诉你怎么做。

路径拦截策略

Spring Security中当然是按照不同的请求路径规则定义专门的过滤器链,你可以通过三种方式来实现路径拦截。然后按照策略定义过滤器链即可:


   
  1. @Bean
  2. @Order(Ordered.HIGHEST_PRECEDENCE + 1)
  3. SecurityFilterChain systemSecurityFilterChain(HttpSecurity http) throws Exception {
  4.             // 省略
  5. }

这三种策略介绍如下。

按照正则过滤

你可以通过HttpSecurity提供的过滤器过滤URI,例如拦截请求中在query参数而且包含id的URI:

http.regexMatcher("/(\\\\?|\\\\&)\" + id + \"=([^\\\\&]+)/")
  

这种常用来匹配一些带参数的URL。

按照Ant规则过滤

这种是我们常见的方式,例如拦截/system开头的所有路径:

http.antMatcher("/system/**")
  

关于这种方式这里不再赘述,详细可以通过Ant规则详解这一篇来了解。

按照RequestMatcher过滤

一些复杂的组合可以通过定义RequestMatcher接口来组合,例如这种复杂的规则:


   
  1. RequestMatcher requestMatcher = new OrRequestMatcher(
  2.     new AntPathRequestMatcher(
  3.       providerSettings.getTokenEndpoint(),
  4.       HttpMethod.POST.name()),
  5.     new AntPathRequestMatcher(
  6.       providerSettings.getTokenIntrospectionEndpoint(),
  7.       HttpMethod.POST.name()),
  8.     new AntPathRequestMatcher(
  9.       providerSettings.getTokenRevocationEndpoint(),
  10.       HttpMethod.POST.name()));
  11.  
  12. http.requestMatcher(requestMatcher)

满足三个路径中的一个就行,这种组合方式能够实现最复杂的拦截策略。

配置隔离的一些要点

这里还要注意配置之间的隔离。

Session会话

默认情况下的Session依赖于cookie中设定的jsessionid, 如果你使用会话模式,必须隔离多个过滤器链的会话存储,这样能够实现一个多个过滤器在同一个会话下不同的登录状态,否则它们共享配置就会发生错乱。

这是因为在一个会话下,默认的属性KeySPRING_SECURITY_CONTEXT,当在同一个会话下(同一个浏览器不同的tab页)获取当前上下文都是这样的:


   
  1. // 默认 SPRING_SECURITY_CONTEXT
  2. Object contextFromSession = httpSession.getAttribute(this.springSecurityContextKey);

这样登录一个,其它都认为是登录状态,这显然不符合预期。你需要在不同的过滤器中定义不同的会话属性Key


   
  1. final String ID_SERVER_SYSTEM_SECURITY_CONTEXT_KEY ="SOME_UNIQUE_KEY"       
  2. HttpSessionSecurityContextRepository hs = new HttpSessionSecurityContextRepository();
  3. hs.setSpringSecurityContextKey(ID_SERVER_SYSTEM_SECURITY_CONTEXT_KEY);
  4. http.securityContext().securityContextRepository(hs)

无状态Token

无状态Token相对简单一些,前端根据路径分开存储即可,而且Token中应该包含校验过滤器链的信息以方便后端校验,避免Token混用。

UserDetailsService

如果你的不同端的用户是独立的,你需要实现不同的UserDetailsService,但是存在多个UserDetailsService的话,

一定不要将它们直接注册到Spring IoC中

一定不要将它们直接注册到Spring IoC中

一定不要将它们直接注册到Spring IoC中

如果你一定要注册到Spring IoC,你需要定义独立的接口,就像这样:


   
  1. @FunctionalInterface
  2. public interface OAuth2UserDetailsService {
  3.  
  4.     UserDetails loadOAuth2UserByUsername(String username) throws UsernameNotFoundException;
  5. }

然后实现该接口再注入Spring IoC,每个过滤器链配置的时候就可以这样写:


   
  1. @Bean
  2. @Order(Ordered.HIGHEST_PRECEDENCE + 2)
  3. SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http,
  4.                      OAuth2UserDetailsService oAuth2UserDetailsService) throws Exception {
  5.    http.userDetailsService(oAuth2UserDetailsService::loadOAuth2UserByUsername)
  6. }

但是Spring IoC中必须有一个UserDetailsService,你得这样写:


   
  1. @Bean
  2. UserDetailsService notFoundUserDetailsService() {
  3. return username -> {
  4. throw new UsernameNotFoundException("用户未找到");
  5. };
  6. }

为啥不可用,因为注入Spring IoCUserDetailsService是一个兜底的实现,如果你只有一个实现,放入Spring IoC无可厚非,如果你想让多个各自走各自的就必须这样写最安全,不然还有一个默认的InMemoryUserDetailsManager也会生效成为兜底的。

其它

其它配置按照各自的配置就行了,目前我还没有发现有冲突的地方。上面所讲的东西,在Id Server授权服务器中就是这样实现授权服务器过滤、后台管理用户和前台授权用户三者之间隔离的:


   
  1. @EnableWebSecurity
  2. @EnableGlobalMethodSecurity(prePostEnabled = true)
  3. public class IdServerSecurityConfiguration {
  4.     private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent";
  5.     private static final String SYSTEM_ANT_PATH = "/system/**";
  6.     /**
  7.      * The constant ID_SERVER_SYSTEM_SECURITY_CONTEXT_KEY.
  8.      */
  9.     public static final String ID_SERVER_SYSTEM_SECURITY_CONTEXT_KEY = "ID_SERVER_SYSTEM_SECURITY_CONTEXT";
  10.     /**
  11.      * 授权服务器配置
  12.      *
  13.      * @author felord.cn
  14.      * @since 1.0.0
  15.      */
  16.     @Configuration(proxyBeanMethods = false)
  17.     public static class AuthorizationServerConfiguration {
  18.         /**
  19.          * Authorization server 集成 优先级要高一些
  20.          *
  21.          * @param http the http
  22.          * @return the security filter chain
  23.          * @throws Exception the exception
  24.          * @since 1.0.0
  25.          */
  26.         @Bean("authorizationServerSecurityFilterChain")
  27.         @Order(Ordered.HIGHEST_PRECEDENCE)
  28.         SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
  29.             OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
  30.                     new OAuth2AuthorizationServerConfigurer<>();
  31.             //  把自定义的授权确认URI加入配置
  32.             authorizationServerConfigurer.authorizationEndpoint(authorizationEndpointConfigurer ->
  33.                     authorizationEndpointConfigurer.consentPage(CUSTOM_CONSENT_PAGE_URI));
  34.             RequestMatcher authorizationServerEndpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
  35.             // 拦截 授权服务器相关的请求端点
  36.             http.requestMatcher(authorizationServerEndpointsMatcher)
  37.                     .authorizeRequests().anyRequest().authenticated()
  38.                     .and()
  39.                     // 忽略掉相关端点的csrf
  40.                     .csrf(csrf -> csrf
  41.                             .ignoringRequestMatchers(authorizationServerEndpointsMatcher))
  42.                     .formLogin()
  43.                     .and()
  44.                     // 应用 授权服务器的配置
  45.                     .apply(authorizationServerConfigurer);
  46.             return http.build();
  47.         }
  48.         /**
  49.          * 配置 OAuth2.0 provider元信息
  50.          *
  51.          * @param port the port
  52.          * @return the provider settings
  53.          * @since 1.0.0
  54.          */
  55.         @Bean
  56.         public ProviderSettings providerSettings(@Value("${server.port}") Integer port) {
  57.             //TODO 配置化 生产应该使用域名
  58.             return ProviderSettings.builder().issuer("http://localhost:" + port).build();
  59.         }
  60.     }
  61.     /**
  62.      * 后台安全配置.
  63.      *
  64.      * @author felord.cn
  65.      * @since 1.0.0
  66.      */
  67.     @Configuration(proxyBeanMethods = false)
  68.     public static class SystemSecurityConfiguration {
  69.         /**
  70.          * 管理后台以{@code /system}开头
  71.          *
  72.          * @param http the http
  73.          * @return the security filter chain
  74.          * @throws Exception the exception
  75.          * @see AuthorizationServerConfiguration
  76.          */
  77.         @Bean
  78.         @Order(Ordered.HIGHEST_PRECEDENCE + 1)
  79.         SecurityFilterChain systemSecurityFilterChain(HttpSecurity http, UserInfoService userInfoService) throws Exception {
  80.             SimpleAuthenticationEntryPoint authenticationEntryPoint = new SimpleAuthenticationEntryPoint();
  81.             AuthenticationEntryPointFailureHandler authenticationFailureHandler = new AuthenticationEntryPointFailureHandler(authenticationEntryPoint);
  82.             HttpSessionSecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
  83.             securityContextRepository.setSpringSecurityContextKey(ID_SERVER_SYSTEM_SECURITY_CONTEXT_KEY);
  84.             http.antMatcher(SYSTEM_ANT_PATH).csrf().disable()
  85.                     .headers().frameOptions().sameOrigin()
  86.                     .and()
  87.                     .securityContext().securityContextRepository(securityContextRepository)
  88.                     .and()
  89.                     .authorizeRequests().anyRequest().authenticated()
  90.                     /*  .and()
  91.                       .exceptionHandling()
  92.                       .authenticationEntryPoint(authenticationEntryPoint)*/
  93.                     .and()
  94.                     .userDetailsService(userInfoService::findByUsername)
  95.                     .formLogin().loginPage("/system/login").loginProcessingUrl("/system/login")
  96.                     .successHandler(new RedirectLoginAuthenticationSuccessHandler("/system"))
  97.                     .failureHandler(authenticationFailureHandler).permitAll();
  98.             return http.build();
  99.         }
  100.     }
  101.     /**
  102.      * 普通用户访问安全配置.
  103.      *
  104.      * @author felord.cn
  105.      * @since 1.0.0
  106.      */
  107.     @Configuration(proxyBeanMethods = false)
  108.     public static class OAuth2SecurityConfiguration {
  109.         /**
  110.          * Default security filter chain security filter chain.
  111.          *
  112.          * @param http                     the http
  113.          * @param oAuth2UserDetailsService the oauth2 user details service
  114.          * @param securityFilterChain      the security filter chain
  115.          * @return the security filter chain
  116.          * @throws Exception the exception
  117.          */
  118.         @Bean
  119.         @Order(Ordered.HIGHEST_PRECEDENCE + 2)
  120.         SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http,
  121.                                                        OAuth2UserDetailsService oAuth2UserDetailsService,
  122.                                                        @Qualifier("authorizationServerSecurityFilterChain") SecurityFilterChain securityFilterChain) throws Exception {
  123.             DefaultSecurityFilterChain authorizationServerFilterChain = (DefaultSecurityFilterChain) securityFilterChain;
  124.             SimpleAuthenticationEntryPoint authenticationEntryPoint = new SimpleAuthenticationEntryPoint();
  125.             AuthenticationEntryPointFailureHandler authenticationFailureHandler = new AuthenticationEntryPointFailureHandler(authenticationEntryPoint);
  126.             http.requestMatcher(new AndRequestMatcher(
  127.                             new NegatedRequestMatcher(new AntPathRequestMatcher(SYSTEM_ANT_PATH)),
  128.                             new NegatedRequestMatcher(authorizationServerFilterChain.getRequestMatcher())
  129.                     )).authorizeRequests(authorizeRequests ->
  130.                             authorizeRequests
  131.                                     .anyRequest().authenticated()
  132.                     ).csrf().disable()
  133.                     .userDetailsService(oAuth2UserDetailsService::loadOAuth2UserByUsername)
  134.                     .formLogin().loginPage("/login")
  135.                     .successHandler(new RedirectLoginAuthenticationSuccessHandler())
  136.                     .failureHandler(authenticationFailureHandler).permitAll()
  137.                     .and()
  138.                     .oauth2ResourceServer().jwt();
  139.             return http.build();
  140.         }
  141.     }
  142. }

你可以通过https://github.com/NotFound403/id-server下载源码进行改造学习,欢迎Star。

OAuth2教程可通过https://blog.csdn.net/qq_35067322/category_11691173.html订阅。

5dda36ae4774ce66aae61a750ee55a74.gif

文章来源: felord.blog.csdn.net,作者:码农小胖哥,版权归原作者所有,如需转载,请联系作者。

原文链接:felord.blog.csdn.net/article/details/124833765

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。