🛡️ 零基础搞懂Java安全认证:Spring Boot + JWT 实战全解析
在前后端分离和微服务架构大行其道的今天,传统的 Session 认证方式显得有些力不从心。它依赖服务器内存,难以应对分布式部署,还存在跨域等麻烦。
那么,现代应用是如何安全地识别“你是谁”的呢?答案就是 JWT (JSON Web Token)。
别被这些名词吓到,今天我们就用最通俗的语言和最直接的代码,带你从零开始,在 Spring Boot 项目中实现一套完整的 JWT 安全认证系统。学完这篇博客,你将彻底理解它的核心原理,并能亲手写出代码。
🤔 JWT 是什么?一个生动的比喻
想象一下你去游乐园玩:
-
传统 Session 模式:你买票入园时,工作人员给你一张实体手环。游乐园的每个项目(服务器)旁边都有一个查询台(数据库),工作人员需要扫描你的手环,去查询台核实你的票是否有效,才能让你玩。这很麻烦,而且查询台一旦拥堵,所有人都得等着。
-
JWT 模式:你买票入园时,工作人员给你一张带有特殊防伪标记的门票。这张门票本身就包含了你的信息(比如VIP等级),并且盖了游乐园的官方印章。当你去玩项目时,工作人员只需要检查一下门票上的印章是不是真的,再看看上面的信息,就可以直接让你玩,完全不需要去查询台核实。
JWT 就是那张“自带信息、自带防伪”的电子门票。
JWT 看起来是一长串字符,比如:xxxxx.yyyyy.zzzzz。它由三部分组成,用点 . 连接:
-
Header (头部)
- 作用:描述“门票”的类型和防伪技术。
- 内容:通常包含令牌类型(就是 JWT)和签名算法(比如 HMAC SHA256)。
- 注意:这部分只是编码,不是加密,谁都能看懂。
-
Payload (载荷)
- 作用:存放“门票”上的具体信息。
- 内容:包含两类信息:
- 标准声明:比如签发者 (
iss)、过期时间 (exp)、主题 (sub,通常是用户名或用户ID)。 - 自定义声明:你可以放任何想放的非敏感信息,比如用户的角色 (
role: "admin")。
- 标准声明:比如签发者 (
- 严重警告:Payload 也只是 Base64 编码,任何人都可以解码查看。所以,千万不要把用户密码等敏感信息放进去!
-
Signature (签名)
- 作用:这就是那个“官方印章”,用来防止门票被伪造或篡改。
- 生成方式:服务器会把编码后的 Header、编码后的 Payload,再加上一个只有服务器知道的密钥 (Secret Key),通过 Header 里指定的算法生成签名。
- 验证方式:当服务器收到 JWT 时,会用同样的方法重新计算一遍签名。如果计算结果和 JWT 里的签名一模一样,就说明这个 JWT 是合法的,且内容没有被修改过。
🛠️ 动手实践:用 Spring Boot 实现 JWT 认证
理论讲完,现在开始动手。我们将构建一个简单的应用,实现登录、颁发令牌、验证令牌的全流程。
技术栈:Spring Boot 3.x, JDK 17, Maven
第一步:搭建项目骨架 (pom.xml)
首先,创建一个 Maven 项目,并在 pom.xml 中添加我们需要的依赖。
spring-boot-starter-web:用于创建 Web 应用。spring-boot-starter-security:Spring 的安全框架,我们用它来处理认证逻辑。jjwt系列依赖:这是一个非常流行的 Java JWT 库,帮助我们轻松地生成和解析 JWT。
<!-- 在 <dependencies> 标签内添加 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT 核心 API -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<!-- JWT 运行时实现 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- JWT JSON 序列化 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
第二步:创建核心组件
我们的项目结构会非常清晰,主要包含以下几个部分:
src/main/java/com/example/demo
├── config // 安全配置
│ ├── SecurityConfig.java
│ └── JwtAuthFilter.java
├── controller // 接口层
│ ├── AuthController.java
│ └── DemoController.java
├── dto // 数据传输对象
│ ├── LoginRequest.java
│ └── AuthResponse.java
├── service // 业务逻辑
│ ├── JwtService.java
│ └── UserService.java
└── DemoApplication.java
1. JWT 工具类 (JwtService.java)
这个类是我们的“制票中心”,专门负责生成和验证 JWT。
package com.example.demo.service;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Service
public class JwtService {
// 密钥,生产环境一定要放在配置文件里,并且要足够复杂!
// 长度至少32位,这里为了演示方便
private static final String SECRET_KEY = "MySuperSecretKeyForJWTHMACSHA256SigningWhichIsLongEnough";
private SecretKey getSignInKey() {
return Keys.hmacShaKeyFor(SECRET_KEY.getBytes());
}
// 从 Token 中提取用户名
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
// 生成 Token
public String generateToken(UserDetails userDetails) {
return generateToken(new HashMap<>(), userDetails);
}
// 核心生成逻辑
public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
return Jwts.builder()
.setClaims(extraClaims) // 可以放自定义信息
.setSubject(userDetails.getUsername()) // 设置主题,通常是用户名
.setIssuedAt(new Date(System.currentTimeMillis())) // 签发时间
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 24)) // 过期时间,24分钟
.signWith(getSignInKey(), SignatureAlgorithm.HS256) // 签名
.compact();
}
// 验证 Token 是否有效
public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
// 通用方法,提取指定 Claim
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
// 解析 Token 获取所有 Claims
private Claims extractAllClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSignInKey())
.build()
.parseClaimsJws(token)
.getBody();
}
}
2. 模拟用户服务 (UserService.java)
这个类模拟从数据库加载用户信息。在实际项目中,你会在这里查询数据库。
package com.example.demo.service;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Collections;
@Service
public class UserService implements UserDetailsService {
// 模拟一个用户:用户名 admin, 密码 123456
// {noop} 表示密码是明文,生产环境必须用 BCrypt 加密
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if ("admin".equals(username)) {
return new User("admin", "{noop}123456", Collections.singletonList(new SimpleGrantedAuthority("ROLE_ADMIN")));
}
throw new UsernameNotFoundException("User not found");
}
}
3. JWT 认证过滤器 (JwtAuthFilter.java)
这是整个流程的“安检员”。它会拦截每一个请求,检查有没有带 JWT,并验证其有效性。
package com.example.demo.config;
import com.example.demo.service.JwtService;
import com.example.demo.service.UserService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserService userService;
public JwtAuthFilter(JwtService jwtService, UserService userService) {
this.jwtService = jwtService;
this.userService = userService;
}
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain) throws ServletException, IOException {
// 1. 从请求头中获取 Authorization 字段
final String authHeader = request.getHeader("Authorization");
final String jwt;
final String username;
// 2. 检查字段是否存在且以 "Bearer " 开头
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
// 3. 提取 Token 字符串
jwt = authHeader.substring(7);
try {
// 4. 从 Token 中解析出用户名
username = jwtService.extractUsername(jwt);
// 5. 如果用户名不为空,且当前用户还未被认证
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userService.loadUserByUsername(username);
// 6. 验证 Token 是否有效
if (jwtService.isTokenValid(jwt, userDetails)) {
// 7. 创建一个认证对象,告诉 Spring Security 这个人已经登录了
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
} catch (Exception e) {
// Token 无效,返回 401
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid Token");
return;
}
filterChain.doFilter(request, response);
}
}
4. 安全配置 (SecurityConfig.java)
这个类告诉 Spring Security 如何配置我们的应用。
package com.example.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final JwtAuthFilter jwtAuthFilter;
public SecurityConfig(JwtAuthFilter jwtAuthFilter) {
this.jwtAuthFilter = jwtAuthFilter;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf().disable() // 开发时暂时关闭,生产环境需开启并正确处理
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll() // 登录接口放行,谁都能访问
.anyRequest().authenticated() // 其他所有接口都需要认证
)
.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 设置为无状态,不使用 Session
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) // 将我们的 JWT 过滤器添加到认证过滤器之前
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
第三步:创建 API 接口
最后,我们创建两个控制器来暴露 API。
1. 认证控制器 (AuthController.java)
提供登录接口,验证用户名密码,成功后颁发 JWT。
package com.example.demo.controller;
import com.example.demo.dto.AuthResponse;
import com.example.demo.dto.LoginRequest;
import com.example.demo.service.JwtService;
import com.example.demo.service.UserService;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtService jwtService;
private final UserService userService;
public AuthController(AuthenticationManager authenticationManager, JwtService jwtService, UserService userService) {
this.authenticationManager = authenticationManager;
this.jwtService = jwtService;
this.userService = userService;
}
@PostMapping("/login")
public AuthResponse login(@RequestBody LoginRequest request) {
// 1. 使用 Spring Security 验证用户名和密码
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
);
// 2. 密码正确,生成 JWT
UserDetails user = userService.loadUserByUsername(request.getUsername());
String jwt = jwtService.generateToken(user);
return new AuthResponse(jwt);
}
}
2. 演示控制器 (DemoController.java)
提供一个需要认证才能访问的接口。
package com.example.demo.controller;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DemoController {
@GetMapping("/api/hello")
public String hello(Authentication authentication) {
// 获取当前登录的用户名
return "Hello, " + authentication.getName() + "! You have accessed a protected resource.";
}
}
3. 简单的 DTO 类
LoginRequest.java: 包含username和password字段。AuthResponse.java: 包含token字段。
🧪 第四步:API 测试
启动你的 Spring Boot 应用,然后打开 Postman 或 curl。
1. 登录,获取 Token
发送一个 POST 请求到登录接口。
- URL:
http://localhost:8080/api/auth/login - Method:
POST - Headers:
Content-Type: application/json - Body (raw JSON):
{ "username": "admin", "password": "123456" }
如果成功,你会收到一个包含 JWT 的 JSON 响应:
{
"token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImlhdCI6MTcwNzI4OTIwMCwiZXhwIjoxNzA3MjkwNjQwfQ..."
}
2. 访问受保护接口
现在,带着你的“门票”(JWT)去访问另一个接口。
- URL:
http://localhost:8080/api/hello - Method:
GET - Headers:
Authorization:Bearer eyJhbGciOiJIUzI1NiJ9...(注意 Bearer 后面有个空格)
如果 Token 有效,你将看到成功的消息:
Hello, admin! You have accessed a protected resource.
3. 尝试不带 Token 访问
如果你不带 Authorization 请求头,或者 Token 是错误的,服务器会直接返回 401 Unauthorized。
恭喜你!你已经亲手搭建了一套完整的 JWT 认证系统。
核心要点回顾:
- 无状态 (Stateless):服务器不保存任何会话信息,每次请求都是独立的。这使得应用更容易扩展。
- 安全性:JWT 的签名机制保证了信息无法被篡改。
- 流程:客户端登录 -> 服务器颁发 JWT -> 客户端携带 JWT 访问 -> 服务器验证 JWT。
面试加分项:
- JWT 的缺点是什么?
- 一旦签发,在过期前无法在服务端直接使其失效(除非引入黑名单机制,但这会牺牲一部分无状态的优势)。
- Payload 不能放敏感信息。
- 如何增强安全性?
- 使用 HTTPS 传输,防止 Token 在传输过程中被窃取。
- 设置较短的 Token 过期时间,并配合 Refresh Token 机制来实现自动续期。
- 在生产环境中,使用更安全的 RSA 非对称加密算法进行签名(授权服务器用私钥签名,资源服务器用公钥验证)。
希望这篇博客能帮助你彻底理解 JWT,并在未来的开发中游刃有余!
共同学习,写下你的评论
评论加载中...
作者其他优质文章