Spring Boot 安全防护实战:JWT+Spring Security,给接口装 “门禁 + 监控”(企业级加固)

内容分享7天前发布
0 0 0

之前咱们把接口性能、健壮性拉满了,但如果有人直接访问
/book/delete/1
删除图书,或者伪造请求篡改图书价格,系统直接被攻击 —— 这就像奶茶店没装门禁、没设监控,陌生人能随便进后厨改配方、偷原料,后果不堪设想。

Spring Boot 生态的
spring-boot-starter-security
就是接口的 “安全防护套装”,核心是认证(谁能进)+ 授权(能做什么)+ 防攻击(防捣乱) ,像奶茶店的 “门禁 + 员工分工 + 监控系统”:只有登录成功才能操作(认证),管理员能改价格、普通员工只能做奶茶(授权),防止外人捣乱(防攻击)。

今天全程聚焦 Spring Boot 生态,以图书管理系统为案例,手把手实现 “JWT 登录认证 + 角色权限控制 + 常见攻击防护”,让接口从 “裸奔” 变成 “铜墙铁壁”,新手也能直接复制代码落地,符合企业上线安全标准!

一、先搞懂:Spring Boot 安全防护的核心价值(奶茶店类比)

1. 解决的 3 个核心安全问题(企业刚需)

认证(Authentication):确认 “你是谁”—— 就像奶茶店员工刷工牌进门,只有登录成功(工牌有效)才能操作接口;授权(Authorization):确认 “你能做什么”—— 就像店长能改原料价格、普通员工只能制作奶茶,不同角色有不同权限;防攻击(Protection):抵御恶意请求 —— 就像奶茶店装监控,防止外人偷原料、恶意捣乱(如 CSRF 攻击、SQL 注入)。

2. 为什么用 Spring Security+JWT?(生态最优解)

Spring Security:Spring Boot 官方安全框架,无缝整合,不用额外适配,像奶茶店的 “原装门禁系统”;JWT(JSON Web Token):无状态令牌,登录成功后返回一串加密字符串,后续请求携带令牌即可,不用存 Session(适合分布式部署),像奶茶店的 “临时通行证”。

核心优势

无状态:JWT 令牌包含用户信息,服务器不用存 Session,多服务器部署时不用同步会话;细粒度权限:支持角色权限(如 ADMIN/USER)、接口级权限(如
/book/add
仅管理员可访问);全面防护:自带防 CSRF、会话固定攻击,配合插件可防 XSS、SQL 注入;密码安全:默认支持 BCrypt 加密,不用手动处理密码明文存储。

二、实操 1:Spring Boot 整合 Spring Security+JWT(核心认证流程)

咱们以图书管理系统的 “登录认证 + 角色权限” 为核心,一步步实现安全防护,步骤清晰,代码可直接复制。

步骤 1:加依赖(Spring Boot 安全核心依赖)

xml



<!-- Spring Security核心依赖(生态内官方支持) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>2.7.10</version>
</dependency>
 
<!-- JWT依赖(处理令牌生成/解析) -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
 
<!-- 工具类依赖(简化JWT操作) -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.20</version>
</dependency>

步骤 2:配置 JWT 工具类(生成 / 解析令牌)

新建
utils
包,创建
JwtUtil.java
,封装 JWT 令牌的生成、解析、验证逻辑:

java

运行



package com.example.springbootdemo.utils;
 
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
 
import java.util.Date;
 
/**
 * JWT工具类:生成令牌、解析令牌、验证令牌
 */
@Component
public class JwtUtil {
 
    // JWT密钥(生产环境存环境变量,别写配置文件)
    @Value("${jwt.secret:myBookSecret123456}")
    private String secret;
 
    // 令牌过期时间:2小时(单位:毫秒)
    @Value("${jwt.expiration:7200000}")
    private long expiration;
 
    /**
     * 生成JWT令牌(登录成功后调用)
     */
    public String generateToken(UserDetails userDetails) {
        return Jwts.builder()
                // 存入用户名(Claims:令牌携带的额外信息)
                .setSubject(userDetails.getUsername())
                // 签发时间
                .setIssuedAt(new Date())
                // 过期时间
                .setExpiration(new Date(System.currentTimeMillis() + expiration))
                // 签名算法+密钥(HS256:对称加密,简单高效)
                .signWith(SignatureAlgorithm.HS256, secret)
                .compact();
    }
 
    /**
     * 从令牌中解析用户名
     */
    public String getUsernameFromToken(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
        return claims.getSubject();
    }
 
    /**
     * 验证令牌是否有效(用户名匹配+未过期)
     */
    public boolean validateToken(String token, UserDetails userDetails) {
        String username = getUsernameFromToken(token);
        // 验证用户名一致且令牌未过期
        return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }
 
