You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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处细节需要优化:

  1. 规范令牌提取逻辑
    确保从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;
    }
    
  2. 添加异常处理
    令牌无效/过期时,直接返回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);
    }
    
  3. 保证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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 05:27:25