Spring Security+JWT实现安全CRUD API:权限验证与用户ID获取咨询
Spring Security JWT 授权验证相关问题解答
项目背景
我正在开发基于Spring Security框架的CRUD API项目,需通过JWT令牌实现授权验证。现有三个端点:
- 用户注册/登录的认证控制器
- 验证授权生效的测试GET接口
- 更新当前用户信息(修改favoriteAnimal字段)的接口
用户实体类代码
public class UserEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; private String password; private String favoriteAnimal; }
更新接口代码
@PostMapping("/update") public ResponseEntity<String> updatePet(@RequestBody UserDTO user) { return new ResponseEntity<>(UserSevices.updateUserInfo, HttpStatus.OK); }
疑问与解答
1. 如何确保调用更新接口的用户是持有有效JWT令牌的正确用户?
- 第一步:配置接口拦截规则
在Spring Security配置类中,给更新接口设置认证拦截,未携带有效JWT的请求直接被拦截:@Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/auth/**").permitAll() // 注册/登录接口放行 .antMatchers("/update").authenticated() // 更新接口需认证 .anyRequest().authenticated(); } - 第二步:校验当前用户与请求用户一致性
在接口中从SecurityContextHolder获取当前认证用户信息,对比请求目标用户是否为自身:@PostMapping("/update") public ResponseEntity<String> updatePet(@RequestBody UserDTO user) { // 获取当前认证用户的用户名 String currentUsername = ((UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername(); // 校验是否为本人操作 if (!currentUsername.equals(user.getUsername())) { return new ResponseEntity<>("无权限修改其他用户信息", HttpStatus.FORBIDDEN); } return new ResponseEntity<>(userServices.updateUserInfo(user), HttpStatus.OK); }
2. 自定义的AuthorizationFilter是否会被请求经过?实现是否正确?
是否会经过该过滤器?
默认不会,必须在Spring Security配置类中手动注册该过滤器,指定其执行顺序(通常放在UsernamePasswordAuthenticationFilter之前):
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private AuthorizationFilter authorizationFilter; @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/auth/**").permitAll() .anyRequest().authenticated() .and() // 注册自定义JWT过滤器 .addFilterBefore(authorizationFilter, UsernamePasswordAuthenticationFilter.class); } }
完成配置后,所有请求都会经过该过滤器。
过滤器实现是否正确?
整体逻辑可行,但有3处细节需要优化:
- 规范令牌提取逻辑
确保从Authorization头的Bearer <token>格式中正确提取令牌:private String getJWTFromRequest(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } - 添加异常处理
令牌无效/过期时,直接返回401错误响应,避免请求继续流转:@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = getJWTFromRequest(request); try { if(StringUtils.hasText(token) && tokenGenerator.validateToken(token)) { String username = tokenGenerator.getUsernameFromJWT(token); UserDetails userDetails = customUserDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authenticationToken); } } catch (JwtException e) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write("无效或过期的JWT令牌"); return; } filterChain.doFilter(request, response); } - 保证UserDetails完整性
自定义UserDetails实现类,包含用户ID等业务所需字段,方便后续使用。
3. 服务层中如何从JWT获取用户ID?是否需要在生成JWT时加入ID字段?
必须在生成JWT时将用户ID存入令牌的Claims中,才能在后续解析获取,使用JJWT库的实现步骤如下:
1. 生成JWT时加入用户ID
public String generateToken(UserDetails userDetails, Long userId) { Map<String, Object> claims = new HashMap<>(); claims.put("userId", userId); // 将用户ID存入Claims return Jwts.builder() .setClaims(claims) .setSubject(userDetails.getUsername()) .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10小时过期 .signWith(SignatureAlgorithm.HS512, "your-secret-key") // 替换为你的密钥 .compact(); }
用户登录成功后调用该方法,传入用户实体的ID:
String token = tokenGenerator.generateToken(userDetails, userEntity.getId());
2. 从JWT中解析用户ID
在TokenGenerator类中添加解析方法:
public Long getUserIdFromJWT(String token) { Claims claims = Jwts.parser() .setSigningKey("your-secret-key") .parseClaimsJws(token) .getBody(); return claims.get("userId", Long.class); }
3. 服务层中获取用户ID
更优雅的方式是通过自定义UserDetails存入上下文,直接获取:
// 自定义UserDetails实现类 public class CustomUserDetails implements UserDetails { private Long id; private String username; private String password; // 省略其他字段、构造方法及接口实现 public Long getId() { return id; } }
在customUserDetailsService.loadUserByUsername中返回该实例,服务层即可直接获取:
public String updateUserInfo(UserDTO userDTO) { CustomUserDetails currentUser = (CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); Long userId = currentUser.getId(); // 根据ID查询用户并更新字段 UserEntity user = userRepository.findById(userId).orElseThrow(() -> new RuntimeException("用户不存在")); user.setFavoriteAnimal(userDTO.getFavoriteAnimal()); userRepository.save(user); return "更新成功"; }
内容的提问来源于stack exchange,提问作者Leo El
相关产品推荐
相关产品推荐

