基于JWT生成Principal并设置到SecurityContext的实现咨询
问题分析
你之前的思路存在核心误区:SecurityContextHolder的上下文是线程绑定的,/principal接口中设置的认证信息仅对当前请求线程有效。请求处理完成后线程销毁,上下文也会被清空,后续的/books请求属于新线程,自然没有认证信息,所以返回403。
正确的做法是通过Spring Security的请求过滤器,在每个请求到达时自动解析JWT Token并注入认证信息,而不是手动通过接口设置。
解决方案步骤
1. 编写JWT工具类(解析Token、转换权限)
基于你生成Token时使用的JJWT库,实现Token解析和权限转换逻辑:
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import org.springframework.security.core.authority.SimpleGrantedAuthority; import java.util.List; import java.util.stream.Collectors; public class JwtUtils { // 和生成Token时一致的密钥 private static final String SECRET_KEY = "mysecret"; // 解析Token并获取Claims public static Claims parseToken(String token) { // 移除Bearer前缀(如果请求头用Bearer格式) String cleanToken = token.startsWith("Bearer ") ? token.substring(7) : token; return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(cleanToken) .getBody(); } // 将Token中的securityGroups转换为Spring Security的权限对象 public static List<SimpleGrantedAuthority> getAuthorities(Claims claims) { List<String> groups = (List<String>) claims.get("securityGroups"); // 加上ROLE_前缀适配Spring Security默认的权限规则 return groups.stream() .map(group -> new SimpleGrantedAuthority("ROLE_" + group)) .collect(Collectors.toList()); } }
2. 实现JWT认证过滤器
自定义过滤器,在每个请求到达时自动解析Token并设置认证信息:
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import io.jsonwebtoken.Claims; public class JwtAuthenticationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 从请求头获取Token(约定用Authorization: Bearer <token>格式) String authHeader = request.getHeader("Authorization"); if (authHeader != null && authHeader.startsWith("Bearer ")) { try { Claims claims = JwtUtils.parseToken(authHeader); String username = claims.getSubject(); // 获取用户权限 var authorities = JwtUtils.getAuthorities(claims); // 创建认证对象并注入上下文 UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( username, null, authorities ); SecurityContextHolder.getContext().setAuthentication(authentication); } catch (Exception e) { // Token无效时清空上下文,返回401 SecurityContextHolder.clearContext(); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "无效的JWT Token"); return; } } // 继续执行后续过滤链 filterChain.doFilter(request, response); } }
3. 修改Spring Security配置
将自定义过滤器加入安全过滤链,替换默认的认证逻辑:
import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @EnableWebSecurity public class SecurityConf extends WebSecurityConfigurerAdapter { @Bean public JwtAuthenticationFilter jwtAuthenticationFilter() { return new JwtAuthenticationFilter(); } @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/principal").permitAll() // 保留该接口(可选,可后续删除) .anyRequest().authenticated() .and() // 在默认的用户名密码过滤器之前注入JWT过滤器 .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); } }
4. 权限控制(可选)
如果需要对特定接口做权限限制,比如仅允许group1访问/books,可以修改配置:
// 在SecurityConf的configure方法中添加 .antMatchers("/books").hasRole("group1")
或者在接口上使用注解:
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class BookController { @GetMapping("/books") @PreAuthorize("hasRole('group1')") public List<String> getBooks() { return List.of("book1", "book2"); } }
后续使用说明
- 前端在获取到JWT Token后,后续所有请求都需要在请求头中携带:
Authorization: Bearer <你的Token> - 原
/principal接口可以保留用来测试Token解析,但不再需要手动设置SecurityContext,过滤器会自动处理所有请求的认证
内容的提问来源于stack exchange,提问作者iPhoneJavaDev
相关产品推荐
相关产品推荐