    /**
     * 检查令牌是否过期
     */
    private boolean isTokenExpired(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
        return claims.getExpiration().before(new Date());
    }
}

步骤 3:配置 Spring Security 核心(认证 + 授权规则)

1. 实现 UserDetailsService(加载用户信息)

Spring Security 需要从数据库加载用户信息(用户名、密码、角色),新建
service
包下的
UserDetailsServiceImpl.java

java

运行



package com.example.springbootdemo.service;
 
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.springbootdemo.entity.SysUser;
import com.example.springbootdemo.mapper.SysUserMapper;
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 javax.annotation.Resource;
 
/**
 * Spring Security用户信息加载服务(从数据库查用户)
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
 
    @Resource
    private SysUserMapper sysUserMapper;
 
    /**
     * 根据用户名加载用户信息(Spring Security自动调用)
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 从数据库查询用户(SysUser:用户表实体,含username、password、role字段)
        SysUser user = sysUserMapper.selectOne(
                new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, username)
        );
        if (user == null) {
            throw new UsernameNotFoundException("用户名不存在");
        }
 
        // 封装成Spring Security需要的UserDetails对象(用户名、密码、角色权限)
        return User.withUsername(user.getUsername())
                .password(user.getPassword()) // 数据库密码必须是BCrypt加密后的字符串
                .roles(user.getRole()) // 角色:如"ADMIN"(Spring Security自动加ROLE_前缀)
                .build();
    }
}
2. 配置 Spring Security 安全规则(
SecurityConfig.java

新建
config
包,创建核心配置类,定义 “哪些接口需要认证、哪些角色能访问”:

java

运行



package com.example.springbootdemo.config;
 
import com.example.springbootdemo.service.UserDetailsServiceImpl;
import com.example.springbootdemo.utils.JwtUtil;
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.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
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.authentication.UsernamePasswordAuthenticationFilter;
 
import javax.annotation.Resource;
 
/**
 * Spring Security核心配置:认证规则、授权规则、JWT过滤
 */
@Configuration
@EnableWebSecurity // 启用Web安全
@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用方法级权限(@PreAuthorize)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
 
    @Resource
    private UserDetailsServiceImpl userDetailsService;
 
    @Resource
    private JwtUtil jwtUtil;
 
    /**
     * 密码加密器(BCrypt算法,Spring Security推荐)
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
 
    /**
     * 认证管理器(登录时验证用户名密码)
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
 
    /**
     * 配置认证逻辑(用自定义的UserDetailsService加载用户,用BCrypt加密)
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(passwordEncoder());
    }
 
    /**
     * 配置授权规则(哪些接口放行、哪些需要认证、哪些需要特定角色)
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // 1. 关闭Session(JWT是无状态,不用Session)
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            // 2. 配置接口访问规则
            .authorizeRequests()
            .antMatchers("/login").permitAll() // 登录接口放行(不用认证)
            .antMatchers("/doc.html/**", "/webjars/**").permitAll() // 接口文档放行
            .antMatchers("/book/list", "/book/{id}").permitAll() // 图书查询接口放行(所有人可看)
            .antMatchers("/book/add", "/book/delete/**", "/book/update").hasRole("ADMIN") // 新增/删除/更新需ADMIN角色
            .anyRequest().authenticated() // 其他所有接口都需要认证
            .and()
            // 3. 关闭CSRF防护(JWT无状态,CSRF防护意义不大,生产环境可根据需求开启)
            .csrf().disable()
            // 4. 禁用默认登录页(用自定义登录接口)
            .formLogin().disable()
            .logout().disable();
 
        // 5. 添加JWT过滤器(在用户名密码认证过滤器之前执行)
        http.addFilterBefore(new JwtAuthenticationFilter(jwtUtil, userDetailsService), UsernamePasswordAuthenticationFilter.class);
    }
}
3. 编写 JWT 过滤器(
JwtAuthenticationFilter.java

过滤器的作用:拦截所有请求,从请求头提取 JWT 令牌,验证通过后自动登录(给 Spring Security 设置认证信息):

java

运行



package com.example.springbootdemo.config;
 
import com.example.springbootdemo.service.UserDetailsServiceImpl;
import com.example.springbootdemo.utils.JwtUtil;
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.web.filter.OncePerRequestFilter;
 
import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
 
/**
 * JWT过滤器:拦截请求,验证令牌,自动登录
 */
public class JwtAuthenticationFilter extends OncePerRequestFilter {
 
    @Resource
    private JwtUtil jwtUtil;
 
