【架构实战】安全性设计:让系统固若金汤

一、一次SQL注入差点让我丢了工作

2016年,我刚入职没多久,组长让我改一个查询接口。

用户输入搜索关键词,后端直接拼SQL:

java 复制代码
String sql = "SELECT * FROM products WHERE name LIKE '%" + keyword + "%'";

我觉得不太对,但想着"老代码应该没问题",就这么提交了。

两周后,渗透测试报告出来:SQL注入漏洞,高危。leader气得脸都绿了。

从那以后,我开始认真对待系统安全。


二、安全威胁与防御体系

2.1 OWASP Top 10

复制代码
OWASP Top 10(2021):

1. 访问控制失效(Broken Access Control)
2. 加密失败(Cryptographic Failures)
3. 注入(Injection)
4. 不安全设计(Insecure Design)
5. 安全配置错误(Security Misconfiguration)
6. 敏感数据泄露(Sensitive Data Exposure)
7. 不足的日志和监控(Insufficient Logging & Monitoring)
8. SSRF(Server-Side Request Forgery)
9. 使用有漏洞的组件(Using Components with Known Vulnerabilities)
10. 身份验证失效(Authentication Failures)

2.2 纵深防御体系

复制代码
┌──────────────────────────────────────────────────────────┐
│                      纵深防御                              │
│                                                           │
│  第一层:边界防护                                          │
│  - WAF(Web应用防火墙)                                    │
│  - API Gateway限流                                        │
│  - CDN隐藏源站                                            │
│                                                           │
│  第二层:身份认证                                          │
│  - 强密码策略                                             │
│  - 多因素认证                                             │
│  - JWT Token过期机制                                      │
│                                                           │
│  第三层:访问控制                                          │
│  - RBAC权限模型                                           │
│  - 资源级别权限                                           │
│  - 行级安全                                              │
│                                                           │
│  第四层:应用安全                                         │
│  - 输入验证                                               │
│  - SQL注入防护                                            │
│  - XSS防护                                               │
│                                                           │
│  第五层:数据安全                                         │
│  - 敏感数据加密存储                                        │
│  - 传输加密(TLS)                                        │
│  - 数据脱敏                                              │
│                                                           │
└──────────────────────────────────────────────────────────┘

三、身份认证与授权

3.1 JWT认证

java 复制代码
// JWT工具类
@Service
@Slf4j
public class JwtService {
    
    @Value("${jwt.secret}")
    private String secret;
    
    @Value("${jwt.access-token-validity}")
    private long accessTokenValidity;
    
    @Value("${jwt.refresh-token-validity}")
    private long refreshTokenValidity;
    
    /**
     * 生成Access Token
     */
    public String generateAccessToken(String userId, String role) {
        Date now = new Date();
        Date expiry = new Date(now.getTime() + accessTokenValidity);
        
        return Jwts.builder()
            .setSubject(userId)
            .claim("role", role)
            .claim("type", "access")
            .setIssuedAt(now)
            .setExpiration(expiry)
            .signWith(Keys.hmacShaKeyFor(secret.getBytes()), SignatureAlgorithm.HS256)
            .compact();
    }
    
    /**
     * 生成Refresh Token(有效期更长)
     */
    public String generateRefreshToken(String userId) {
        Date now = new Date();
        Date expiry = new Date(now.getTime() + refreshTokenValidity);
        
        return Jwts.builder()
            .setSubject(userId)
            .claim("type", "refresh")
            .setIssuedAt(now)
            .setExpiration(expiry)
            .signWith(Keys.hmacShaKeyFor(secret.getBytes()), SignatureAlgorithm.HS256)
            .compact();
    }
    
    /**
     * 验证Token
     */
    public Claims validateToken(String token) {
        try {
            return Jwts.parserBuilder()
                .setSigningKey(Keys.hmacShaKeyFor(secret.getBytes()))
                .build()
                .parseClaimsJws(token)
                .getBody();
        } catch (ExpiredJwtException e) {
            log.warn("Token已过期: {}", e.getMessage());
            throw new TokenExpiredException("Token已过期");
        } catch (JwtException e) {
            log.warn("Token无效: {}", e.getMessage());
            throw new InvalidTokenException("Token无效");
        }
    }
    
