SpringBoot 实现 JWT 认证完整方案

JWT(JSON Web Token)是一种用于在网络应用间安全传递信息的开放标准,特别适合分布式系统的身份验证。下面我将详细介绍如何在SpringBoot项目中实现JWT认证。

一、JWT 基础概念

JWT由三部分组成,用.连接:

  1. Header(头部):包含令牌类型和使用的加密算法
  2. Payload(载荷):包含用户信息和声明
  3. Signature(签名):使用密钥对前两部分进行签名,确保信息不被篡改

二、项目准备

1. 添加依赖

pom.xml中添加JWT相关依赖:

<!-- JWT依赖 -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.19.2</version>
</dependency>
<!-- Spring Web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok(可选,用于简化代码) -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

2. 配置文件

application.yml中添加JWT相关配置:

spring:
  application:
    name: springboot-jwt-demo

jwt:
  # 密钥
  secret: your-secret-key-change-in-production
  # 过期时间(毫秒)
  expire-time: 86400000
  # 请求头中Token的名称
  header: Authorization
  # Token前缀
  token-prefix: Bearer

三、核心代码实现

1. JWT工具类

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Calendar;
import java.util.Map;

@Component
public class JWTUtil {
    
    @Value("${jwt.secret}")
    private String secret;
    
    @Value("${jwt.expire-time}")
    private long expireTime;
    
    /**
     * 生成Token
     * @param claims 要存储在Token中的信息
     * @return 生成的Token字符串
     */
    public String generateToken(Map<String, String> claims) {
        // 设置过期时间
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.MILLISECOND, (int) expireTime);
        
        // 创建JWT Builder
        JWTCreator.Builder builder = JWT.create();
        
        // 添加载荷
        claims.forEach(builder::withClaim);
        
        // 设置签名和过期时间,生成Token
        return builder.withExpiresAt(instance.getTime())
                .sign(Algorithm.HMAC256(secret));
    }
    
    /**
     * 验证Token
     * @param token 要验证的Token
     * @return 验证后的DecodedJWT对象
     * @throws JWTVerificationException 验证失败时抛出异常
     */
    public DecodedJWT verifyToken(String token) {
        return JWT.require(Algorithm.HMAC256(secret))
                .build()
                .verify(token);
    }
    
    /**
     * 解析Token
     * @param token 要解析的Token
     * @return 解析后的DecodedJWT对象
     */
    public DecodedJWT parseToken(String token) {
        return JWT.decode(token);
    }
    
    /**
     * 从Token中获取指定的Claim
     * @param token Token字符串
     * @param claimName Claim名称
     * @return Claim值
     */
    public String getClaim(String token, String claimName) {
        DecodedJWT decodedJWT = parseToken(token);
        return decodedJWT.getClaim(claimName).asString();
    }
}

2. JWT拦截器

import com.auth0.jwt.exceptions.JWTVerificationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;

@Component
@Slf4j
public class JWTInterceptor implements HandlerInterceptor {
    
    @Autowired
    private JWTUtil jwtUtil;
    
    @Value("${jwt.header}")
    private String jwtHeader;
    
    @Value("${jwt.token-prefix}")
    private String tokenPrefix;
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String token = request.getHeader(jwtHeader);
        
        // 检查请求头中是否包含Token
        if (token == null || "".equals(token.trim())) {
            response401(response, "Token不存在");
            return false;
        }
        
        // 移除Token前缀
        if (token.startsWith(tokenPrefix)) {
            token = token.replace(tokenPrefix, "").trim();
        } else {
            response401(response, "Token格式错误");
            return false;
        }
        
        try {
            // 验证Token
            jwtUtil.verifyToken(token);
            log.info("Token验证成功");
            return true;
        } catch (JWTVerificationException e) {
            log.error("Token验证失败: {}", e.getMessage());
            response401(response, "Token无效或已过期");
            return false;
        }
    }
    
    /**
     * 返回401错误响应
     */
    private void response401(HttpServletResponse response, String message) {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        
        try (PrintWriter out = response.getWriter()) {
            out.append("{"code": 401, "message": "").append(message).append(""}");
        } catch (Exception e) {
            log.error("响应错误: {}", e.getMessage());
        }
    }
}