    @Resource
    private UserDetailsServiceImpl userDetailsService;
 
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        // 1. 从请求头提取JWT令牌(请求头key:Authorization,值:Bearer 令牌)
        String authorizationHeader = request.getHeader("Authorization");
        String token = null;
        String username = null;
        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            token = authorizationHeader.substring(7); // 截取"Bearer "后面的令牌
            username = jwtUtil.getUsernameFromToken(token); // 从令牌解析用户名
        }
 
        // 2. 令牌有效且未登录,自动设置认证信息
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            // 验证令牌有效性
            if (jwtUtil.validateToken(token, userDetails)) {
                // 设置认证信息(Spring Security会认为当前用户已登录)
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities()
                );
                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }
        }
 
        // 3. 继续执行后续过滤器(如用户名密码认证、接口权限校验)
        filterChain.doFilter(request, response);
    }
}

步骤 4:编写登录接口(自定义登录逻辑)

Spring Security 默认登录页不友好,自定义登录接口,返回 JWT 令牌:

java

运行



package com.example.springbootdemo.controller;
 
import com.example.springbootdemo.common.Result;
import com.example.springbootdemo.utils.JwtUtil;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
 
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
 
/**
 * 登录接口(自定义,替代Spring Security默认登录页)
 */
@RestController
public class LoginController {
 
    @Resource
    private AuthenticationManager authenticationManager;
 
    @Resource
    private UserDetailsService userDetailsService;
 
    @Resource
    private JwtUtil jwtUtil;
 
    // 登录请求体(用户名+密码)
    static class LoginRequest {
        private String username;
        private String password;
        // getter/setter省略
    }
 
    @PostMapping("/login")
    public Result<Map<String, String>> login(@RequestBody LoginRequest loginRequest) {
        // 1. 验证用户名密码(Spring Security自动调用UserDetailsService加载用户,对比密码)
        authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())
        );
 
        // 2. 验证通过,生成JWT令牌
        UserDetails userDetails = userDetailsService.loadUserByUsername(loginRequest.getUsername());
        String token = jwtUtil.generateToken(userDetails);
 
        // 3. 返回令牌(前端存储,后续请求携带在请求头)
        Map<String, String> result = new HashMap<>();
        result.put("token", token);
        result.put("expiration", "7200秒(2小时)");
        return Result.success(result);
    }
}

步骤 5:测试认证 + 授权效果

1. 准备测试数据(数据库插入用户)

先给
sys_user
表插入 BCrypt 加密后的用户(密码 123456):

sql



-- 管理员用户(角色ADMIN):username=admin,password=BCrypt加密后的123456
INSERT INTO sys_user (username, password, role) 
VALUES ('admin', '$2a$10$E5kX7H8Z8y7G6F5D4C3B2A1S0D9F8G7H6J5K4L3M2N1O0P', 'ADMIN');
 
-- 普通用户(角色USER):username=user1,password=BCrypt加密后的123456
INSERT INTO sys_user (username, password, role) 
VALUES ('user1', '$2a$10$E5kX7H8Z8y7G6F5D4C3B2A1S0D9F8G7H6J5K4L3M2N1O0P', 'USER');

(BCrypt 加密可通过
new BCryptPasswordEncoder().encode("123456")
生成)

2. 测试登录接口

POST 
http://localhost:8080/login
,传 JSON 参数:

json



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

返回 JWT 令牌:

json



{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "expiration": "7200秒(2小时)"
  }
}
3. 测试授权规则

管理员携带令牌访问
/book/add
(新增图书):请求头加
Authorization: Bearer 令牌
,正常访问;普通用户(user1)携带令牌访问
/book/add
:返回 403 无权限;未携带令牌访问
/book/delete/1
:返回 403 无权限;直接访问
/book/list
(查询图书):无需令牌,正常访问。

三、实操 2:接口防攻击加固(企业级安全补充)

除了认证授权,还要防护常见攻击(XSS、SQL 注入、接口限流),Spring Boot 生态有现成工具,配置简单。

1. 防 XSS 攻击(跨站脚本攻击)

XSS 攻击:用户输入含恶意脚本(如
<script>alert('攻击')</script>
),存入数据库后,前端渲染时执行脚本。

解决方案:添加 XSS 过滤器,过滤恶意标签

java

运行



package com.example.springbootdemo.config;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.HiddenHttpMethodFilter;
 
import javax.servlet.Filter;
import javax.servlet.http.HttpServletRequest;
import java.util.regex.Pattern;
 
/**
 * XSS过滤器:过滤请求参数中的恶意脚本标签
 */
@Configuration
public class XssFilterConfig {
 
    @Bean
    public Filter xssFilter() {
        return (request, response, chain) -> {
            chain.doFilter(new XssHttpServletRequestWrapper((HttpServletRequest) request), response);
        };
    }
 