    /**
     * 解析用户ID
     */
    public String getUserIdFromToken(String token) {
        return validateToken(token).getSubject();
    }
}

3.2 Spring Security配置

java 复制代码
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
    
    @Autowired
    private JwtAuthenticationFilter jwtFilter;
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // 禁用CSRF(使用JWT不需要)
            .csrf(csrf -> csrf.disable())
            
            // CORS配置
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            
            // Session管理
            .sessionManagement(session -> 
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            
            // 权限配置
            .authorizeHttpRequests(auth -> auth
                // 公开接口
                .antMatchers("/auth/**", "/public/**", "/health").permitAll()
                // 管理员接口
                .antMatchers("/admin/**").hasRole("ADMIN")
                // 其他接口需要认证
                .anyRequest().authenticated()
            )
            
            // 添加JWT过滤器
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
            
            // 异常处理
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint((request, response, authException) -> {
                    response.setContentType("application/json");
                    response.getWriter().write("{\"code\":401,\"message\":\"未登录\"}");
                })
                .accessDeniedHandler((request, response, accessDeniedException) -> {
                    response.setContentType("application/json");
                    response.getWriter().write("{\"code\":403,\"message\":\"权限不足\"}");
                })
            );
        
        return http.build();
    }
    
    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOriginPatterns(Arrays.asList("*"));
        config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
        config.setAllowedHeaders(Arrays.asList("*"));
        config.setAllowCredentials(true);
        config.setMaxAge(3600L);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return source;
    }
}

3.3 JWT过滤器

java 复制代码
@Component
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    @Autowired
    private JwtService jwtService;
    
    @Autowired
    private UserService userService;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {
        
        String authHeader = request.getHeader("Authorization");
        
        // 没有Token,放行(让Security判断是否需要认证)
        if (StringUtils.isBlank(authHeader)) {
            filterChain.doFilter(request, response);
            return;
        }
        
        // 格式必须是 Bearer xxx
        if (!authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }
        
        String token = authHeader.substring(7);
        
        try {
            // 验证Token
            Claims claims = jwtService.validateToken(token);
            String userId = claims.getSubject();
            String role = claims.get("role", String.class);
            String tokenType = claims.get("type", String.class);
            
            // Refresh Token不能用于API访问
            if ("refresh".equals(tokenType)) {
                filterChain.doFilter(request, response);
                return;
            }
            
            // 查询用户信息
            UserDetails user = userService.loadUserById(userId);
            
            // 创建认证对象
            UsernamePasswordAuthenticationToken authentication =
                new UsernamePasswordAuthenticationToken(
                    user, null, user.getAuthorities()
                );
            authentication.setDetails(
                new WebAuthenticationDetailsSource().buildDetails(request)
            );
            
            // 设置到SecurityContext
            SecurityContextHolder.getContext().setAuthentication(authentication);
            
            filterChain.doFilter(request, response);
            
        } catch (TokenExpiredException e) {
            response.setContentType("application/json");
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write("{\"code\":401,\"message\":\"Token已过期\"}");
        } catch (InvalidTokenException e) {
            response.setContentType("application/json");
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write("{\"code\":401,\"message\":\"Token无效\"}");
        }
    }
}

四、RBAC权限模型

4.1 数据库设计

