如何结合Spring Security、容器安全与JWT实现REST登录服务?
可行性结论
需求完全可实现,核心逻辑是自定义Spring Security认证Provider对接容器内置的用户体系做身份校验,再整合JWT的生成、校验逻辑即可,具体实现步骤如下:
第一步:引入核心依赖
在Spring Boot项目的pom.xml(Gradle配置同理)中引入必要依赖:
<!-- Spring Security 核心依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- JJWT JWT处理依赖 --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency>
第二步:自定义容器用户认证Provider
对接容器的用户查询API完成身份校验,以下是Tomcat容器的实现示例,Jetty等其他容器替换对应用户查询接口即可:
@Component public class ContainerUserAuthProvider implements AuthenticationProvider { // 注入Tomcat内置的用户数据库实例,外置Tomcat部署时可通过JNDI获取 @Autowired private UserDatabase userDatabase; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String username = authentication.getName(); String inputPassword = authentication.getCredentials().toString(); // 从容器用户库查询用户 User containerUser = userDatabase.findUser(username); if (containerUser == null) { throw new BadCredentialsException("用户名不存在"); } // 密码校验,加密规则要和容器配置的密码加密规则一致 if (!new BCryptPasswordEncoder().matches(inputPassword, containerUser.getPassword())) { throw new BadCredentialsException("密码错误"); } // 提取用户角色生成权限列表 List<GrantedAuthority> authorities = containerUser.getRoles().stream() .map(role -> new SimpleGrantedAuthority(role.getName())) .collect(Collectors.toList()); return new UsernamePasswordAuthenticationToken(username, inputPassword, authorities); } @Override public boolean supports(Class<?> authentication) { return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); } }
第三步:封装JWT工具类
实现JWT的生成、校验、解析逻辑:
@Component public class JwtUtils { // JWT签名密钥,建议配置在application.yml或配置中心 @Value("${jwt.secret}") private String jwtSecret; // JWT有效期,默认24小时 @Value("${jwt.expire-ms:86400000}") private long expireMs; // 生成JWT Token public String generateToken(String username, Collection<? extends GrantedAuthority> authorities) { Map<String, Object> claims = new HashMap<>(); claims.put("roles", authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList())); return Jwts.builder() .setClaims(claims) .setSubject(username) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + expireMs)) .signWith(SignatureAlgorithm.HS512, jwtSecret) .compact(); } // 校验JWT有效性 public boolean validateToken(String token) { try { Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token); return true; } catch (JwtException | IllegalArgumentException e) { return false; } } // 从JWT解析认证信息 public Authentication getAuthentication(String token) { Claims claims = Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody(); List<String> roles = claims.get("roles", List.class); List<GrantedAuthority> authorities = roles.stream() .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); return new UsernamePasswordAuthenticationToken(claims.getSubject(), null, authorities); } }
第四步:实现登录接口
接收前端传入的用户名密码,完成认证后返回JWT:
@RestController @RequestMapping("/auth") public class AuthController { @Autowired private AuthenticationManager authenticationManager; @Autowired private JwtUtils jwtUtils; @PostMapping("/login") public Map<String, String> login(@RequestBody LoginDTO loginDTO) { Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(loginDTO.getUsername(), loginDTO.getPassword()) ); SecurityContextHolder.getContext().setAuthentication(authentication); String token = jwtUtils.generateToken(authentication.getName(), authentication.getAuthorities()); return Collections.singletonMap("token", token); } } // LoginDTO结构示例,仅需包含两个字段 // @Data // public class LoginDTO { // private String username; // private String password; // }
第五步:实现JWT校验过滤器
所有业务接口请求先经过该过滤器完成JWT校验:
public class JwtAuthFilter extends OncePerRequestFilter { @Autowired private JwtUtils jwtUtils; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = parseTokenFromRequest(request); if (token != null && jwtUtils.validateToken(token)) { Authentication authentication = jwtUtils.getAuthentication(token); SecurityContextHolder.getContext().setAuthentication(authentication); } filterChain.doFilter(request, response); } // 从请求头提取JWT private String parseTokenFromRequest(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (bearerToken != null && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } }
第六步:Spring Security 规则配置
将自定义的认证Provider和JWT过滤器注册到安全配置中:
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private ContainerUserAuthProvider containerUserAuthProvider; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 注册自定义容器认证Provider auth.authenticationProvider(containerUserAuthProvider); } @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() // 采用无状态认证,不使用Session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/auth/login").permitAll() // 登录接口放行 .anyRequest().authenticated() // 其余所有接口需要认证 .and() // 将JWT过滤器添加到用户名密码认证过滤器之前 .addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean public JwtAuthFilter jwtAuthFilter() { return new JwtAuthFilter(); } }
第七步:前端适配
React端登录页发起POST请求到/auth/login,携带JSON格式的用户名密码,拿到返回的token后存储在localStorage/sessionStorage中,后续所有业务请求的请求头都携带Authorization: Bearer <token>即可。
注意事项
- 容器侧的用户配置(如Tomcat的tomcat-users.xml)要保证用户名、密码、角色配置正确,密码加密规则和代码中的密码编码器保持一致。
- JWT签名密钥生产环境禁止硬编码,建议通过环境变量或配置中心注入。
- 可根据业务需求补充JWT刷新、过期重试逻辑,不影响核心流程。
内容的提问来源于stack exchange,提问作者Benno
相关产品推荐
相关产品推荐

