之前咱们把接口性能、健壮性拉满了,但如果有人直接访问删除图书,或者伪造请求篡改图书价格,系统直接被攻击 —— 这就像奶茶店没装门禁、没设监控,陌生人能随便进后厨改配方、偷原料,后果不堪设想。
/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)、接口级权限(如仅管理员可访问);全面防护:自带防 CSRF、会话固定攻击,配合插件可防 XSS、SQL 注入;密码安全:默认支持 BCrypt 加密,不用手动处理密码明文存储。
/book/add
二、实操 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,封装 JWT 令牌的生成、解析、验证逻辑:
JwtUtil.java
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)
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)
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. 准备测试数据(数据库插入用户)
先给表插入 BCrypt 加密后的用户(密码 123456):
sys_user
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 ,传 JSON 参数:
http://localhost:8080/login
json
{
"username": "admin",
"password": "123456"
}
返回 JWT 令牌:
json
{
"code": 200,
"msg": "操作成功",
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiration": "7200秒(2小时)"
}
}
3. 测试授权规则
管理员携带令牌访问(新增图书):请求头加
/book/add,正常访问;普通用户(user1)携带令牌访问
Authorization: Bearer 令牌:返回 403 无权限;未携带令牌访问
/book/add:返回 403 无权限;直接访问
/book/delete/1(查询图书):无需令牌,正常访问。
/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加密,Spring Security 自动对比加密后的密码。
BCryptPasswordEncoder
JWT 密钥写在配置文件:
坑:密钥明文存,被泄露后可伪造令牌;解决:生产环境存环境变量(如
application.yml),服务器配置环境变量。
@Value("${JWT_SECRET}")
角色名没加 ROLE_前缀,权限失效:
坑:数据库角色存,
admin识别不到;解决:数据库角色存
hasRole("ADMIN")(Spring Security 自动加
ADMIN前缀),或用
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 项目就能满足企业上线的安全要求,避免因未授权访问、恶意攻击导致的数据泄露或系统故障。