sql 复制代码
-- 用户表
CREATE TABLE users (
    id VARCHAR(36) PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    password VARCHAR(255) NOT NULL,  -- BCrypt加密
    email VARCHAR(100),
    status TINYINT DEFAULT 1,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- 角色表
CREATE TABLE roles (
    id VARCHAR(36) PRIMARY KEY,
    code VARCHAR(50) NOT NULL UNIQUE,  -- ADMIN, USER, VIP
    name VARCHAR(100),
    description VARCHAR(255)
);

-- 权限表
CREATE TABLE permissions (
    id VARCHAR(36) PRIMARY KEY,
    code VARCHAR(100) NOT NULL UNIQUE,  -- user:read, user:write, order:delete
    name VARCHAR(100),
    resource_type VARCHAR(50),  -- MENU, BUTTON, API
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 用户角色关联表
CREATE TABLE user_roles (
    user_id VARCHAR(36),
    role_id VARCHAR(36),
    PRIMARY KEY (user_id, role_id),
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE
);

-- 角色权限关联表
CREATE TABLE role_permissions (
    role_id VARCHAR(36),
    permission_id VARCHAR(36),
    PRIMARY KEY (role_id, permission_id),
    FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
    FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
);

4.2 权限服务实现

java 复制代码
@Service
@Slf4j
public class PermissionService implements UserDetailsService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private RoleRepository roleRepository;
    
    @Autowired
    private PermissionRepository permissionRepository;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException("用户不存在"));
        
        return buildUserDetails(user);
    }
    
    public UserDetails loadUserById(String userId) {
        User user = userRepository.findById(userId)
            .orElseThrow(() -> new UsernameNotFoundException("用户不存在"));
        
        return buildUserDetails(user);
    }
    
    private UserDetails buildUserDetails(User user) {
        // 获取用户的所有角色
        List<Role> roles = roleRepository.findByUserId(user.getId());
        
        // 获取角色对应的所有权限
        List<Permission> permissions = permissionRepository
            .findByRoleIds(roles.stream().map(Role::getId).collect(Collectors.toList()));
        
        // 构建权限列表
        List<GrantedAuthority> authorities = new ArrayList<>();
        roles.forEach(role -> 
            authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getCode()))
        );
        permissions.forEach(perm -> 
            authorities.add(new SimpleGrantedAuthority(perm.getCode()))
        );
        
        return new org.springframework.security.core.userdetails.User(
            user.getUsername(),
            user.getPassword(),
            user.getStatus() == 1,
            true, true, true,
            authorities
        );
    }
    
    /**
     * 检查用户是否有指定权限
     */
    public boolean hasPermission(String userId, String permission) {
        List<Permission> permissions = permissionRepository
            .findByUserId(userId);
        return permissions.stream()
            .anyMatch(p -> p.getCode().equals(permission));
    }
}

4.3 方法级权限控制

java 复制代码
@RestController
@RequestMapping("/api")
public class UserController {
    
    /**
     * 查询用户(需要user:read权限)
     */
    @GetMapping("/user/{id}")
    @PreAuthorize("hasAuthority('user:read') or hasRole('ADMIN')")
    public Result<User> getUser(@PathVariable String id) {
        // ...
        return Result.success(user);
    }
    
    /**
     * 创建用户(需要user:write权限)
     */
    @PostMapping("/user")
    @PreAuthorize("hasAuthority('user:write')")
    public Result<Void> createUser(@RequestBody @Validated UserRequest request) {
        // ...
        return Result.success();
    }
    
    /**
     * 删除用户(需要user:delete权限)
     */
    @DeleteMapping("/user/{id}")
    @PreAuthorize("hasAuthority('user:delete') or hasRole('ADMIN')")
    public Result<Void> deleteUser(@PathVariable String id) {
        // ...
        return Result.success();
    }
}

五、常见攻击防护

5.1 SQL注入防护

java 复制代码
// ❌ 危险:字符串拼接SQL
@Query("SELECT * FROM users WHERE username = '" + username + "'")
User findByUsernameDangerous(String username);

// ✅ 安全:参数化查询
@Query("SELECT * FROM users WHERE username = :username")
User findByUsername(@Param("username") String username);

// ✅ 安全:使用JPA自动实现
User findByUsername(String username);

// ✅ 安全:手动参数化
@Repository
public class UserRepositoryImpl implements UserRepositoryCustom {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    @Override
    public User findByUsernameSafe(String username) {
        String sql = "SELECT * FROM users WHERE username = ?";
        return jdbcTemplate.queryForObject(sql, 
            new Object[]{username}, 
            new UserRowMapper()
        );
    }
}

5.2 XSS防护

java 复制代码
// HTML转义工具类
public class XssUtil {
    
    private static final HtmlEscaper HTML_ESCAPER = HtmlEscapers.htmlEscaper();
    
    /**
     * HTML转义
     */
    public static String escape(String input) {
        if (input == null) {
            return null;
        }
        return HTML_ESCAPER.escape(input);
    }
    
