Spring Boot + JWT实现登录认证,密码加密及Token校验全过程

项目简介

本项目是一个基于Spring Boot + JWT的前后端分离认证系统,使用拦截器而非过滤器进行JWT验证。项目包含用户注册、登录、JWT生成、Token验证、权限控制等完整功能。

JWT认证原理

通俗地说,JWT的本质就是一个字符串,它是将用户信息保存到一个Json字符串中,然后进行编码后得到一个JWT token,并且这个JWT token带有签名信息,接收后可以校验是否被篡改,所以可以用于在各方之间安全地将信息作为Json对象传输。

JWT认证流程

  1. 前端登录请求:前端通过Web表单将用户名和密码发送到后端的接口,这个过程一般是一个POST请求。建议的方式是通过SSL加密的传输(HTTPS),从而避免敏感信息被嗅探。

  2. 后端验证并生成Token:后端核对用户名和密码成功后,将包含用户信息的数据作为JWT的Payload,将其与JWT Header分别进行Base64编码拼接后签名,形成一个JWT Token,形成的JWT Token就是一个如同xxx.yyy.zzz的字符串。

  3. 返回Token:后端将JWT Token字符串作为登录成功的结果返回给前端。前端可以将返回的结果保存在浏览器中,退出登录时删除保存的JWT Token即可。

  4. 携带Token请求:前端在每次请求时将JWT Token放入HTTP请求头中的Authorization属性中(解决XSS和XSRF问题)。

  5. 后端验证Token:后端检查前端传过来的JWT Token,验证其有效性,比如检查签名是否正确、是否过期、token的接收方是否是自己等等。

  6. 业务处理:验证通过后,后端解析出JWT Token中包含的用户信息,进行其他逻辑操作(一般是根据用户信息得到权限等),返回结果。

image-20250908213049506

项目架构设计

核心组件

  • 用户实体(User): 实现UserDetails接口,包含用户信息和权限
  • JWT工具类(JwtUtils): 负责JWT的生成、解析和验证
  • JWT拦截器(JwtInterceptor): 拦截请求,验证JWT token
  • 认证控制器(AuthController): 处理登录和注册请求
  • 用户服务(UserService): 处理用户相关业务逻辑
  • 安全配置(SecurityConfig): Spring Security配置
  • 全局异常处理器: 统一处理各种异常情况

技术栈

  • Spring Boot 3.5.5: 主框架
  • Spring Security: 安全框架
  • Spring Data JPA: 数据访问层
  • H2 Database: 内存数据库(便于演示)
  • JJWT: JWT库
  • BCrypt: 密码加密

详细实现流程

1. 用户实体设计

@Entity
@Table(name = "users")
public class User implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false)
    private String username;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    private String email;

    @Enumerated(EnumType.STRING)
    private Role role = Role.USER;

    @Column(nullable = false)
    private boolean enabled = true;

    // 实现UserDetails接口方法
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Collections.singleton(new SimpleGrantedAuthority("ROLE_" + role.name()));
    }

    // 角色枚举
    public enum Role {
        USER, ADMIN
    }
    
    // 其他getter/setter方法...
}

关键点

  • 实现UserDetails接口,与Spring Security集成
  • 使用BCrypt加密密码
  • 支持USER和ADMIN两种角色

2. JWT工具类实现

@Component
public class JwtUtils {
    
    @Value("${app.jwt.secret:mySecretKey}")
    private String jwtSecret;

    @Value("${app.jwt.expiration:86400000}") // 24小时
    private int jwtExpirationMs;

    private SecretKey getSigningKey() {
        return Keys.hmacShaKeyFor(jwtSecret.getBytes());
    }

    // 生成JWT Token
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        return createToken(claims, userDetails.getUsername());
    }

    // 创建Token
    private String createToken(Map<String, Object> claims, String subject) {
        return Jwts.builder()
                .claims(claims)
                .subject(subject)
                .issuedAt(new Date(System.currentTimeMillis()))
                .expiration(new Date(System.currentTimeMillis() + jwtExpirationMs))
                .signWith(getSigningKey())
                .compact();
    }

    // 验证Token
    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = getUsernameFromToken(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }

    // 从Token中获取用户名
    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }
    
    // 其他工具方法...
}

关键功能

  • 使用HMAC SHA算法签名
  • 支持Token过期时间配置
  • 提供Token生成、验证、解析功能

3. JWT拦截器实现

