每天5分钟,掌握一个SpringBoot核心知识点。大家好,我是SpringBoot指南的小坏。经过一周的学习,我们从配置、日志、监控到性能优化都讲完了,今天来聊聊最重要也最容易被忽略的话题——安全。
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java 回复"Swagger源码",获取本文所有示例代码、配置模板及导出工具。
一、真实故事:我差点丢了工作
去年公司上线了一个新系统,我负责开发。上线后一切正常,直到有一天...
凌晨2点,老板电话来了:"小坏,数据库被删了!" 我:"不可能啊,我们密码很复杂的..." 查日志发现:有人用SQL注入,直接删除了整个用户表。
更可怕的是:
- 用户密码全部明文存储
- 越权访问,普通用户能看到管理员数据
- XSS攻击,网站被挂上了赌博广告
最终结果:
- 用户数据全部丢失
- 公司被罚款50万
- 我差点被开除
今天,我要用血的教训告诉你:安全没做好,一切都白搞!
二、安全防护的5个等级
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java 回复"Swagger源码",获取本文所有示例代码、配置模板及导出工具。 先看你的应用在哪个等级:
等级1:裸奔(危险)
// 什么都没做
@GetMapping("/user/{id}")
public User getUser(@PathVariable String id) {
return userRepository.findById(id); // SQL注入危险!
}
等级2:有门(及格)
// 加了基础防护
@GetMapping("/user/{id}")
public User getUser(@PathVariable Long id) { // 用Long类型
return userRepository.findById(id).orElse(null);
}
等级3:有锁(良好)
// 加了权限控制
@PreAuthorize("hasRole('USER')") // 需要用户角色
@GetMapping("/user/{id}")
public User getUser(@PathVariable Long id) {
return userService.getUserById(id);
}
等级4:有监控(优秀)
// 加了安全审计
@GetMapping("/user/{id}")
@AuditLog(action = "查询用户", userId = "#userId")
public User getUser(@PathVariable Long id,
@CurrentUser User currentUser) {
// 记录谁在什么时候查了谁
auditService.logQuery(currentUser.getId(), id);
return userService.getUserById(id);
}
等级5:保险柜(专业)
// 全链路安全防护
@RateLimit(limit = 10) // 限流
@PreAuthorize("@securityService.canViewUser(#id, #currentUser)") // 权限
@AuditLog(action = "查询用户") // 审计
@GetMapping("/user/{id}")
@ResponseBody
public UserDTO getUser(@PathVariable @ValidUserId Long id, // 参数验证
@CurrentUser User currentUser) {
// 返回脱敏后的DTO
return userService.getSafeUserInfo(id);
}
你是哪个等级? 如果还在1或2,请继续往下看!
三、基础防护:Spring Security快速入门
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java 回复"Swagger源码",获取本文所有示例代码、配置模板及导出工具。
3.1 5分钟搭建登录系统
第一步:加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
第二步:什么都不用配!
是的,你没看错!启动应用,访问任意页面,会自动跳转到登录页:
- 用户名:
user - 密码:控制台里找(类似:
Using generated security password: xxxx)
第三步:自定义配置
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
// 首页所有人都能访问
.requestMatchers("/", "/home").permitAll()
// 用户页面需要登录
.requestMatchers("/user/**").authenticated()
// 管理员页面需要角色
.requestMatchers("/admin/**").hasRole("ADMIN")
// 其他页面需要登录
.anyRequest().authenticated()
)
.formLogin(form -> form
// 自定义登录页
.loginPage("/login")
.permitAll()
)
.logout(logout -> logout
// 退出登录
.permitAll()
);
return http.build();
}
// 配置内存用户(生产环境用数据库)
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.withUsername("user")
.password("{noop}123456") // {noop}表示不加密
.roles("USER")
.build();
UserDetails admin = User.withUsername("admin")
.password("{noop}admin123")
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
}
3.2 密码加密:别再存明文了!
常见错误:
// ❌ 错误:明文存储
user.setPassword("123456");
userRepository.save(user);
// ❌ 还是错误:MD5太弱了
user.setPassword(md5("123456"));
正确做法:
@Configuration
public class PasswordConfig {
@Bean
public PasswordEncoder passwordEncoder() {
// 使用BCrypt,自动加盐
return new BCryptPasswordEncoder();
}
}
@Service
public class UserService {
@Autowired
private PasswordEncoder passwordEncoder;
public void register(User user) {
// ✅ 正确:BCrypt加密
String encodedPassword = passwordEncoder.encode(user.getPassword());
user.setPassword(encodedPassword);
userRepository.save(user);
}
public boolean login(String username, String password) {
User user = userRepository.findByUsername(username);
if (user == null) {
return false;
}
// 验证密码
return passwordEncoder.matches(password, user.getPassword());
}
}
BCrypt的优点:
- 自动加盐,相同密码加密结果不同
- 计算慢,防暴力破解
- 行业标准,Spring Security默认
四、常见攻击与防护
4.1 SQL注入:数据库的"万能钥匙"
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java 回复"Swagger源码",获取本文所有示例代码、配置模板及导出工具。 攻击方式:
-- 正常查询
SELECT * FROM users WHERE username = 'admin' AND password = '123456'
-- SQL注入攻击
username输入:admin' --
password输入:任意
-- 变成
SELECT * FROM users WHERE username = 'admin' --' AND password = 'xxx'
-- -- 是SQL注释,后面条件被忽略,直接登录admin账号!
防护方法:
// ❌ 危险:字符串拼接
@Query("SELECT * FROM users WHERE username = '" + username + "'")
User findByUsername(String username);
// ✅ 安全:使用参数绑定
@Query("SELECT u FROM User u WHERE u.username = :username")
User findByUsername(@Param("username") String username);
// ✅ 更安全:用JPA方法
User findByUsername(String username); // JPA自动防注入
4.2 XSS攻击:网页里的"木马"
攻击方式:
<!-- 用户输入这个 -->
<script>alert('你的cookie是:' + document.cookie)</script>
<!-- 显示在页面上时,脚本被执行 -->
<div>用户评论:<script>alert('你的cookie是:' + document.cookie)</script></div>
防护方法:
// 方法1:Spring Boot自动防护(默认开启)
// 在application.yml中
spring:
web:
resources:
add-mappings: false # 禁止直接访问静态资源
security:
enable-csrf: true # 开启CSRF防护
// 方法2:手动转义
import org.springframework.web.util.HtmlUtils;
public String safeHtml(String input) {
// 转义HTML特殊字符
// < 变成 <
// > 变成 >
// " 变成 "
return HtmlUtils.htmlEscape(input);
}
// 方法3:使用安全的模板引擎(Thymeleaf自动转义)
// 在Thymeleaf模板中
<p th:text="${userInput}"></p> <!-- 自动转义 -->
<p th:utext="${userInput}"></p> <!-- 不转义,危险! -->
4.3 CSRF攻击:借刀杀人
攻击场景:
- 你登录了银行网站
- 你访问了一个恶意网站
- 恶意网站偷偷向银行网站发转账请求
- 因为你有登录状态,银行认为是你的操作
防护方法:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 开启CSRF防护(默认开启)
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)
// 或者针对某些请求禁用(比如API)
.csrf(csrf -> csrf
.ignoringRequestMatchers("/api/**")
);
return http.build();
}
}
在前端提交表单时:
<!-- 自动添加CSRF Token -->
<form method="post">
<input type="hidden"
name="${_csrf.parameterName}"
value="${_csrf.token}" />
<!-- 其他表单字段 -->
</form>
<!-- 或者使用meta标签 -->
<meta name="_csrf" content="${_csrf.token}">
<meta name="_csrf_header" content="${_csrf.headerName}">
4.4 越权访问:看到了不该看的
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java 回复"Swagger源码",获取本文所有示例代码、配置模板及导出工具。 常见漏洞:
// ❌ 错误:只验证是否登录,没验证是否能看这个用户
@GetMapping("/user/{userId}/orders")
public List<Order> getOrders(@PathVariable Long userId) {
// 任何登录用户都能看别人的订单!
return orderRepository.findByUserId(userId);
}
防护方法:
// 方法1:在方法里校验
@GetMapping("/user/{userId}/orders")
public List<Order> getOrders(@PathVariable Long userId,
@AuthenticationPrincipal User currentUser) {
// 检查是否查看自己的订单
if (!currentUser.getId().equals(userId)) {
throw new AccessDeniedException("不能查看别人的订单");
}
return orderRepository.findByUserId(userId);
}
// 方法2:使用注解(推荐)
@PreAuthorize("#userId == authentication.principal.id")
@GetMapping("/user/{userId}/orders")
public List<Order> getOrders(@PathVariable Long userId) {
return orderRepository.findByUserId(userId);
}
// 方法3:使用自定义权限检查
@Component("securityService")
public class SecurityService {
public boolean canViewOrders(Long userId, User currentUser) {
// 复杂的权限逻辑
return userId.equals(currentUser.getId()) ||
currentUser.getRoles().contains("ADMIN");
}
}
@PreAuthorize("@securityService.canViewOrders(#userId, authentication.principal)")
@GetMapping("/user/{userId}/orders")
public List<Order> getOrders(@PathVariable Long userId) {
return orderRepository.findByUserId(userId);
}
五、进阶防护:OAuth2 + JWT
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java 回复"Swagger源码",获取本文所有示例代码、配置模板及导出工具。
5.1 什么是JWT?
JWT(JSON Web Token)就像电影票:
- 买票时验身份(登录)
- 拿到票(Token)
- 进场时验票(请求带Token)
- 票上有座位号(用户信息)
- 票会过期(Token有效期)
5.2 快速集成JWT
第一步:加依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
第二步:生成Token工具类
@Component
public class JwtTokenUtil {
@Value("${jwt.secret}")
private String secret; // 密钥
@Value("${jwt.expiration}")
private Long expiration; // 有效期(秒)
// 生成Token
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("username", userDetails.getUsername());
claims.put("roles", userDetails.getAuthorities());
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
// 验证Token
public boolean validateToken(String token, UserDetails userDetails) {
String username = extractUsername(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
// 从Token中获取用户名
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
// 检查Token是否过期
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
// 获取过期时间
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
}
第三步:JWT过滤器
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
// 1. 从请求头获取Token
String token = getTokenFromRequest(request);
if (token != null && jwtTokenUtil.validateToken(token)) {
// 2. 从Token中获取用户名
String username = jwtTokenUtil.extractUsername(token);
// 3. 加载用户信息
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 4. 设置认证信息
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.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;
}
}
第四步:配置Security
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 禁用CSRF(因为用Token)
.csrf(csrf -> csrf.disable())
// 设置权限
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll() // 登录注册公开
.requestMatchers("/api/public/**").permitAll() // 公开接口
.anyRequest().authenticated() // 其他需要认证
)
// 添加JWT过滤器
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
// 异常处理
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint((request, response, authException) -> {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "未认证");
})
.accessDeniedHandler((request, response, accessDeniedException) -> {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "无权限");
})
);
return http.build();
}
}
第五步:登录接口
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserDetailsService userDetailsService;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
try {
// 1. 认证
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);
// 2. 生成Token
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String token = jwtTokenUtil.generateToken(userDetails);
// 3. 返回Token
return ResponseEntity.ok(new LoginResponse(token));
} catch (BadCredentialsException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body("用户名或密码错误");
}
}
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody RegisterRequest request) {
// 注册逻辑
userService.register(request);
return ResponseEntity.ok("注册成功");
}
}
使用方式:
# 1. 登录获取Token
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"user","password":"123456"}'
# 返回:{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}
# 2. 用Token访问受保护接口
curl http://localhost:8080/api/user/profile \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
六、安全审计:知道谁干了什么
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java 回复"Swagger源码",获取本文所有示例代码、配置模板及导出工具。
6.1 记录操作日志
@Entity
@Table(name = "audit_log")
@Data
public class AuditLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username; // 操作用户
private String action; // 操作类型
private String target; // 操作目标
private String description; // 描述
private String ipAddress; // IP地址
private String userAgent; // 浏览器信息
private LocalDateTime time; // 操作时间
private Boolean success; // 是否成功
private String errorMessage; // 错误信息
}
@Aspect
@Component
@Slf4j
public class AuditAspect {
@Autowired
private AuditLogService auditLogService;
@Autowired
private HttpServletRequest request;
// 记录Controller操作
@Around("@annotation(audit)")
public Object audit(ProceedingJoinPoint joinPoint, Audit audit) throws Throwable {
long startTime = System.currentTimeMillis();
String username = getCurrentUsername();
String ip = getClientIp();
AuditLog auditLog = new AuditLog();
auditLog.setUsername(username);
auditLog.setAction(audit.action());
auditLog.setIpAddress(ip);
auditLog.setUserAgent(request.getHeader("User-Agent"));
auditLog.setTime(LocalDateTime.now());
try {
Object result = joinPoint.proceed();
auditLog.setSuccess(true);
auditLog.setDescription("操作成功");
return result;
} catch (Exception e) {
auditLog.setSuccess(false);
auditLog.setErrorMessage(e.getMessage());
auditLog.setDescription("操作失败: " + e.getMessage());
throw e;
} finally {
long costTime = System.currentTimeMillis() - startTime;
auditLog.setDescription(auditLog.getDescription() + ",耗时:" + costTime + "ms");
auditLogService.save(auditLog);
}
}
private String getCurrentUsername() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
return authentication.getName();
}
return "anonymous";
}
private String getClientIp() {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
// 使用注解记录操作
@RestController
public class UserController {
@Audit(action = "查询用户")
@GetMapping("/user/{id}")
public User getUser(@PathVariable Long id) {
return userService.getUserById(id);
}
@Audit(action = "删除用户")
@DeleteMapping("/user/{id}")
public void deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
}
}
6.2 敏感操作二次验证
@Service
public class SecurityService {
@Autowired
private SmsService smsService;
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 发送验证码
public void sendVerifyCode(String username, String operation) {
String code = generateRandomCode(); // 生成6位验证码
String key = "verify:" + username + ":" + operation;
// 存入Redis,5分钟过期
redisTemplate.opsForValue().set(key, code, 5, TimeUnit.MINUTES);
// 发送短信
smsService.sendSms(username, "验证码:" + code + ",用于" + operation);
}
// 验证验证码
public boolean verifyCode(String username, String operation, String code) {
String key = "verify:" + username + ":" + operation;
String storedCode = redisTemplate.opsForValue().get(key);
if (code.equals(storedCode)) {
// 验证成功,删除验证码
redisTemplate.delete(key);
return true;
}
return false;
}
private String generateRandomCode() {
Random random = new Random();
return String.format("%06d", random.nextInt(1000000));
}
}
// 敏感操作接口
@RestController
@RequestMapping("/api/security")
public class SecurityController {
@Autowired
private SecurityService securityService;
// 请求修改密码验证码
@PostMapping("/change-password/code")
public ResponseEntity<?> requestChangePasswordCode(@CurrentUser User user) {
securityService.sendVerifyCode(user.getUsername(), "change-password");
return ResponseEntity.ok("验证码已发送");
}
// 修改密码(需要验证码)
@PostMapping("/change-password")
public ResponseEntity<?> changePassword(@RequestBody ChangePasswordRequest request,
@CurrentUser User user) {
// 验证验证码
boolean verified = securityService.verifyCode(
user.getUsername(),
"change-password",
request.getCode()
);
if (!verified) {
return ResponseEntity.badRequest().body("验证码错误");
}
// 修改密码
userService.changePassword(user.getId(), request.getNewPassword());
return ResponseEntity.ok("密码修改成功");
}
}
七、实战:电商系统安全防护
7.1 完整的安全配置
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java 回复"Swagger源码",获取本文所有示例代码、配置模板及导出工具。
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法级安全
public class SecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Autowired
private RateLimitFilter rateLimitFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 1. 基础配置
.cors(cors -> cors.configurationSource(corsConfigurationSource())) // CORS
.csrf(csrf -> csrf.disable()) // API禁用CSRF
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 无状态
// 2. 权限配置
.authorizeHttpRequests(auth -> auth
// 公开接口
.requestMatchers(
"/api/auth/**", // 认证相关
"/api/public/**", // 公开数据
"/swagger-ui/**", // 文档
"/v3/api-docs/**" // OpenAPI
).permitAll()
// 用户接口
.requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
// 管理接口
.requestMatchers("/api/admin/**").hasRole("ADMIN")
// 其他需要认证
.anyRequest().authenticated()
)
// 3. 添加过滤器
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(rateLimitFilter, JwtAuthenticationFilter.class)
// 4. 异常处理
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(jwtAuthenticationEntryPoint())
.accessDeniedHandler(accessDeniedHandler())
)
// 5. 记住我(可选)
.rememberMe(remember -> remember
.tokenValiditySeconds(7 * 24 * 60 * 60) // 7天
.rememberMeParameter("remember-me")
)
// 6. 安全头
.headers(headers -> headers
.contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'"))
.frameOptions(frame -> frame.sameOrigin()) // 防止点击劫持
.xssProtection(xss -> xss.enable()) // XSS防护
);
return http.build();
}
// CORS配置
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("https://example.com")); // 允许的域名
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", configuration);
return source;
}
// 认证失败处理
@Bean
public AuthenticationEntryPoint jwtAuthenticationEntryPoint() {
return (request, response, authException) -> {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("{\"code\":401,\"message\":\"未认证或Token已过期\"}");
};
}
// 权限不足处理
@Bean
public AccessDeniedHandler accessDeniedHandler() {
return (request, response, accessDeniedException) -> {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write("{\"code\":403,\"message\":\"权限不足\"}");
};
}
// 密码加密器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
7.2 限流防护(防暴力破解)
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java 回复"Swagger源码",获取本文所有示例代码、配置模板及导出工具。
@Component
public class RateLimitFilter extends OncePerRequestFilter {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
String key = getRateLimitKey(request);
// 1. IP限流:每个IP每分钟100次
String ipKey = "rate:ip:" + getClientIp(request) + ":" + getMinuteKey();
if (!allowRequest(ipKey, 100, 60)) {
response.sendError(429, "请求过于频繁,请稍后重试");
return;
}
// 2. 用户限流:每个用户每分钟50次(需要登录)
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.isAuthenticated()) {
String username = auth.getName();
String userKey = "rate:user:" + username + ":" + getMinuteKey();
if (!allowRequest(userKey, 50, 60)) {
response.sendError(429, "操作过于频繁,请稍后重试");
return;
}
}
// 3. 接口限流:每个接口每分钟1000次
String apiKey = "rate:api:" + request.getRequestURI() + ":" + getMinuteKey();
if (!allowRequest(apiKey, 1000, 60)) {
response.sendError(429, "系统繁忙,请稍后重试");
return;
}
chain.doFilter(request, response);
}
private boolean allowRequest(String key, int limit, int windowSeconds) {
String countStr = redisTemplate.opsForValue().get(key);
int count = countStr == null ? 0 : Integer.parseInt(countStr);
if (count >= limit) {
return false;
}
redisTemplate.opsForValue().increment(key);
if (count == 0) {
redisTemplate.expire(key, windowSeconds + 10, TimeUnit.SECONDS); // 多加10秒缓冲
}
return true;
}
private String getRateLimitKey(HttpServletRequest request) {
String ip = getClientIp(request);
String uri = request.getRequestURI();
return "rate:" + ip + ":" + uri + ":" + getMinuteKey();
}
private String getClientIp(HttpServletRequest request) {
// 获取真实IP(考虑代理)
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
private String getMinuteKey() {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmm"));
}
}
八、安全自查清单
每次上线前,检查这10项:
-
✅ 密码是否加密存储?
不能用明文,不能用MD5,要用BCrypt -
✅ SQL是否防注入?
用参数绑定,不用字符串拼接 -
✅ XSS防护是否开启?
前端转义,后端过滤 -
✅ CSRF防护是否开启?
表单提交要带Token -
✅ 权限控制是否到位?
每个接口都要检查权限 -
✅ 敏感信息是否脱敏?
手机号、身份证、邮箱要脱敏显示 -
✅ 限流是否配置?
防暴力破解,防刷接口 -
✅ 日志是否记录操作?
谁在什么时候做了什么 -
✅ 错误信息是否安全?
不能暴露数据库结构、服务器信息 -
✅ HTTPS是否启用?
生产环境一定要用HTTPS
九、今日思考题
场景:你要设计一个银行转账系统,需要:
- 用户登录(密码+短信验证)
- 转账需要U盾验证
- 记录所有操作日志
- 防止重放攻击
- 防止中间人攻击
问题:
- 你会如何设计这个安全体系?
- 用到哪些安全技术?
- 如何平衡安全性和用户体验?
在评论区分享你的设计方案,点赞最高的送《白帽子讲Web安全》+《Java安全编码规范》!
系列总结:这7天我们学了什么?
- 第一天:自动配置原理 ✅
- 第二天:全局异常处理 ✅
- 第三天:配置管理 ✅
- 第四天:接口文档 ✅
- 第五天:限流防护 ✅
- 第六天:日志监控 ✅
- 今天:安全防护 ✅
SpringBoot的核心技能都掌握了! 从今天起,你不再是个只会CRUD的程序员,而是能设计、开发、部署、监控、优化、保护完整系统的工程师!
安全工具包:关注公众号回复"安全防护",获取完整的安全配置模板、渗透测试工具、安全自查清单!
CSDN运营小贴士:
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java 回复"Swagger源码",获取本文所有示例代码、配置模板及导出工具。
🏷️ 今日标签:#SpringBoot安全 #Web安全 #Java安全
💡 互动设计:
- 话题讨论:你经历过或知道哪些安全事件?
- 投票:你们项目最重视哪方面的安全?
- 经验征集:分享你的安全防护经验,抽3位送《Web安全攻防》实体书
🎁 系列福利: 4. 关注后回复"SpringBoot7天",获取全套源码和PPT 5. 转发全部7篇文章到朋友圈,截图领《SpringBoot实战全家桶》纸质书 6. 评论区抽奖:送5个《Spring Boot揭秘》实体书
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java 回复"Swagger源码",获取本文所有示例代码、配置模板及导出工具。
🔥 新系列预告: "下周开始新系列:《微服务实战7天从入门到精通》,想看的在评论区扣1!"
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java 回复"Swagger源码",获取本文所有示例代码、配置模板及导出工具。