SpringBoot安全防护:你的应用正在被黑客“光顾“吗?

25 阅读16分钟

每天5分钟,掌握一个SpringBoot核心知识点。大家好,我是SpringBoot指南的小坏。经过一周的学习,我们从配置、日志、监控到性能优化都讲完了,今天来聊聊最重要也最容易被忽略的话题——安全

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java 回复"Swagger源码",获取本文所有示例代码、配置模板及导出工具。

一、真实故事:我差点丢了工作

去年公司上线了一个新系统,我负责开发。上线后一切正常,直到有一天...

凌晨2点,老板电话来了:"小坏,数据库被删了!" 我:"不可能啊,我们密码很复杂的..." 查日志发现:有人用SQL注入,直接删除了整个用户表。

更可怕的是

  1. 用户密码全部明文存储
  2. 越权访问,普通用户能看到管理员数据
  3. 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的优点

  1. 自动加盐,相同密码加密结果不同
  2. 计算慢,防暴力破解
  3. 行业标准,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特殊字符
    // < 变成 &lt;
    // > 变成 &gt;
    // " 变成 &quot;
    return HtmlUtils.htmlEscape(input);
}

// 方法3:使用安全的模板引擎(Thymeleaf自动转义)
// 在Thymeleaf模板中
<p th:text="${userInput}"></p>  <!-- 自动转义 -->
<p th:utext="${userInput}"></p> <!-- 不转义,危险! -->

4.3 CSRF攻击:借刀杀人

攻击场景

  1. 你登录了银行网站
  2. 你访问了一个恶意网站
  3. 恶意网站偷偷向银行网站发转账请求
  4. 因为你有登录状态,银行认为是你的操作

防护方法

@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项:

  1. ✅ 密码是否加密存储?
    不能用明文,不能用MD5,要用BCrypt

  2. ✅ SQL是否防注入?
    用参数绑定,不用字符串拼接

  3. ✅ XSS防护是否开启?
    前端转义,后端过滤

  4. ✅ CSRF防护是否开启?
    表单提交要带Token

  5. ✅ 权限控制是否到位?
    每个接口都要检查权限

  6. ✅ 敏感信息是否脱敏?
    手机号、身份证、邮箱要脱敏显示

  7. ✅ 限流是否配置?
    防暴力破解,防刷接口

  8. ✅ 日志是否记录操作?
    谁在什么时候做了什么

  9. ✅ 错误信息是否安全?
    不能暴露数据库结构、服务器信息

  10. ✅ HTTPS是否启用?
    生产环境一定要用HTTPS

九、今日思考题

场景:你要设计一个银行转账系统,需要:

  1. 用户登录(密码+短信验证)
  2. 转账需要U盾验证
  3. 记录所有操作日志
  4. 防止重放攻击
  5. 防止中间人攻击

问题

  1. 你会如何设计这个安全体系?
  2. 用到哪些安全技术?
  3. 如何平衡安全性和用户体验?

在评论区分享你的设计方案,点赞最高的送《白帽子讲Web安全》+《Java安全编码规范》!


系列总结:这7天我们学了什么?

  1. 第一天:自动配置原理 ✅
  2. 第二天:全局异常处理 ✅
  3. 第三天:配置管理 ✅
  4. 第四天:接口文档 ✅
  5. 第五天:限流防护 ✅
  6. 第六天:日志监控 ✅
  7. 今天:安全防护 ✅

SpringBoot的核心技能都掌握了! 从今天起,你不再是个只会CRUD的程序员,而是能设计、开发、部署、监控、优化、保护完整系统的工程师!

安全工具包:关注公众号回复"安全防护",获取完整的安全配置模板、渗透测试工具、安全自查清单!


CSDN运营小贴士:

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java 回复"Swagger源码",获取本文所有示例代码、配置模板及导出工具。

🏷️ 今日标签:#SpringBoot安全 #Web安全 #Java安全

💡 互动设计

  1. 话题讨论:你经历过或知道哪些安全事件?
  2. 投票:你们项目最重视哪方面的安全?
  3. 经验征集:分享你的安全防护经验,抽3位送《Web安全攻防》实体书

🎁 系列福利: 4. 关注后回复"SpringBoot7天",获取全套源码和PPT 5. 转发全部7篇文章到朋友圈,截图领《SpringBoot实战全家桶》纸质书 6. 评论区抽奖:送5个《Spring Boot揭秘》实体书

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java 回复"Swagger源码",获取本文所有示例代码、配置模板及导出工具。

🔥 新系列预告: "下周开始新系列:《微服务实战7天从入门到精通》,想看的在评论区扣1!"

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java 回复"Swagger源码",获取本文所有示例代码、配置模板及导出工具。