@Component
public class JwtInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtUtils jwtUtils;

    @Autowired
    private CustomUserDetailsService userDetailsService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 获取Authorization头
        String authHeader = request.getHeader("Authorization");
        
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            String token = authHeader.substring(7); // 移除"Bearer "前缀
            
            try {
                // 验证token
                if (jwtUtils.validateToken(token)) {
                    String username = jwtUtils.getUsernameFromToken(token);
                    
                    // 如果用户未认证,则进行认证
                    if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                        
                        // 再次验证token与用户信息
                        if (jwtUtils.validateToken(token, userDetails)) {
                            UsernamePasswordAuthenticationToken authentication = 
                                new UsernamePasswordAuthenticationToken(
                                    userDetails, 
                                    null, 
                                    userDetails.getAuthorities()
                                );
                            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                            SecurityContextHolder.getContext().setAuthentication(authentication);
                        }
                    }
                }
            } catch (Exception e) {
                logger.error("JWT认证过程中发生错误: {}", e.getMessage());
            }
        }
        
        return true;
    }
}

关键特性

  • 使用拦截器而非过滤器
  • 自动从请求头提取JWT token
  • 验证成功后设置Spring Security上下文

4. 认证控制器

@RestController
@RequestMapping("/api/auth")
@CrossOrigin(origins = "*", maxAge = 3600)
public class AuthController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserService userService;

    @Autowired
    private JwtUtils jwtUtils;

    @PostMapping("/login")
    public ResponseEntity<ApiResponse<JwtResponse>> login(@Valid @RequestBody LoginRequest loginRequest) {
        try {
            // 认证用户
            Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                    loginRequest.getUsername(), 
                    loginRequest.getPassword()
                )
            );

            SecurityContextHolder.getContext().setAuthentication(authentication);
            
            // 获取用户详情
            User user = (User) authentication.getPrincipal();
            
            // 生成JWT token
            String jwt = jwtUtils.generateToken(user);

            // 获取用户角色
            List<String> roles = user.getAuthorities().stream()
                .map(item -> item.getAuthority())
                .collect(Collectors.toList());

            JwtResponse jwtResponse = new JwtResponse(jwt, user.getUsername(), user.getEmail(), roles);

            return ResponseEntity.ok(ApiResponse.success("登录成功", jwtResponse));
        } catch (Exception e) {
            return ResponseEntity.badRequest()
                .body(ApiResponse.error("用户名或密码错误"));
        }
    }

    @PostMapping("/register")
    public ResponseEntity<ApiResponse<String>> register(@Valid @RequestBody RegisterRequest registerRequest) {
        try {
            User user = userService.registerUser(registerRequest);
            return ResponseEntity.ok(ApiResponse.success("用户注册成功!用户名: " + user.getUsername()));
        } catch (RuntimeException e) {
            return ResponseEntity.badRequest()
                .body(ApiResponse.error(e.getMessage()));
        }
    }
}

登录流程

  1. 接收用户名和密码
  2. 使用Spring Security进行认证
  3. 认证成功后生成JWT token
  4. 返回token和用户信息

5. Spring Security配置

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/h2-console/**").permitAll()
                .requestMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .authenticationProvider(authenticationProvider());

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    // 其他配置...
}

配置要点

  • 禁用CSRF(前后端分离项目)
  • 设置无状态会话管理
  • 配置公开接口路径
  • 启用CORS支持

6. 拦截器注册配置

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private JwtInterceptor jwtInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor)
                .addPathPatterns("/api/**")  // 拦截所有API请求
                .excludePathPatterns(
                    "/api/auth/**",      // 排除认证相关接口
                    "/api/public/**",    // 排除公共接口
                    "/h2-console/**"     // 排除H2控制台
                );
    }
}

实际测试流程

1. 用户登录

请求

POST http://localhost:8080/api/auth/login
Content-Type: application/json

{
    "username": "user",
    "password": "123456"
}

响应

{
    "success": true,
    "message": "登录成功",
    "data": {
        "token": "eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJ1c2VyIiwiaWF0IjoxNzU3MzM5ODUyLCJleHAiOjE3NTc0MjYyNTJ9.mFfQgpO8iCEUQyHzdg9XOu_K8WZVwiZBVIFeazzYNbANwizDRqEn-hLtQy2DG9GI",
        "type": "Bearer",
        "username": "user",
        "email": "user@example.com",
        "roles": ["ROLE_USER"]
    }
}

2. 携带Token访问受保护资源

请求

GET http://localhost:8080/api/test/user
Authorization: Bearer eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJ1c2VyIiwiaWF0IjoxNzU3MzM5ODUyLCJleHAiOjE3NTc0MjYyNTJ9.mFfQgpO8iCEUQyHzdg9XOu_K8WZVwiZBVIFeazzYNbANwizDRqEn-hLtQy2DG9GI

响应

{
    "success": true,
    "message": "用户内容访问成功!",
    "data": "欢迎您,user!这是需要认证的内容。"
}