在业务系统中很可能遇到两个或者用户体系,套系统多套用比如后台管理用户和前台APP用户。户安很多时候这两种用户走的全体还是两种不同的体系,比如后台用户用有状态的系该Session,而前台用户用流行的套系统多套用无状态JWT,总之它们是户安两种完全不同的隔离体系。这种需求该怎么实现呢?全体其中有哪些坑要踩呢?本文将告诉你怎么做。 在Spring Security中当然是系该按照不同的请求路径规则定义专门的过滤器链,你可以通过三种方式来实现路径拦截。套系统多套用然后按照策略定义过滤器链即可: @Bean @Order(Ordered.HIGHEST_PRECEDENCE + 1) SecurityFilterChain systemSecurityFilterChain(HttpSecurity http) throws Exception { // 省略 这三种策略介绍如下。户安 你可以通过HttpSecurity提供的全体过滤器过滤URI,例如拦截请求中在query参数而且包含id的系该URI: 这种常用来匹配一些带参数的URL。 这种是套系统多套用我们常见的方式,例如拦截/system开头的户安所有路径: 关于这种方式这里不再赘述,详细可以通过Ant规则详解这一篇来了解。全体 一些复杂的组合可以通过定义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())); 满足三个路径中的一个就行,这种组合方式能够实现最复杂的拦截策略。 这里还要注意配置之间的隔离。 默认情况下的Session依赖于cookie中设定的jsessionid, 如果你使用会话模式,必须隔离多个过滤器链的会话存储,这样能够实现一个多个过滤器在同一个会话下不同的登录状态,否则它们共享配置就会发生错乱。 这是因为在一个会话下,默认的属性Key是SPRING_SECURITY_CONTEXT,当在同一个会话下(同一个浏览器不同的tab页)获取当前上下文都是这样的: // 默认 SPRING_SECURITY_CONTEXT 这样登录一个,其它都认为是登录状态,这显然不符合预期。你需要在不同的过滤器中定义不同的会话属性Key。 final String ID_SERVER_SYSTEM_SECURITY_CONTEXT_KEY ="SOME_UNIQUE_KEY" HttpSessionSecurityContextRepository hs = new HttpSessionSecurityContextRepository(); hs.setSpringSecurityContextKey(ID_SERVER_SYSTEM_SECURITY_CONTEXT_KEY); 无状态Token相对简单一些,前端根据路径分开存储即可,而且Token中应该包含校验过滤器链的源码库信息以方便后端校验,避免Token混用。 如果你的不同端的用户是独立的,你需要实现不同的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也会生效成为兜底的。 其它配置按照各自的配置就行了,云南idc服务商目前我还没有发现有冲突的地方。上面所讲的东西,在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 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。路径拦截策略
配置隔离的一些要点