spring security的UserDetail实现

本文深入剖析SpringSecurity中如何通过SecurityContextPersistenceFilter过滤器管理会话,利用ThreadLocal存储SecurityContext,确保每个请求间安全上下文的独立性和一致性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

在使用spring security开发的过程中,我们常常会用到这样的写法:

UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication();

来获取UserDetails,即使是在web多线程环境下,我们也总是能拿到想要的结果。很奇怪spring security是如何做到的,因此开了这篇文章来分析。

注:本篇文章采用spring security4.2.3版本

spring security的过滤器链

首先我们得对spring security的过滤器链有一个整体上的认识。

在使用配置web.xml这种开发方式时,我们如果要使用spring security就必须得在web.xml中写入这样一段:

<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

这个意味着向tomcat容器注册一个Filter,它会过滤/*所有的请求。看DelegatingFilterProxy,顾名思义,这是一个代理类,真的过滤操作是由FilterChainProxy来中的内部类VirtualFilterChain来完成的。其中注册了一定数量的Filter(一般是12个),来达到对请求的权限管理操作。具体代码:

public void doFilter(ServletRequest request, ServletResponse response)
				throws IOException, ServletException {
	//如果每个过滤器都已通过,会转回tomcat中ApplicationFilterChain
	if (currentPosition == size) {
		if (logger.isDebugEnabled()) {
			logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
					+ " reached end of additional filter chain; proceeding with original chain");
		}

		// Deactivate path stripping as we exit the security filter chain
		this.firewalledRequest.reset();

		originalChain.doFilter(request, response);
	}
	else {
		//按顺序取出过滤器   
		currentPosition++;

		Filter nextFilter = additionalFilters.get(currentPosition - 1);

		if (logger.isDebugEnabled()) {
			logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
					+ " at position " + currentPosition + " of " + size
					+ " in additional filter chain; firing Filter: '"
					+ nextFilter.getClass().getSimpleName() + "'");
		}

		nextFilter.doFilter(request, response, this);
	}
}

在一般情况下,spring security有一系列的Filter

  • WebAsyncManagerIntegrationFilter:提供了对securityContext和WebAsyncManager的集成,其会把SecurityContext设置到异步线程中,使其也能获取到用户上下文认证信息
  • SecurityContextPersistenceFilter:这个是本篇文章的重点,它会根据策略获取一个SecurityContext放到SecurityContextHolder中,并且在请求结束后清空
  • HeaderWriterFilter:其会往该请求的Header中添加相应的信息,在http标签内部使用security:headers来控制
  • LogoutFilter:匹配URL,默认为/logout,匹配成功后则用户退出,清除认证信息.如果有自己的退出逻辑,那么这个过滤器可以disable
  • UsernamePasswordAuthenticationFilter:登录认证过滤器,根据用户名密码进行认证
  • ConcurrentSessionFilter:session同步过滤器,主要有两个功能,一是会刷新当前session的最后访问时间,二是判断当前session是否失效,失效了的话会做退出操作并触发相应事件。
  • RequestCacheAwareFilter:重新恢复被打断的请求
  • SecurityContextHolderAwareRequestFilter:将request包装成HttpServletRequest
  • AnonymousAuthenticationFilter:判断SecurityContext中是否有一个Authentication对象,如果没有创建一个新的(AnonymousAuthenticationToken)
  • SessionManagementFilter:检查session在spring security中是否是失效了(注意不是在web容器中),比如说配置设置了最大session数量为1,那么之前的session会被设置expired = true
  • ExceptionTranslationFilter:处理AccessDeniedException和AuthenticationException,为java exceptions和HTTP responses提供了桥梁
  • FilterSecurityInterceptor:对http资源做权限拦截,我们平时设置的不同角色不同权限访问就是借此Filter过滤

ThreadLocal

在详细介绍SecurityContextPersistenceFilter之前,必须了解ThreadLocal这个类,spring中也许多地方用到了ThreadLocal。

先看一下它的百科:

JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序,ThreadLocal并不是一个Thread,而是Thread的局部变量。

其中说的很清楚,ThreadLocal并不是一个Thread,而是Thread的局部变量,我想这也是大部分人将其翻译为“本地线程变量”的原因。

然后介绍一下其中的数据结构。

每个Thread会维护一个本地变量ThreadLocalMap,它是HashMap的另一种实现,key是ThreadLocal变量本身,value才是要存储的值。ThreadLocal本事并不存储任何值,它只是充当了key的作用。如下图:

ThreadLocalMap具体实现在这里我们不做分析,大可以将其想象为一个普通的Map,其中key是ThreadLocal本身,value是要存储的值。

SecurityContextPersistenceFilter

最关键的步骤就是SecurityContextPersistenceFilter这个过滤器了。先介绍下其中关键的两个类:

SecurityContextRepository

顾名思义,是存储SecurityContext的仓库,默认实现是HttpSessionSecurityContextRepository,基于session,将SecurityContext用key="SPRING_SECURITY_CONTEXT"存入session中。

Object contextFromSession = httpSession.getAttribute(springSecurityContextKey);

SecurityContextHolder

在请求之间保存SecurityContext,提供了一系列的静态方法。使用了策略设计模式,默认使用的策略是ThreadLocalSecurityContextHolderStrategy,这其中便是使用ThreadLocal进行存储的。

我们再说下该过滤器的大概流程:首先会从SecurityContextRepository中获取SecurityContext,然后将其设置到SecurityContextHolder中,之后会转到下一个过滤器。在请求结束之后,清空SecurityContextHolder,并将请求后的SecurityContext在保存到SecurityContextRepository中。
贴上源码:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
	HttpServletRequest request = (HttpServletRequest) req;
	HttpServletResponse response = (HttpServletResponse) res;
    
        //省略若干语句...

	HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
			response);
	//从session中根据key取出SecurityContext,如果没有会创建一个新的
	SecurityContext contextBeforeChainExecution = repo.loadContext(holder);

	try {
	        //设置到ThreadLoacal中
		SecurityContextHolder.setContext(contextBeforeChainExecution);

		chain.doFilter(holder.getRequest(), holder.getResponse());

	}
	finally {
		SecurityContext contextAfterChainExecution = SecurityContextHolder
				.getContext();
		// Crucial removal of SecurityContextHolder contents - do this before anything
		// else.
		SecurityContextHolder.clearContext();
		repo.saveContext(contextAfterChainExecution, holder.getRequest(),
				holder.getResponse());
		request.removeAttribute(FILTER_APPLIED);

		if (debug) {
			logger.debug("SecurityContextHolder now cleared, as request processing completed");
		}
	}
	}

注意:这个finally语句块是在request经过Filter,到达DispatcherServlet完成业务处理之后才会运行的。所以我们可以在controller中直接使用文章开头的方式获取到当前登录用户。

总结

最后再来梳理一遍流程:

浏览器发起一个Http请求到达Tomcat,Tomcat将其封装成一个Request,先经过Filter,其中经过spring security的SecurityContextPersistenceFilter,从session中取出SecurityContext(如果没有就创建新的)存入当前线程的ThreadLocalMap中,因为是当前线程,所以不同的线程之间根本互不影响。之后完成Servlet调用,执行finally语句块,清除当前线程中ThreadLocalMap对应的SecurityContext,再将其覆盖session中之前的部分。

这里多说一句,为什么在每次请求之后要清空当前线程呢?看一下spring的官方api说明:

In an application which receives concurrent requests in a single session, the same SecurityContext instance will be shared between threads. Even though a ThreadLocal is being used, it is the same instance that is retrieved from the HttpSession for each thread. This has implications if you wish to temporarily change the context under which a thread is running. If you just use SecurityContextHolder.getContext(), and call setAuthentication(anAuthentication) on the returned context object, then the Authentication object will change in all concurrent threads which share the same SecurityContext instance. You can customize the behaviour of SecurityContextPersistenceFilter to create a completely new SecurityContext for each request, preventing changes in one thread from affecting another. Alternatively you can create a new instance just at the point where you temporarily change the context. The method SecurityContextHolder.createEmptyContext() always returns a new context instance.

简而言之,因为SecurityContext,是放在session中的,所有一个session下的request的都是共享一个SecurityContext,也就是会有多个Thread共享一个SecurityContext。如果我们在某一个线程中只是想临时对SecurityContext做点更改,那么其他线程中SecurityContext也会受到影响,这是不被允许的。

参考

Spring Security(二) – Spring Security的Filter

【java并发】详解ThreadLocal

Spring Security Reference

<think>我们已知海关金关工程4A系统需要与Spring Security集成,并且用户希望使用UserDetails接口实现。根据之前的讨论,我们假设4A系统提供OAuth2认证,但用户现在要求通过UserDetailsService来加载用户信息。因此,我们需要自定义一个UserDetailsService,该服务通过4A系统的接口(可能是RESTful)获取用户信息,并转换为Spring SecurityUserDetails对象。 步骤: 1. 创建一个实现UserDetailsService接口的类,例如`FourAUserDetailsService`。 2. 在该类中,通过调用4A系统的用户信息接口(需要身份认证,如使用client_credentials方式获取token)来获取用户详细信息。 3. 将获取到的用户信息(包括用户名、密码、权限等)转换为UserDetails对象(可以使用Spring Security提供的User类或自定义实现)。 4. 在Spring Security配置中,使用这个自定义的UserDetailsService。 注意:由于4A系统可能使用自己的用户模型,我们需要将4A系统的用户属性映射到UserDetails的相应属性。 代码示例: 首先,我们定义一个用于接收4A系统返回的用户信息的DTO(假设4A系统返回JSON格式): ```java public class FourAUser { private String username; // 用户名 private String password; // 密码(加密的) private boolean enabled; // 账户是否启用 private List<String> authorities; // 权限列表 // getters and setters } ``` 然后,实现UserDetailsService: ```java @Service public class FourAUserDetailsService implements UserDetailsService { @Autowired private FourARestClient fourARestClient; // 假设有一个用于调用4A系统接口的客户端 @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 通过4A系统的接口获取用户信息 FourAUser fourAUser = fourARestClient.getUserByUsername(username); if (fourAUser == null) { throw new UsernameNotFoundException("User not found: " + username); } // 将权限字符串列表转换为GrantedAuthority列表 List<GrantedAuthority> authorities = fourAUser.getAuthorities().stream() .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); // 使用Spring Security的User类(或者可以自定义实现UserDetails接口) return new User( fourAUser.getUsername(), fourAUser.getPassword(), fourAUser.isEnabled(), true, // 账户未过期 true, // 凭证未过期 true, // 账户未锁定 authorities ); } } ``` 接下来,我们需要在Spring Security配置中使用这个UserDetailsService。注意,由于我们使用了OAuth2资源服务器(JWT),但同时需要自定义用户加载,我们可以结合使用。但根据用户要求,我们可能希望使用密码模式(即使用用户名密码登录),或者使用JWT中包含的用户名来加载用户详细信息。这里假设我们使用JWT,并且在JWT中包含了用户名,然后使用这个用户名通过UserDetailsService加载用户。 修改之前的SecurityConfig,指定UserDetailsService: ```java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private FourAUserDetailsService userDetailsService; // ... 其他配置 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService); } @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() // ... 授权配置 .and() .oauth2ResourceServer() .jwt() .jwtAuthenticationConverter(new FourAJwtConverter()); // 在自定义的JwtConverter中,我们可能需要使用UserDetailsService // ... 其他配置 } } ``` 但是,在OAuth2资源服务器中,默认情况下,JWT的认证不使用UserDetailsService。因此,我们需要在自定义的JwtAuthenticationConverter中利用UserDetailsService来加载用户权限。我们可以这样做: 修改自定义的JwtConverter,使其在转换时使用UserDetailsService: ```java public class FourAJwtConverter implements Converter<Jwt, AbstractAuthenticationToken> { private final JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); private final UserDetailsService userDetailsService; public FourAJwtConverter(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; } @Override public AbstractAuthenticationToken convert(Jwt jwt) { // 首先,使用默认转换器获取权限(如果JWT中有权限声明,则使用) AbstractAuthenticationToken token = jwtAuthenticationConverter.convert(jwt); if (token == null) { return null; } // 从JWT中获取用户名 String username = jwt.getSubject(); // 使用UserDetailsService加载用户 UserDetails userDetails = userDetailsService.loadUserByUsername(username); // 创建一个新的认证令牌,包含从UserDetailsService中加载的权限 return new JwtAuthenticationToken(jwt, userDetails.getAuthorities(), userDetails.getUsername()); } } ``` 这样,我们就将4A系统的用户信息通过UserDetailsService整合到了Spring Security的认证流程中。 另外,如果4A系统不支持直接通过用户名获取用户信息(比如需要token),那么我们需要在FourARestClient中处理认证问题。例如,使用客户端凭证获取访问令牌,然后用这个令牌去请求用户信息。 关于FourARestClient的实现示例(使用RestTemplate): ```java @Component public class FourARestClient { @Value("${customs.4a.user-info-uri}") private String userInfoUri; @Value("${customs.4a.client-id}") private String clientId; @Value("${customs.4a.client-secret}") private String clientSecret; // 获取用户信息 public FourAUser getUserByUsername(String username) { // 首先获取客户端凭证的token String accessToken = getClientCredentialsToken(); // 使用token调用用户信息接口 RestTemplate restTemplate = new RestTemplate(); HttpHeaders headers = new HttpHeaders(); headers.setBearerAuth(accessToken); headers.setContentType(MediaType.APPLICATION_JSON); // 假设4A系统提供的用户信息接口需要传递用户名 HttpEntity<String> entity = new HttpEntity<>(headers); ResponseEntity<FourAUser> response = restTemplate.exchange( userInfoUri + "?username=" + username, HttpMethod.GET, entity, FourAUser.class ); return response.getBody(); } private String getClientCredentialsToken() { // 使用RestTemplate向4A系统的token端点发送请求 // 这里省略具体实现,通常需要构造一个表单请求,包含grant_type=client_credentials // 返回访问令牌 } } ``` 注意:上述代码仅为示例,实际实现需要根据4A系统的具体接口调整。 总结:通过自定义UserDetailsService,我们能够将海关金关工程4A系统的用户信息集成到Spring Security中,从而在认证和授权过程中使用4A系统的数据。 相关问题: 1. 如何在没有4A系统用户信息接口的情况下实现UserDetailsService? 2. 如何处理4A系统用户信息与本地用户信息的融合? 3. 当4A系统不可用时,如何保证系统的认证功能降级(例如使用本地缓存)? 注意:在实际项目中,还需要考虑性能问题(比如缓存用户信息)和异常处理。</think>### Spring Security 集成海关金关工程4A系统的UserDetails实现 下面提供完整的实现方案,通过自定义`UserDetails`接口对接海关金关工程4A系统,实现用户认证和授权管理。 #### 1. 核心接口实现 ```java // 自定义UserDetails实现 public class FourAUserDetails implements UserDetails { private final String userCode; // 海关4A用户唯一编码 private final String password; // 加密密码 private final boolean enabled; // 账户状态 private final List<GrantedAuthority> authorities; // 4A系统授权角色 public FourAUserDetails(String userCode, String password, boolean enabled, List<String> roles) { this.userCode = userCode; this.password = password; this.enabled = enabled; this.authorities = roles.stream() .map(role -> new SimpleGrantedAuthority("CUSTOMS_" + role)) .collect(Collectors.toList()); } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @Override public String getPassword() { return password; } @Override public String getUsername() { return userCode; // 使用海关用户编码作为唯一标识 } @Override public boolean isEnabled() { return enabled; } // 账户状态管理(对接4A账户状态) @Override public boolean isAccountNonExpired() { return true; // 需对接4A账户有效期接口 } @Override public boolean isAccountNonLocked() { return enabled; // 账户锁定状态同步 } } ``` #### 2. UserDetailsService实现 ```java @Service public class FourAUserDetailsService implements UserDetailsService { @Autowired private FourASystemClient fourAClient; // 4A系统对接客户端 @Override public UserDetails loadUserByUsername(String userCode) throws UsernameNotFoundException { // 调用4A系统获取用户信息 FourAUserResponse userInfo = fourAClient.getUserInfo(userCode); // 验证账户状态(符合海关账户管理规范) if (!"ACTIVE".equals(userInfo.getStatus())) { throw new DisabledException("4A账户状态异常: " + userInfo.getStatus()); } // 转换为Spring Security用户模型 return new FourAUserDetails( userCode, userInfo.getEncryptedPassword(), // SM4国密加密密码 true, userInfo.getRoles() // 海关角色如: ["DECLARE_ADMIN", "INSPECTION_OFFICER"] ); } } ``` #### 3. 安全配置类 ```java @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private FourAUserDetailsService userDetailsService; // 配置密码加密器(国密SM4算法) @Bean public PasswordEncoder passwordEncoder() { return new SM4PasswordEncoder(); // 实现国密算法 } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/api/customs/**") .hasAnyAuthority("CUSTOMS_DECLARE_ADMIN", "CUSTOMS_INSPECTION_OFFICER") .antMatchers("/api/admin/**") .hasAuthority("CUSTOMS_SYSTEM_ADMIN") .anyRequest().authenticated() .and() .formLogin() .loginPage("/4a-login") // 海关统一登录入口 .defaultSuccessUrl("/home") .and() .logout() .logoutUrl("/4a-logout") // 对接4A审计系统 .addLogoutHandler(new FourAAuditLogoutHandler()); } } ``` #### 4. 4A系统客户端实现(关键) ```java @Component public class FourASystemClient { private static final String USER_INFO_URI = "https://4a.customs.gov.cn/api/v1/user"; @Value("${customs.4a.client-id}") private String clientId; @Value("${customs.4a.client-secret}") private String clientSecret; public FourAUserResponse getUserInfo(String userCode) { // 构建海关4A认证请求 HttpHeaders headers = new HttpHeaders(); headers.set("X-4A-Client-ID", clientId); headers.set("X-4A-Signature", generateSignature(userCode)); // 调用4A系统接口 ResponseEntity<FourAUserResponse> response = restTemplate.exchange( USER_INFO_URI + "?userCode=" + userCode, HttpMethod.GET, new HttpEntity<>(headers), FourAUserResponse.class ); if (!response.getStatusCode().is2xxSuccessful()) { throw new AuthenticationServiceException("4A系统访问异常"); } return response.getBody(); } // 生成国密SM3签名(符合海关安全规范) private String generateSignature(String userCode) { // 实现海关要求的签名算法 } } // 4A系统返回数据结构 @Data public class FourAUserResponse { private String userCode; // 用户唯一编码 private String encryptedPassword; // SM4加密密码 private String status; // 账户状态 ACTIVE/LOCKED/EXPIRED private List<String> roles; // 海关角色编码 private String department; // 所属海关部门 } ``` #### 5. 国密算法实现示例 ```java public class SM4PasswordEncoder implements PasswordEncoder { private final SM4Engine sm4Engine; // 国密SM4实现 @Override public String encode(CharSequence rawPassword) { // 调用海关加密服务 return sm4Engine.encrypt(rawPassword.toString()); } @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { // 使用4A系统密码策略验证 String encrypted = sm4Engine.encrypt(rawPassword.toString()); return encrypted.equals(encodedPassword); } } ``` ### 关键集成点说明 1. **账户映射策略** - 使用海关用户编码`userCode`作为Spring Security用户名 - 角色前缀统一添加`CUSTOMS_`标识(如`CUSTOMS_DECLARE_ADMIN`) - 账户状态同步4A系统状态(禁用/锁定/过期) 2. **安全合规性** - 采用国密SM3/SM4算法实现密码加密 - 请求签名符合海关金关工程安全规范 - 账户状态检查符合海关账户管理要求[^1] 3. **审计追踪** - 登出时触发审计日志(`FourAAuditLogoutHandler`) - 操作日志需包含海关要求的字段(用户编码、部门、操作类型) 4. **权限控制** ```java // 控制器方法级权限控制 @PreAuthorize("hasAuthority('CUSTOMS_DECLARE_ADMIN')") @PostMapping("/submit-declaration") public Response submitDeclaration(/*...*/) { // 报关业务逻辑 } ``` ### 部署注意事项 1. **账户同步机制** - 实现定时任务同步账户状态变更 - 处理4A系统不可用时的降级策略 2. **性能优化** ```java @Cacheable(value = "4aUsers", key = "#userCode") public FourAUserResponse getUserInfo(String userCode) { // 添加缓存减少4A系统调用 } ``` 3. **安全加固** - 配置HTTPS双向认证 - 敏感数据使用国密算法加密传输 - 实现海关要求的审计日志格式[^3] > 系统集成需遵循海关金关工程标准规范,包括账户生命周期管理、安全审计、加密算法等要求[^1][^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值