    /**
     * 富文本白名单过滤(保留部分HTML标签)
     */
    public static String filterHtml(String input) {
        if (input == null) {
            return null;
        }
        
        // 配置白名单
        Policy policy = PolicyFactoryFactory.create(
            TagWhitelist,
            Arrays.asList("p", "br", "b", "i", "u", "em", "strong", "a", "img")
        );
        
        return policy.sanitize(input);
    }
}

// 全局XSS过滤器
@Component
@Slf4j
public class XssFilter extends OncePerRequestFilter {
    
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain)
            throws ServletException, IOException {
        
        chain.doFilter(new XssRequestWrapper(request), response);
    }
}

public class XssRequestWrapper extends HttpServletRequestWrapper {
    
    public XssRequestWrapper(HttpServletRequest request) {
        super(request);
    }
    
    @Override
    public String getParameter(String name) {
        String value = super.getParameter(name);
        return XssUtil.escape(value);
    }
    
    @Override
    public String[] getParameterValues(String name) {
        String[] values = super.getParameterValues(name);
        if (values == null) {
            return null;
        }
        return Arrays.stream(values)
            .map(XssUtil::escape)
            .toArray(String[]::new);
    }
}

5.3 CSRF防护

java 复制代码
@Configuration
@EnableWebSecurity
public class CsrfConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf
                // 开启CSRF保护
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                // 忽略API路径
                .ignoringAntMatchers("/api/public/**", "/auth/**")
            )
            // 其他配置...
            ;
        
        return http.build();
    }
}

// 前端获取CSRF Token
function getCsrfToken() {
    const name = 'XSRF-TOKEN';
    let token = Cookies.get(name);
    if (!token) {
        const cookie = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
        if (cookie) token = cookie[2];
    }
    return token;
}

// 请求时添加Token
function request(url, options) {
    const token = getCsrfToken();
    return fetch(url, {
        ...options,
        headers: {
            ...options.headers,
            'X-XSRF-TOKEN': token
        }
    });
}

5.4 SSRF防护

java 复制代码
@Service
@Slf4j
public class UrlValidationService {
    
    /**
     * URL白名单验证
     */
    public boolean isUrlAllowed(String urlStr) {
        try {
            URL url = new URL(urlStr);
            String host = url.getHost();
            
            // 只允许HTTP/HTTPS
            String protocol = url.getProtocol();
            if (!protocol.equals("http") && !protocol.equals("https")) {
                return false;
            }
            
            // 解析IP,禁止内网IP
            InetAddress address = InetAddress.getByName(host);
            if (address.isSiteLocalAddress() ||
                address.isLoopbackAddress() ||
                address.isLinkLocalAddress() ||
                address.isAnyLocalAddress()) {
                log.warn("SSRF风险:禁止访问内网地址 {}", host);
                return false;
            }
            
            // 检查IP是否为内网
            byte[] ipBytes = address.getAddress();
            if (isPrivateIp(ipBytes)) {
                log.warn("SSRF风险:禁止访问私有IP {}", host);
                return false;
            }
            
            // DNS Rebinding防护:二次解析
            Thread.sleep(500);  // 延迟500ms
            InetAddress secondAddress = InetAddress.getByName(host);
            if (!address.equals(secondAddress)) {
                log.warn("SSRF风险:DNS Rebinding检测 {}", host);
                return false;
            }
            
            return true;
            
        } catch (Exception e) {
            log.error("URL验证异常: {}", urlStr, e);
            return false;
        }
    }
    
    /**
     * 判断是否为私有IP
     */
    private boolean isPrivateIp(byte[] ip) {
        // 10.0.0.0 - 10.255.255.255
        if (ip[0] == 10) return true;
        
        // 172.16.0.0 - 172.31.255.255
        if (ip[0] == (byte) 172 && ip[1] >= 16 && ip[1] <= 31) return true;
        
        // 192.168.0.0 - 192.168.255.255
        if (ip[0] == (byte) 192 && ip[1] == (byte) 168) return true;
        
        // 127.0.0.0 - 127.255.255.255
        if (ip[0] == 127) return true;
        
        return false;
    }
}

