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

开发 架构
很多时候这两种用户走的还是两种不同的体系,比如后台用户用有状态的Session,而前台用户用流行的无状态JWT,总之它们是两种完全不同的隔离体系。这种需求该怎么实现呢?其中有哪些坑要踩呢?

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

路径拦截策略

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

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

这三种策略介绍如下。

按照正则过滤

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

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

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

按照Ant规则过滤

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

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

关于这种方式这里不再赘述,详细可以通过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)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

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

配置隔离的一些要点

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

Session会话

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

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

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

这样登录一个,其它都认为是登录状态,这显然不符合预期。你需要在不同的过滤器中定义不同的会话属性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)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

无状态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;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

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

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

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

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

为啥不可用,因为注入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();
        }

    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.
  • 154.

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

责任编辑:武晓燕 来源: 码农小胖哥
相关推荐

2025-02-21 08:17:13

2010-05-20 16:08:01

亚马逊故障

2019-07-01 12:55:05

安全体系架构网络安全企业安全

2013-07-15 09:51:04

2017-06-30 13:23:59

SaaS供应商破产

2022-04-22 10:30:07

框架JavaScript前端

2011-06-30 17:58:30

网站被K

2009-12-09 09:49:29

ibmdwWebSphere

2021-05-27 07:12:19

单点登录系统

2025-01-08 10:30:24

2012-12-25 14:23:10

2017-07-17 10:15:07

Windows 10Windowsinternet ex

2010-11-17 11:06:34

跳槽

2018-08-20 19:39:14

区块链职业岗位

2016-11-24 14:44:49

云计算

2021-01-26 08:02:04

Redis内存数据库

2021-10-20 22:47:17

Windows 10Windows微软

2017-11-10 07:20:06

2020-08-18 08:11:08

安全体系化建设漏洞网络安全

2019-04-15 10:45:37

点赞
收藏

51CTO技术栈公众号