【Java全栈教程】第 43 课:Spring Boot安全

2 阅读5分钟

第 43 课:Spring Boot安全

本课目标:掌握Spring Security基础配置,理解JWT认证流程,实现基于角色的访问控制。


一、概念讲解

1.1 Spring Security核心架构

请求过滤链:
┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐
│  客户端   │───>│ Filter1  │───>│ Filter2  │───>│ FilterN  │───> Controller
│          │<───│ (认证)    │<───│ (授权)    │<───│ (安全)    │<───
└──────────┘    └──────────┘    └──────────┘    └──────────┘

1.2 认证 vs 授权

认证(Authentication):你是谁? → 登录验证身份
授权(Authorization):你能干什么? → 权限控制

┌─────────────────────────────────────────────────┐
│  用户登录                                         │
│  ┌─────────┐    ┌────────────┐    ┌──────────┐  │
│  │ 用户名   │───>│ Security   │───>│ JWT Token │  │
│  │ 密码     │    │ 过滤器链   │    │ (返回)    │  │
│  └─────────┘    └────────────┘    └──────────┘  │
│                                                   │
│  访问资源                                          │
│  ┌─────────┐    ┌────────────┐    ┌──────────┐  │
│  │ 请求+    │───>│ JWT过滤器   │───>│ Controller│  │
│  │ Token    │    │ 验证Token  │    │ (返回)    │  │
│  └─────────┘    └────────────┘    └──────────┘  │
└─────────────────────────────────────────────────┘

1.3 JWT结构

JWT由三部分组成:
Header.Payload.Signature

eyJhbGciOiJIUzI1NiJ9    ← Header (算法类型)
.
eyJzdWIiOiIxMjM0NTY3ODkwIn0    ← Payload (载荷/用户信息)
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c    ← Signature (签名)

二、语法格式

2.1 Security配置类

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable())           // 禁用CSRF(REST API)
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()  // 公开接口
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()                 // 其他需认证
            )
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
            .build();
    }
}

2.2 JWT相关方法

// 生成Token
String token = jwtUtils.generateToken(userId, username, role);

// 解析Token
Claims claims = jwtUtils.parseToken(token);

// 验证Token
boolean valid = jwtUtils.validateToken(token);

三、代码案例

3.1 添加依赖

<!-- Spring Security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- JWT -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.12.3</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.12.3</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.12.3</version>
    <scope>runtime</scope>
</dependency>

3.2 配置文件

# application.yml
jwt:
  secret: mySecretKeyForJwtTokenGenerationMustBeLongEnough2024!
  expiration: 86400000   # 24小时(毫秒)

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/demo_db?useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: root

3.3 JWT工具类

package com.example.demo.security;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Component
public class JwtUtils {

    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.expiration}")
    private long expiration;

    private SecretKey getSigningKey() {
        return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
    }

    // 生成Token
    public String generateToken(Long userId, String username, String role) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("userId", userId);
        claims.put("username", username);
        claims.put("role", role);

        return Jwts.builder()
            .claims(claims)
            .subject(username)
            .issuedAt(new Date())
            .expiration(new Date(System.currentTimeMillis() + expiration))
            .signWith(getSigningKey())
            .compact();
    }

    // 解析Token
    public Claims parseToken(String token) {
        return Jwts.parser()
            .verifyWith(getSigningKey())
            .build()
            .parseSignedClaims(token)
            .getPayload();
    }

    // 验证Token是否有效
    public boolean validateToken(String token) {
        try {
            Claims claims = parseToken(token);
            return !claims.getExpiration().before(new Date());
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }

    // 从Token中获取用户ID
    public Long getUserId(String token) {
        Claims claims = parseToken(token);
        return ((Number) claims.get("userId")).longValue();
    }

    // 从Token中获取用户名
    public String getUsername(String token) {
        return parseToken(token).getSubject();
    }

    // 从Token中获取角色
    public String getRole(String token) {
        Claims claims = parseToken(token);
        return (String) claims.get("role");
    }
}

3.4 JWT认证过滤器

package com.example.demo.security;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Collections;

@Component
public class JwtFilter extends OncePerRequestFilter {

    private final JwtUtils jwtUtils;

    public JwtFilter(JwtUtils jwtUtils) {
        this.jwtUtils = jwtUtils;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {

        String token = getTokenFromRequest(request);

        if (StringUtils.hasText(token) && jwtUtils.validateToken(token)) {
            String username = jwtUtils.getUsername(token);
            String role = jwtUtils.getRole(token);

            SimpleGrantedAuthority authority =
                new SimpleGrantedAuthority("ROLE_" + role);

            UsernamePasswordAuthenticationToken authentication =
                new UsernamePasswordAuthenticationToken(
                    username, null, Collections.singletonList(authority));

            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }

    private String getTokenFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

3.5 Security配置类

package com.example.demo.config;

import com.example.demo.security.JwtFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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
@EnableMethodSecurity
public class SecurityConfig {

    private final JwtFilter jwtFilter;

    public SecurityConfig(JwtFilter jwtFilter) {
        this.jwtFilter = jwtFilter;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
            .build();
    }
}

3.6 用户实体(数据库)

package com.example.demo.entity;

import jakarta.persistence.*;

@Entity
@Table(name = "sys_user")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

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

    @Column(nullable = false, length = 100)
    private String email;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false, length = 20)
    private String role;    // ROLE_USER, ROLE_ADMIN

    @Column(columnDefinition = "INT DEFAULT 1")
    private int status;

    // 构造方法
    public User() {}

    public User(String username, String email, String password, String role) {
        this.username = username;
        this.email = email;
        this.password = password;
        this.role = role;
        this.status = 1;
    }

    // getter/setter
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }
    public String getRole() { return role; }
    public void setRole(String role) { this.role = role; }
    public int getStatus() { return status; }
    public void setStatus(int status) { this.status = status; }
}

