为了账号安全,请及时绑定邮箱和手机立即绑定

🛡️ 零基础搞懂Java安全认证:Spring Boot + JWT 实战全解析

在前后端分离和微服务架构大行其道的今天,传统的 Session 认证方式显得有些力不从心。它依赖服务器内存,难以应对分布式部署,还存在跨域等麻烦。

那么,现代应用是如何安全地识别“你是谁”的呢?答案就是 JWT (JSON Web Token)

别被这些名词吓到,今天我们就用最通俗的语言和最直接的代码,带你从零开始,在 Spring Boot 项目中实现一套完整的 JWT 安全认证系统。学完这篇博客,你将彻底理解它的核心原理,并能亲手写出代码。


🤔 JWT 是什么?一个生动的比喻

想象一下你去游乐园玩:

  1. 传统 Session 模式:你买票入园时,工作人员给你一张实体手环。游乐园的每个项目(服务器)旁边都有一个查询台(数据库),工作人员需要扫描你的手环,去查询台核实你的票是否有效,才能让你玩。这很麻烦,而且查询台一旦拥堵,所有人都得等着。

  2. JWT 模式:你买票入园时,工作人员给你一张带有特殊防伪标记的门票。这张门票本身就包含了你的信息(比如VIP等级),并且盖了游乐园的官方印章。当你去玩项目时,工作人员只需要检查一下门票上的印章是不是真的,再看看上面的信息,就可以直接让你玩,完全不需要去查询台核实。

JWT 就是那张“自带信息、自带防伪”的电子门票。


JWT 看起来是一长串字符,比如:xxxxx.yyyyy.zzzzz。它由三部分组成,用点 . 连接:

  1. Header (头部)

    • 作用:描述“门票”的类型和防伪技术。
    • 内容:通常包含令牌类型(就是 JWT)和签名算法(比如 HMAC SHA256)。
    • 注意:这部分只是编码,不是加密,谁都能看懂。
  2. Payload (载荷)

    • 作用:存放“门票”上的具体信息。
    • 内容:包含两类信息:
      • 标准声明:比如签发者 (iss)、过期时间 (exp)、主题 (sub,通常是用户名或用户ID)。
      • 自定义声明:你可以放任何想放的非敏感信息,比如用户的角色 (role: "admin")。
    • 严重警告:Payload 也只是 Base64 编码,任何人都可以解码查看。所以,千万不要把用户密码等敏感信息放进去!
  3. 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: 包含 usernamepassword 字段。
  • 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,并在未来的开发中游刃有余!

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
JAVA开发工程师
手记
粉丝
27
获赞与收藏
81

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号

举报

0/150
提交
取消