六、敏感数据安全

6.1 加密存储

java 复制代码
// SM4国密加密(也支持AES)
@Service
@Slf4j
public class EncryptionService {
    
    @Value("${encryption.sm4.key}")
    private String sm4Key;  // 32位十六进制密钥
    
    /**
     * SM4加密
     */
    public String encrypt(String plaintext) {
        try {
            SM4Util sm4 = new SM4Util();
            sm4.setKey(Hex.decodeHex(sm4Key.toCharArray()));
            
            String ciphertext = sm4.encryptData_CBC(plaintext);
            return Base64.getEncoder().encodeToString(
                Hex.decodeHex(ciphertext.toCharArray())
            );
        } catch (Exception e) {
            log.error("加密失败", e);
            throw new EncryptionException("加密失败");
        }
    }
    
    /**
     * SM4解密
     */
    public String decrypt(String ciphertext) {
        try {
            SM4Util sm4 = new SM4Util();
            sm4.setKey(Hex.decodeHex(sm4Key.toCharArray()));
            
            byte[] encrypted = Base64.getDecoder().decode(ciphertext);
            return sm4.decryptData_CBC(Hex.encodeHexString(encrypted));
        } catch (Exception e) {
            log.error("解密失败", e);
            throw new EncryptionException("解密失败");
        }
    }
}

// 字段级加密注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Encrypted {
    // 可以指定加密类型等
}

// 自动加密/解密处理器
@Component
public class EncryptedFieldHandler {
    
    @Autowired
    private EncryptionService encryptionService;
    
    /**
     * 对象保存前加密
     */
    public void encrypt(Object entity) {
        Class<?> clazz = entity.getClass();
        for (Field field : clazz.getDeclaredFields()) {
            if (field.isAnnotationPresent(Encrypted.class)) {
                try {
                    field.setAccessible(true);
                    Object value = field.get(entity);
                    if (value != null) {
                        String encrypted = encryptionService.encrypt(value.toString());
                        field.set(entity, encrypted);
                    }
                } catch (Exception e) {
                    log.error("字段加密失败: {}", field.getName(), e);
                }
            }
        }
    }
    
    /**
     * 对象读取后解密
     */
    public void decrypt(Object entity) {
        Class<?> clazz = entity.getClass();
        for (Field field : clazz.getDeclaredFields()) {
            if (field.isAnnotationPresent(Encrypted.class)) {
                try {
                    field.setAccessible(true);
                    Object value = field.get(entity);
                    if (value != null) {
                        String decrypted = encryptionService.decrypt(value.toString());
                        field.set(entity, decrypted);
                    }
                } catch (Exception e) {
                    log.error("字段解密失败: {}", field.getName(), e);
                }
            }
        }
    }
}

6.2 密码加密

java 复制代码
// BCrypt密码加密
@Service
public class PasswordService {
    
    private final PasswordEncoder encoder = new BCryptPasswordEncoder();
    
    /**
     * 密码加密
     */
    public String encode(String rawPassword) {
        // BCrypt会自动加盐,每次的加密结果都不同
        return encoder.encode(rawPassword);
    }
    
    /**
     * 密码验证
     */
    public boolean matches(String rawPassword, String encodedPassword) {
        return encoder.matches(rawPassword, encodedPassword);
    }
    
    /**
     * 密码强度验证
     */
    public boolean isStrongPassword(String password) {
        if (password == null || password.length() < 8) {
            return false;
        }
        
        boolean hasUpper = false, hasLower = false, hasDigit = false, hasSpecial = false;
        for (char c : password.toCharArray()) {
            if (Character.isUpperCase(c)) hasUpper = true;
            else if (Character.isLowerCase(c)) hasLower = true;
            else if (Character.isDigit(c)) hasDigit = true;
            else hasSpecial = true;
        }
        
        int strength = 0;
        if (hasUpper) strength++;
        if (hasLower) strength++;
        if (hasDigit) strength++;
        if (hasSpecial) strength++;
        
        return strength >= 3;
    }
}

6.3 数据脱敏

java 复制代码
// 敏感数据脱敏
@Component
public class DataMaskingUtil {
    
