Spring Boot Security结合JWTAuthenticationFilter/JWTTokenizer实现Argon2加盐密码与用户问卷结果关联的技术问询
我来帮你拆解一下这个问题的核心,以及一步步解决的方案——你现在的困惑主要来自对Argon2特性的误解和对JWT传递自定义信息的不熟悉,咱们逐个击破:
首先纠正核心误区:不要用动态生成的密码哈希作为结果关联键
你当前的思路有个根本性问题:Argon2的设计就是每次调用encode()都会生成随机盐,同一个密码每次生成的哈希完全不同,这是防止彩虹表攻击的核心安全特性,不可能让它生成重复的哈希。所以用动态生成的密码哈希绑定问卷结果,从一开始就行不通。
正确的关联思路:两种可选方案
你需要的是一个和用户绑定的、固定的唯一标识来关联问卷结果,同时确保只有用户本人能访问。这里有两个最靠谱的方案:
方案A:基于用户身份的直接关联(推荐)
这是最符合Spring Security设计逻辑的方案:
- 给
Result实体添加userId字段,关联用户表的主键(比如Long userId) - 用户填写问卷时,从当前认证的
Authentication对象中获取用户ID(登录后Spring Security会自动把用户信息存入SecurityContext),将其存入Result的userId字段 - 用户登录后查询结果时,直接用
userId = 当前登录用户ID过滤即可——Spring Security的授权机制已经确保只有本人能访问自己的数据,完全不需要依赖密码哈希
方案B:基于固定密码哈希的匿名关联(如果需要匿名但仅本人可访问)
如果你的场景需要问卷结果不直接关联用户ID(比如匿名问卷但仅提交者能查看),可以这样做:
- 用户注册时,一次性生成固定的Argon2哈希:
- 生成一个随机盐(或者用用户ID作为盐,确保唯一性)
- 用这个固定盐对用户密码进行Argon2哈希,得到固定的
userHash - 将盐和
userHash一起存入用户表(因为Argon2的matches()方法需要盐来验证)
- 用户填写问卷后,从用户表中取出该用户的
userHash,存入Result的userHash字段 - 用户登录后,从用户表中取出
userHash,通过JWT传递给后续接口,用来查询对应的结果
同事建议的具体实现(对应方案B或需要传递自定义标识的场景)
同事说的本质是:把用户的关联标识(比如userHash或用户ID)存入JWT的自定义Claim中,这样在授权阶段可以直接从JWT解析出该标识,无需重复查询数据库。以下是具体调整步骤:
1. 修改JWTTokenizer,添加自定义Claim
在生成JWT时,把userHash(或用户ID)加入Claim:
public String getAuthToken(String user, List<String> roles, String userHash) { // 新增userHash参数 byte[] signingKey = securityProperties.getJwtSecret().getBytes(); String token = Jwts.builder() .signWith(Keys.hmacShaKeyFor(signingKey), SignatureAlgorithm.HS512) .setHeaderParam("typ", securityProperties.getJwtType()) .setIssuer(securityProperties.getJwtIssuer()) .setAudience(securityProperties.getJwtAudience()) .setSubject(user) .setExpiration(new Date(System.currentTimeMillis() + securityProperties.getJwtExpirationTime())) .claim("rol", roles) .claim("userHash", userHash) // 新增自定义Claim .compact(); return securityProperties.getAuthTokenPrefix() + token; }
2. 修改JWTAuthenticationFilter,传递关联标识
在认证成功后,从自定义UserDetails中取出userHash,传给JWTTokenizer生成Token:
@Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { CustomUserDetails userDetails = (CustomUserDetails) authResult.getPrincipal(); // 假设你用了自定义UserDetails List<String> roles = userDetails.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList()); String userHash = userDetails.getUserHash(); // 从用户详情中获取预先生成的固定哈希 String token = jwtTokenizer.getAuthToken(userDetails.getUsername(), roles, userHash); response.addHeader("Authorization", token); }
3. 修改JWTAuthorizationFilter,解析自定义Claim并生成自定义AuthenticationToken
在授权阶段,从JWT中取出userHash,存入自定义的AuthenticationToken,方便后续业务逻辑使用:
public class JWTAuthorizationFilter extends BasicAuthenticationFilter { private final JWTTokenizer jwtTokenizer; public JWTAuthorizationFilter(AuthenticationManager authenticationManager, JWTTokenizer jwtTokenizer) { super(authenticationManager); this.jwtTokenizer = jwtTokenizer; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String header = request.getHeader("Authorization"); if (header == null || !header.startsWith(jwtTokenizer.getSecurityProperties().getAuthTokenPrefix())) { chain.doFilter(request, response); return; } Authentication authentication = getAuthentication(request); SecurityContextHolder.getContext().setAuthentication(authentication); chain.doFilter(request, response); } private Authentication getAuthentication(HttpServletRequest request) { String token = request.getHeader("Authorization").replace(jwtTokenizer.getSecurityProperties().getAuthTokenPrefix(), ""); if (token != null) { Claims claims = Jwts.parserBuilder() .setSigningKey(Keys.hmacShaKeyFor(jwtTokenizer.getSecurityProperties().getJwtSecret().getBytes())) .build() .parseClaimsJws(token) .getBody(); String username = claims.getSubject(); List<String> roles = (List<String>) claims.get("rol"); String userHash = claims.get("userHash", String.class); // 取出自定义Claim // 生成自定义AuthenticationToken,携带userHash Collection<? extends GrantedAuthority> authorities = roles.stream() .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); CustomAuthenticationToken authToken = new CustomAuthenticationToken(username, null, authorities); authToken.setUserHash(userHash); // 将userHash存入Token return authToken; } return null; } }
4. 在业务逻辑中使用关联标识
当用户访问问卷结果时,从SecurityContext中取出userHash,用来查询对应的结果:
@GetMapping("/my-results") public ResponseEntity<List<Result>> getMyResults() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth instanceof CustomAuthenticationToken customAuth) { String userHash = customAuth.getUserHash(); List<Result> results = resultRepository.findByUserHash(userHash); return ResponseEntity.ok(results); } return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); }
你的核心疑问解答
何时生成加盐哈希?
- 如果用方案B:用户注册时一次性生成,并将盐和哈希一起存入用户表,之后不再重新生成,直接复用即可。
- 如果用方案A:不需要生成密码哈希,直接用用户ID关联。
如何确保每次登录得到相同的哈希?
只有在使用固定盐的情况下,Argon2才能生成相同的哈希。所以必须在注册时一次性生成盐和哈希,并存入用户表,后续登录时直接取出已有的哈希,而不是重新调用encode()。同事的建议具体指什么?
同事建议你把用于关联问卷结果的标识(比如userHash或用户ID)存入JWT的自定义Claim中,这样在后续请求中,无需再次查询数据库获取该标识,直接从JWT解析即可;同时通过自定义AuthenticationToken把这个标识传递给业务逻辑,用来查询对应的问卷结果。
内容的提问来源于stack exchange,提问作者The Doctor