    // 包装请求,过滤参数
    static class XssHttpServletRequestWrapper extends javax.servlet.http.HttpServletRequestWrapper {
        // XSS攻击正则表达式(过滤<script>、<iframe>等标签)
        private static final Pattern XSS_PATTERN = Pattern.compile(
                "<script.*?>.*?</script>|<iframe.*?>.*?</iframe>|<img.*?src=.*? onerror=.*?>",
                Pattern.CASE_INSENSITIVE
        );
 
        public XssHttpServletRequestWrapper(HttpServletRequest request) {
            super(request);
        }
 
        // 重写getParameter方法,过滤单个参数
        @Override
        public String getParameter(String name) {
            String value = super.getParameter(name);
            return value != null ? filterXss(value) : null;
        }
 
        // 过滤恶意标签
        private String filterXss(String value) {
            return XSS_PATTERN.matcher(value).replaceAll("");
        }
    }
}

2. 接口限流(防止恶意刷接口)

用 Spring Cloud Gateway+Redis 实现限流(如果是单体项目,用 Guava RateLimiter):

java

运行



package com.example.springbootdemo.config;
 
import com.google.common.util.concurrent.RateLimiter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
 
/**
 * 接口限流配置:每个IP每秒最多10次请求
 */
@Configuration
public class RateLimitConfig implements WebMvcConfigurer {
 
    // 限流对象:每秒生成10个令牌(每个请求消耗1个令牌)
    @Bean
    public RateLimiter rateLimiter() {
        return RateLimiter.create(10.0);
    }
 
    // 限流拦截器
    @Bean
    public HandlerInterceptor rateLimitInterceptor(RateLimiter rateLimiter) {
        return new HandlerInterceptor() {
            @Override
            public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
                // 获取客户端IP
                String ip = request.getRemoteAddr();
                // 尝试获取令牌,超时时间0秒(获取不到直接拒绝)
                if (!rateLimiter.tryAcquire(0)) {
                    response.setContentType("application/json;charset=utf-8");
                    response.getWriter().write("{"code":429,"msg":"请求过于频繁,请稍后重试","data":null}");
                    return false;
                }
                return true;
            }
        };
    }
 
    // 注册拦截器(对所有接口限流)
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(rateLimitInterceptor(rateLimiter()))
                .addPathPatterns("/**")
                .excludePathPatterns("/login"); // 登录接口不限流
    }
}

3. 密码安全加固(补充)

密码必须 BCrypt 加密(Spring Security 默认支持,不用手动处理);密码复杂度校验(长度≥8 位,含大小写字母 + 数字 + 特殊字符),可通过自定义校验注解实现;登录失败次数限制(如 5 次失败锁定 10 分钟),可通过 Redis 存储失败次数实现。

四、避坑总结:Spring Boot 安全防护的 6 个新手坑

密码没加密,直接存明文

坑:数据库
sys_user
表密码存明文,被泄露后直接登录;解决:用
BCryptPasswordEncoder
加密,Spring Security 自动对比加密后的密码。

JWT 密钥写在配置文件

坑:密钥明文存
application.yml
,被泄露后可伪造令牌;解决:生产环境存环境变量(如
@Value("${JWT_SECRET}")
),服务器配置环境变量。

角色名没加 ROLE_前缀,权限失效

坑:数据库角色存
admin

hasRole("ADMIN")
识别不到;解决:数据库角色存
ADMIN
(Spring Security 自动加
ROLE_
前缀),或用
hasAuthority("ROLE_ADMIN")

过滤器没加,JWT 令牌验证失效

坑:携带令牌访问接口仍返回 403,因为没加
JwtAuthenticationFilter
;解决:在
SecurityConfig
中添加过滤器,且要在
UsernamePasswordAuthenticationFilter
之前。

禁用 CSRF 后没防护其他攻击

坑:禁用 CSRF 后,没做 XSS、限流防护,导致接口被恶意攻击;解决:根据场景选择是否禁用 CSRF,同时开启 XSS 过滤、接口限流。

放行接口配置错误,导致无需认证

坑:
antMatchers("/book/**").permitAll()
,导致所有图书接口都无需认证;解决:精准配置放行接口(如
/book/list
),其他接口按权限控制。

总结:Spring Boot 安全防护的核心是 “认证 + 授权 + 防护”

Spring Security+JWT 的组合,让 Spring Boot 接口实现 “三重保护”:

认证:只有登录成功(持有有效 JWT 令牌)才能访问接口;授权:不同角色只能做对应操作(管理员能增删改,普通用户只能查);防护:过滤恶意脚本、限制请求频率,抵御常见攻击。

就像奶茶店的安全体系:门禁(认证)确保只有员工能进,分工(授权)确保员工不越权,监控(防护)确保没人捣乱,三者结合才能让奶茶店安全运营。

掌握今天的安全防护技巧,你的 Spring Boot 项目就能满足企业上线的安全要求,避免因未授权访问、恶意攻击导致的数据泄露或系统故障。

© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...