    /**
     * 手机号脱敏:138****5678
     */
    public static String maskPhone(String phone) {
        if (phone == null || phone.length() < 11) {
            return phone;
        }
        return phone.substring(0, 3) + "****" + phone.substring(7);
    }
    
    /**
     * 身份证脱敏:310***********1234
     */
    public static String maskIdCard(String idCard) {
        if (idCard == null || idCard.length() < 15) {
            return idCard;
        }
        return idCard.substring(0, 3) + "***********" + idCard.substring(idCard.length() - 4);
    }
    
    /**
     * 银行卡脱敏:622202***********1234
     */
    public static String maskBankCard(String card) {
        if (card == null || card.length() < 12) {
            return card;
        }
        return card.substring(0, 6) + "*********" + card.substring(card.length() - 4);
    }
    
    /**
     * 邮箱脱敏:t***@example.com
     */
    public static String maskEmail(String email) {
        if (email == null || !email.contains("@")) {
            return email;
        }
        String[] parts = email.split("@");
        String name = parts[0];
        if (name.length() <= 2) {
            return name.charAt(0) + "***@" + parts[1];
        }
        return name.charAt(0) + "***@" + parts[1];
    }
    
    /**
     * 地址脱敏
     */
    public static String maskAddress(String address) {
        if (address == null || address.length() < 6) {
            return address;
        }
        return address.substring(0, 6) + "***";
    }
}

七、安全监控与日志

7.1 安全日志记录

java 复制代码
@Service
@Slf4j
public class SecurityLogService {
    
    /**
     * 记录登录日志
     */
    public void logLogin(String userId, String ip, boolean success, String reason) {
        SecurityLog log = new SecurityLog();
        log.setType("LOGIN");
        log.setUserId(userId);
        log.setIp(ip);
        log.setSuccess(success);
        log.setReason(reason);
        log.setCreateTime(new Date());
        securityLogRepository.save(log);
        
        // 登录失败告警
        if (!success && "PASSWORD_ERROR".equals(reason)) {
            alertingService.alert("登录失败", 
                String.format("用户 %s 密码错误,IP: %s", userId, ip));
        }
    }
    
    /**
     * 记录敏感操作
     */
    public void logSensitiveOperation(String userId, String operation, 
                                      String resource, String ip) {
        SecurityLog log = new SecurityLog();
        log.setType("SENSITIVE_OPERATION");
        log.setUserId(userId);
        log.setOperation(operation);
        log.setResource(resource);
        log.setIp(ip);
        log.setCreateTime(new Date());
        securityLogRepository.save(log);
        
        log.info("敏感操作: user={}, op={}, resource={}, ip={}", 
            userId, operation, resource, ip);
    }
    
    /**
     * 记录权限变更
     */
    public void logPermissionChange(String adminId, String targetUserId,
                                   String oldRole, String newRole) {
        SecurityLog log = new SecurityLog();
        log.setType("PERMISSION_CHANGE");
        log.setUserId(adminId);
        log.setTargetUserId(targetUserId);
        log.setDetail(String.format("角色变更: %s → %s", oldRole, newRole));
        log.setCreateTime(new Date());
        securityLogRepository.save(log);
    }
}

7.2 异常访问监控

java 复制代码
@Component
@Slf4j
public class SecurityMonitor {
    
    private ConcurrentHashMap<String, AccessRecord> accessCache = new ConcurrentHashMap<>();
    
    /**
     * 记录访问
     */
    public void recordAccess(String ip, String path, int status) {
        String key = ip + ":" + LocalDate.now();
        AccessRecord record = accessCache.computeIfAbsent(key, k -> new AccessRecord());
        record.increment(status);
    }
    
    /**
     * 检测异常访问
     */
    public boolean isAbnormalAccess(String ip) {
        String key = ip + ":" + LocalDate.now();
        AccessRecord record = accessCache.get(key);
        
        if (record == null) {
            return false;
        }
        
        // 1分钟内超过100次404,异常
        if (record.getNotFoundCount() > 100) {
            log.warn("异常访问检测:IP {} 大量404请求", ip);
            return true;
        }
        
        // 1分钟内超过500次请求,异常
        if (record.getTotalCount() > 500) {
            log.warn("异常访问检测:IP {} 请求过于频繁", ip);
            return true;
        }
        
        return false;
    }
    
