一、一次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
- 监控:安全日志,异常检测
最佳实践:
- 密钥不写在代码里
- 密码不加密存储,用BCrypt
- 敏感数据要加密
- 用Security框架,不要自己判断权限
- 安全日志要记录,关键操作要审计
血的教训:
安全不是事后补救,是从设计阶段就要考虑的。不要等到出了问题再想起来加安全措施,那时候可能已经晚了。
思考题: 你的系统有没有做过安全渗透测试?如果有,发现了哪些问题?
个人观点,仅供参考