3. 拦截器配置

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    
    @Autowired
    private JWTInterceptor jwtInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 添加拦截器,拦截需要认证的路径
        registry.addInterceptor(jwtInterceptor)
                // 需要拦截的路径
                .addPathPatterns("/api/**")
                // 排除不需要拦截的路径
                .excludePathPatterns("/api/login", "/api/register", "/error");
    }
}

4. 实体类

import lombok.Data;

@Data
public class LoginRequest {
    private String username;
    private String password;
}

@Data
public class LoginResponse {
    private String token;
    private String username;
    private String message;
}

5. 用户服务接口

public interface UserService {
    boolean login(String username, String password);
    boolean register(String username, String password);
}

6. 用户服务实现

import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

@Service
public class UserServiceImpl implements UserService {
    
    // 模拟数据库,实际项目中应使用数据库存储
    private final Map<String, String> userMap = new HashMap<>();
    
    public UserServiceImpl() {
        // 初始化测试用户
        userMap.put("admin", "123456");
    }
    
    @Override
    public boolean login(String username, String password) {
        return userMap.containsKey(username) && userMap.get(username).equals(password);
    }
    
    @Override
    public boolean register(String username, String password) {
        if (userMap.containsKey(username)) {
            return false; // 用户名已存在
        }
        userMap.put(username, password);
        return true;
    }
}

7. 登录控制器

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api")
public class AuthController {
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private JWTUtil jwtUtil;
    
    @Value("${jwt.token-prefix}")
    private String tokenPrefix;
    
    /**
     * 用户登录
     */
    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest request) {
        if (userService.login(request.getUsername(), request.getPassword())) {
            // 构建Token中的载荷信息
            Map<String, String> claims = new HashMap<>();
            claims.put("username", request.getUsername());
            
            // 生成Token
            String token = jwtUtil.generateToken(claims);
            
            // 构建响应
            LoginResponse response = new LoginResponse();
            response.setToken(tokenPrefix + " " + token);
            response.setUsername(request.getUsername());
            response.setMessage("登录成功");
            
            return ResponseEntity.ok(response);
        }
        return ResponseEntity.badRequest().body("用户名或密码错误");
    }
    
    /**
     * 用户注册
     */
    @PostMapping("/register")
    public ResponseEntity<?> register(@RequestBody LoginRequest request) {
        if (userService.register(request.getUsername(), request.getPassword())) {
            return ResponseEntity.ok("注册成功");
        }
        return ResponseEntity.badRequest().body("用户名已存在");
    }
    
    /**
     * 测试接口,需要验证Token
     */
    @GetMapping("/test")
    public ResponseEntity<?> test() {
        return ResponseEntity.ok("访问成功,Token有效");
    }
}

四、安全性考虑

  1. 密钥安全:密钥必须保密,生产环境应使用强密钥
  2. Token过期时间:设置合理的过期时间,避免永久有效
  3. HTTPS传输:在生产环境中使用HTTPS传输Token
  4. 防XSS攻击:Token存储在localStorage时可能被XSS攻击窃取
  5. Token刷新机制:实现Token刷新机制,避免频繁登录
  6. 黑名单机制:对于已注销的Token,添加到黑名单中

五、优化建议

  1. 使用Redis存储Token:可以更灵活地管理Token生命周期
  2. 实现Refresh Token:提供长期的Refresh Token用于获取新的Access Token
  3. 增加角色权限:在Token中添加用户角色信息,实现更细粒度的权限控制
  4. 使用非对称加密:生产环境可考虑使用RSA等非对称加密算法
  5. 添加IP绑定:在Token中绑定用户IP,增加安全性
  6. 添加日志记录:记录Token的生成、验证和使用情况

六、测试方法

  1. 启动SpringBoot应用
  2. 使用Postman或其他工具测试登录接口:POST http://localhost:8080/api/login
  3. 在请求体中添加用户名和密码
  4. 成功后将返回的Token添加到请求头
  5. 访问需要认证的接口:GET http://localhost:8080/api/test

这个实现提供了SpringBoot中JWT认证的完整流程,包括Token生成、验证和拦截器实现。您可以根据实际需求进行调整和扩展。

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]