Spring Boot无状态JWT认证疑问:查库、上下文存储等问题
我是Spring Boot新手,之前用NodeJS(Javascript)开发。NodeJS里JWT令牌的生成和验证不用写太多代码,但Spring Boot里要写不少。研究了4天Spring Boot的JWT实现,反而越来越困惑:有些教程会在过滤器里给每个请求获取UserDetails。如果每个请求都要查数据库拿UserDetails,那JWT的意义何在?
示例代码如下:
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { //Authorization String requestHeader = request.getHeader("Authorization"); //Bearer 2352345235sdfrsfgsdfsdf logger.info(" Header : {}", requestHeader); String username = null; String token = null; if (requestHeader != null && requestHeader.startsWith("Bearer")) { //looking good token = requestHeader.substring(7); try { username = this.jwtHelper.getUsernameFromToken(token); } catch (IllegalArgumentException e) { logger.info("Illegal Argument while fetching the username !!"); e.printStackTrace(); } catch (ExpiredJwtException e) { logger.info("Given jwt token is expired !!"); e.printStackTrace(); } catch (MalformedJwtException e) { logger.info("Some changed has done in token !! Invalid Token"); e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } } else { logger.info("Invalid Header Value !! "); } // if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { //fetch user detail from username but why?? UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); Boolean validateToken = this.jwtHelper.validateToken(token, userDetails); if (validateToken) { //set the authentication UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } else { logger.info("Validation fails !!"); } } filterChain.doFilter(request, response); }
在最后的if条件里,要获取用户详情来验证令牌,那用JWT还有什么意义?另外我还疑惑为什么要把验证通过的用户信息存入应用的SecurityContext,就算部署在多Docker容器里,请求也不一定路由到持有这个上下文的实例。具体疑问如下:
- 为何后续每个请求都要获取用户详情?JWT不是无状态的吗?这不会增加数据库查询负载吗?
- 能否将用户名、邮箱、权限等用户信息存入JWT负载,验证时直接提取,无需查询数据库?
- 为何要将用户信息存入SecurityContext?若应用部署在多Docker容器中,请求不一定路由到持有该上下文的实例。
- 必须使用内置的UserDetails类进行验证吗?能否自定义用户详情逻辑?多数YouTube教程都使用UserDetails类。
解答
1. 为什么每个请求都要查用户详情?
这种写法是为了双重验证:一方面校验JWT本身的签名、有效期等合法性,另一方面通过数据库确认用户状态(比如账号是否被封禁)。但这确实违背了JWT无状态的设计初衷,会额外增加数据库负载。
如果你的业务场景不需要实时校验用户状态(比如允许令牌有效期内,用户即使被封禁仍能正常访问),完全可以去掉这个数据库查询,只验证JWT的签名和有效期即可。
2. 能不能把用户信息存入JWT负载?
当然可以。JWT的Payload部分就是用来存储自定义字段的,你可以把用户名、邮箱、权限列表等信息放进去。验证时直接从Payload里提取数据,无需查询数据库。
需要注意:Payload是Base64明文编码的,绝对不能存敏感信息(比如密码);另外Payload内容越多,JWT令牌越长,会增加请求开销;如果用户权限等信息在令牌有效期内发生变化,旧令牌仍会保留旧权限,除非你额外实现令牌黑名单等失效机制。
3. 为什么要存入SecurityContext?
Spring Security的整个权限体系都是基于SecurityContext运转的,后续的接口权限校验(比如@PreAuthorize注解)、获取当前登录用户信息,都是从这个上下文里读取数据。
至于多Docker容器的问题:SecurityContext是请求线程级别的,每个请求进来时在当前处理线程中创建,请求结束后销毁,和容器实例无关。不管请求路由到哪个容器,只要在处理请求的线程里设置了SecurityContext,后续逻辑就能正常获取到数据,不存在跨容器共享的问题。
4. 必须用UserDetails吗?能不能自定义?
不是必须的,但UserDetails是Spring Security提供的标准接口,用来统一封装用户核心信息(用户名、密码、权限等),和框架的其他组件(比如AuthenticationManager、UserDetailsService)兼容性最好,所以大部分教程都会使用它。
你完全可以自定义用户信息类,只要在创建Authentication对象时传入自定义主体即可。示例代码如下:
// 自定义用户信息类 public class CustomUser { private String username; private String email; private List<String> roles; // getter、setter方法 } // 在过滤器中创建Authentication时传入自定义对象 CustomUser customUser = new CustomUser(); // 从JWT Payload中提取数据赋值给customUser UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(customUser, null, authorities); SecurityContextHolder.getContext().setAuthentication(authentication);
后续就能通过SecurityContextHolder拿到自定义的CustomUser对象了。
内容的提问来源于stack exchange,提问作者Rishabh Raghwendra