3.7 认证Controller(登录/注册)

package com.example.demo.controller;

import com.example.demo.common.Result;
import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;
import com.example.demo.security.JwtUtils;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final JwtUtils jwtUtils;

    public AuthController(UserRepository userRepository,
                          PasswordEncoder passwordEncoder,
                          JwtUtils jwtUtils) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
        this.jwtUtils = jwtUtils;
    }

    // 注册
    @PostMapping("/register")
    public Result<User> register(@RequestBody User user) {
        if (userRepository.existsByUsername(user.getUsername())) {
            return Result.error(400, "用户名已存在");
        }
        if (userRepository.existsByEmail(user.getEmail())) {
            return Result.error(400, "邮箱已被注册");
        }

        user.setPassword(passwordEncoder.encode(user.getPassword()));
        user.setRole("USER");
        user.setStatus(1);
        User saved = userRepository.save(user);
        saved.setPassword(null); // 不返回密码
        return Result.success("注册成功", saved);
    }

    // 登录
    @PostMapping("/login")
    public Result<Map<String, Object>> login(@RequestBody User loginUser) {
        User user = userRepository.findByUsername(loginUser.getUsername())
            .orElse(null);

        if (user == null || !passwordEncoder.matches(
                loginUser.getPassword(), user.getPassword())) {
            return Result.error(401, "用户名或密码错误");
        }

        if (user.getStatus() == 0) {
            return Result.error(403, "账号已被禁用");
        }

        String token = jwtUtils.generateToken(
            user.getId(), user.getUsername(), user.getRole());

        Map<String, Object> result = new HashMap<>();
        result.put("token", token);
        result.put("username", user.getUsername());
        result.put("role", user.getRole());

        return Result.success("登录成功", result);
    }

    // 获取当前用户信息
    @GetMapping("/info")
    public Result<Map<String, Object>> getUserInfo(
            @RequestHeader("Authorization") String authHeader) {
        String token = authHeader.substring(7);
        String username = jwtUtils.getUsername(token);
        String role = jwtUtils.getRole(token);

        Map<String, Object> info = new HashMap<>();
        info.put("username", username);
        info.put("role", role);
        return Result.success(info);
    }
}

3.8 受保护的业务接口

package com.example.demo.controller;

import com.example.demo.common.Result;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.Map;

@RestController
@RequestMapping("/api/admin")
public class AdminController {

    @GetMapping("/dashboard")
    @PreAuthorize("hasRole('ADMIN')")
    public Result<Map<String, Object>> dashboard() {
        Map<String, Object> data = Map.of(
            "totalUsers", 100,
            "activeToday", 25,
            "revenue", 50000
        );
        return Result.success(data);
    }

    @GetMapping("/users")
    @PreAuthorize("hasRole('ADMIN')")
    public Result<String> manageUsers() {
        return Result.success("管理员可以查看所有用户");
    }

    @GetMapping("/settings")
    @PreAuthorize("hasRole('ADMIN')")
    public Result<String> settings() {
        return Result.success("管理员可以管理系统设置");
    }
}

3.9 需认证的普通接口

package com.example.demo.controller;

import com.example.demo.common.Result;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import java.util.Map;

@RestController
@RequestMapping("/api/user")
public class UserCenterController {

    @GetMapping("/profile")
    public Result<Map<String, Object>> profile() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        String username = auth.getName();

        Map<String, Object> profile = Map.of(
            "username", username,
            "message", "这是你的个人信息页面"
        );
        return Result.success(profile);
    }

    @GetMapping("/settings")
    public Result<String> settings() {
        return Result.success("用户可以修改自己的设置");
    }
}

四、常见错误

4.1 403 Forbidden

原因:未携带Token或Token无效。

解决:请求头添加Authorization: Bearer <token>

4.2 Bad credentials

原因:密码不匹配,或未使用BCrypt编码。

解决:注册时必须用passwordEncoder.encode()加密密码。

4.3 Method not allowed

原因:Spring Security默认禁止CSRF,但表单提交需要。

解决:REST API禁用CSRF,表单场景启用CSRF并携带Token。

4.4 Token expired

原因:JWT过期。

解决:实现Token刷新机制,或在前端检测过期后重新登录。


五、课后练习

  1. 实现密码修改接口,需要验证旧密码后才能设置新密码
  2. 添加角色ADMIN,测试@PreAuthorize的权限控制
  3. 实现简单的Token刷新接口:当Token剩余有效期小于1小时时,返回新Token
  4. 拦截未认证请求,返回统一的JSON错误信息(而非跳转登录页)

六、本课小结

知识点说明
Spring SecuritySpring生态中的安全框架,提供认证和授权能力
JWT无状态Token机制,适合REST API认证
BCrypt密码加密算法,Spring Security默认推荐
@PreAuthorize方法级别权限控制,支持SpEL表达式
Stateless无状态会话,每次请求携带Token,不依赖Session
JwtFilter自定义过滤器,拦截请求验证Token并设置认证信息

本课程持续更新中,欢迎关注!