    /**
     * 检测扫描行为
     */
    public boolean isScanning(String ip) {
        String key = ip + ":" + LocalDate.now();
        AccessRecord record = accessCache.get(key);
        
        if (record == null) {
            return false;
        }
        
        // 1分钟内访问超过50个不同路径,可能是扫描
        if (record.getUniquePaths() > 50) {
            log.warn("扫描行为检测:IP {} 访问了多个路径", ip);
            return true;
        }
        
        return false;
    }
}

八、踩坑实录

坑1:JWT密钥硬编码

生产环境JWT密钥写在代码里,被人翻到了。

教训:密钥必须通过环境变量或密钥管理服务注入。

yaml 复制代码
# ❌ 错误
jwt:
  secret: my-super-secret-key

# ✅ 正确:使用环境变量
jwt:
  secret: ${JWT_SECRET}  # 从环境变量或K8s Secret读取

坑2:权限判断用了字符串比较

用了if (role.equals("ADMIN"))而不是hasRole("ADMIN"),结果普通用户也能执行管理员操作。

教训:用Spring Security的方法,不要自己判断。

java 复制代码
// ❌ 危险:字符串比较
if (user.getRole().equals("ADMIN")) {
    // 删除数据
}

// ✅ 安全:使用Security框架
@PreAuthorize("hasRole('ADMIN')")
public void deleteData() {
    // ...
}

坑3:日志打印了密码

调试时打印了用户对象,结果密码明文出现在日志里。

教训:打印日志前要先脱敏。

java 复制代码
// ❌ 危险
log.info("登录成功: {}", user);

// ✅ 安全:打印ID,不打印敏感信息
log.info("登录成功: userId={}, username={}", user.getId(), user.getUsername());

九、总结

系统安全是重中之重:

  • 认证:JWT + Refresh Token机制
  • 授权:RBAC权限模型,方法级控制
  • 防护:防止SQL注入、XSS、CSRF、SSRF
  • 加密:敏感数据加密存储,密码BCrypt
  • 监控:安全日志,异常检测

最佳实践:

  1. 密钥不写在代码里
  2. 密码不加密存储,用BCrypt
  3. 敏感数据要加密
  4. 用Security框架,不要自己判断权限
  5. 安全日志要记录,关键操作要审计

血的教训:

安全不是事后补救,是从设计阶段就要考虑的。不要等到出了问题再想起来加安全措施,那时候可能已经晚了。

思考题: 你的系统有没有做过安全渗透测试?如果有,发现了哪些问题?


个人观点,仅供参考

相关推荐
东方佑1 小时前
FRSM 规模效应与架构对比补充报告
架构
隔窗听雨眠3 小时前
大模型加爬虫上篇:技术融合与架构革新
爬虫·架构
Vergelight4 小时前
实战拆解|三类RAG架构差异:朴素、进阶、多轮RAG落地选型指南
架构·大模型·aigc·agent·ai产品经理·转行·ai后台设计
Database_Cool_4 小时前
大规模数据分析降本指南:AnalyticDB Serverless 弹性架构实战
数据仓库·阿里云·架构·数据分析·serverless
绿算技术4 小时前
Mooncake 与绿算ForinnBase GroundPool如何联手打破推理僵局?
科技·算法·架构
阿米亚波5 小时前
【Windows】QEMU 启动 openEuler aarch64/arm64 架构系统 + 离线软件源
linux·windows·经验分享·笔记·架构·arm
taocarts_bidfans6 小时前
反向海淘跨境缓存架构优化:taocarts Redis分层缓存实战技术
redis·缓存·架构·反向海淘·taocarts
by————组态7 小时前
Ricon组态系统 - 新一代Web可视化组态平台
前端·后端·物联网·架构·组态·组态软件
@insist1237 小时前
系统架构设计师-5G 技术、冗余设计与分层架构
5g·架构·系统架构·软考·系统架构设计师·软件水平考试
yspwf7 小时前
NestJS 配置管理完整方案
后端·架构·node.js