diff --git a/docs/design/technical/SEC-安全设计.md b/docs/design/technical/SEC-安全设计.md new file mode 100644 index 0000000..b5c47b1 --- /dev/null +++ b/docs/design/technical/SEC-安全设计.md @@ -0,0 +1,626 @@ +# 健身房管理系统安全设计文档 + +> 文档编号:GYM-SEC-DESIGN-001 +> 版本:v1.0 +> 创建日期:2026-03-08 +> 最后更新日期:2026-03-08 +> 作者:张翔 +> 状态:正式发布 + +## 文档修订历史 + +| 版本 | 日期 | 作者 | 修订内容 | +| ---- | ---------- | ---- | -------- | +| v1.0 | 2026-03-08 | 张翔 | 创建安全设计文档 | + +## 参考文档 + +- OWASP Top 10 安全规范 +- Spring Security 官方文档 +- GDPR 数据保护条例 +- 网络安全等级保护 2.0 + +--- + +## 一、安全架构设计 + +### 1.1 安全分层 + +``` +┌─────────────────────────────────────┐ +│ 应用层安全 │ +│ (认证、授权、输入验证、输出编码) │ +├─────────────────────────────────────┤ +│ 数据层安全 │ +│ (加密、脱敏、审计、备份) │ +├─────────────────────────────────────┤ +│ 基础设施安全 │ +│ (网络安全、主机安全、容器安全) │ +└─────────────────────────────────────┘ +``` + +### 1.2 安全原则 + +1. **纵深防御**:多层安全防护 +2. **最小权限**:只授予必要权限 +3. **默认安全**:默认配置即安全 +4. **零信任**:始终验证,永不信任 +5. **安全审计**:所有操作可追溯 + +--- + +## 二、认证与授权 + +### 2.1 认证机制 + +#### 2.1.1 JWT Token 认证 + +**Token 生成**: +```java +@Component +public class JwtTokenProvider { + + @Value("${jwt.secret}") + private String secretKey; + + @Value("${jwt.expiration}") + private long expiration; + + public String generateToken(Authentication auth) { + UserPrincipal principal = (UserPrincipal) auth.getPrincipal(); + + Date now = new Date(); + Date expiryDate = new Date(now.getTime() + expiration); + + return Jwts.builder() + .setSubject(principal.getId().toString()) + .claim("tenantId", principal.getTenantId()) + .claim("storeId", principal.getStoreId()) + .claim("roles", principal.getRoles()) + .setIssuedAt(now) + .setExpiration(expiryDate) + .signWith(SignatureAlgorithm.HS512, secretKey) + .compact(); + } +} +``` + +**Token 验证**: +```java +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + @Autowired + private JwtTokenProvider tokenProvider; + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) + throws ServletException, IOException { + try { + String jwt = getJwtFromRequest(request); + + if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) { + Authentication auth = tokenProvider.getAuthentication(jwt); + SecurityContextHolder.getContext().setAuthentication(auth); + } + } catch (Exception ex) { + logger.error("Could not set user authentication", ex); + } + + filterChain.doFilter(request, response); + } +} +``` + +#### 2.1.2 Token 刷新机制 + +**双 Token 机制**: +- Access Token:有效期 2 小时 +- Refresh Token:有效期 7 天 + +**刷新流程**: +```java +@PostMapping("/refresh") +public Mono> refreshToken( + @RequestBody RefreshTokenRequest request) { + return authService.refreshToken(request.getRefreshToken()) + .map(tokens -> ApiResponse.success(tokens)); +} +``` + +### 2.2 授权机制 + +#### 2.2.1 基于角色的访问控制 (RBAC) + +**角色定义**: +```java +public enum Role { + SUPER_ADMIN, // 超级管理员 + TENANT_ADMIN, // 租户管理员 + STORE_MANAGER, // 店长 + COACH, // 教练 + MEMBER // 会员 +} +``` + +**权限配置**: +```java +@Configuration +@EnableWebFluxSecurity +public class SecurityConfig { + + @Bean + public SecurityWebFilterFilterChain securityFilterChain( + ServerHttpSecurity http) { + http + .authorizeExchange(exchanges -> exchanges + .pathMatchers("/api/v1/auth/**").permitAll() + .pathMatchers("/api/v1/admin/**").hasRole("ADMIN") + .pathMatchers("/api/v1/members/**").hasAnyRole("ADMIN", "COACH") + .pathMatchers("/api/v1/my/**").authenticated() + .anyExchange().permitAll() + ) + .oauth2ResourceServer(oauth2 -> oauth2.jwt()); + + return http.build(); + } +} +``` + +#### 2.2.2 数据权限隔离 + +**租户隔离**: +```java +@Component +public class TenantInterceptor implements HandlerInterceptor { + + @Override + public boolean preHandle(HttpServletRequest request, + HttpServletResponse response, + Object handler) { + String tenantId = request.getHeader("X-Tenant-ID"); + if (StringUtils.isEmpty(tenantId)) { + throw new UnauthorizedException("缺少租户标识"); + } + TenantContext.setTenantId(tenantId); + return true; + } + + @Override + public void afterCompletion(HttpServletRequest request, + HttpServletResponse response, + Object handler, + Exception ex) { + TenantContext.clear(); + } +} +``` + +--- + +## 三、数据安全 + +### 3.1 数据加密 + +#### 3.1.1 敏感数据加密存储 + +**加密算法**:AES-256-GCM + +**加密工具类**: +```java +@Component +public class EncryptionUtil { + + @Value("${encryption.key}") + private String encryptionKey; + + public String encrypt(String plaintext) throws Exception { + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + SecretKeySpec keySpec = new SecretKeySpec( + encryptionKey.getBytes(), "AES"); + GCMParameterSpec gcmParameterSpec = new GCMParameterSpec( + 128, generateIV()); + + cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmParameterSpec); + byte[] cipherText = cipher.doFinal(plaintext.getBytes()); + + return Base64.getEncoder().encodeToString(cipherText); + } + + public String decrypt(String cipherText) throws Exception { + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + SecretKeySpec keySpec = new SecretKeySpec( + encryptionKey.getBytes(), "AES"); + + cipher.init(Cipher.DECRYPT_MODE, keySpec, + new GCMParameterSpec(128, generateIV())); + byte[] plainText = cipher.doFinal( + Base64.getDecoder().decode(cipherText)); + + return new String(plainText); + } +} +``` + +**加密字段**: +- 手机号 +- 身份证号 +- 银行卡号 +- 地址 + +#### 3.1.2 密码加密 + +**BCrypt 加密**: +```java +@Bean +public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(12); +} +``` + +### 3.2 数据脱敏 + +#### 3.2.1 脱敏规则 + +**手机号脱敏**: +```java +public class DesensitizationUtil { + + public static String maskPhone(String phone) { + if (StringUtils.isEmpty(phone)) { + return ""; + } + return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"); + } + + public static String maskIdCard(String idCard) { + if (StringUtils.isEmpty(idCard)) { + return ""; + } + return idCard.replaceAll("(\\d{6})\\d{8}(\\d{4})", "$1********$2"); + } +} +``` + +#### 3.2.2 JSON 序列化脱敏 + +**自定义注解**: +```java +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotationsInside +@JsonSerialize(using = DesensitizationSerializer.class) +public @interface Desensitization { + DesensitizationType type(); +} + +public enum DesensitizationType { + PHONE, // 手机号 + ID_CARD, // 身份证 + BANK_CARD, // 银行卡 + ADDRESS // 地址 +} +``` + +**使用示例**: +```java +public class MemberDTO { + + @Desensitization(type = DesensitizationType.PHONE) + private String phone; + + @Desensitization(type = DesensitizationType.ID_CARD) + private String idCard; +} +``` + +### 3.3 数据备份 + +#### 3.3.1 备份策略 + +**全量备份**: +- 频率:每天凌晨 2 点 +- 保留:最近 30 天 +- 存储:异地灾备中心 + +**增量备份**: +- 频率:每小时 +- 保留:最近 7 天 +- 存储:本地高速存储 + +#### 3.3.2 恢复演练 + +- 频率:每季度一次 +- 范围:随机抽取 10% 数据 +- 验证:数据完整性校验 + +--- + +## 四、网络安全 + +### 4.1 HTTPS 强制 + +**配置**: +```yaml +server: + ssl: + enabled: true + key-store: classpath:keystore.p12 + key-store-password: ${SSL_KEY_PASSWORD} + key-store-type: PKCS12 +``` + +**HTTP 重定向**: +```java +@Bean +public SecurityWebFilterFilterChain securityFilterChain( + ServerHttpSecurity http) { + http + .redirectHttpsRedirect(Customizer.withDefaults()); + return http.build(); +} +``` + +### 4.2 CORS 配置 + +**跨域配置**: +```java +@Bean +public WebMvcConfigurer corsConfigurer() { + return new WebMvcConfigurer() { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/api/**") + .allowedOrigins("https://yourdomain.com") + .allowedMethods("GET", "POST", "PUT", "DELETE") + .allowedHeaders("*") + .allowCredentials(true) + .maxAge(3600); + } + }; +} +``` + +### 4.3 限流与防 DDOS + +**限流配置**: +```yaml +resilience4j: + ratelimiter: + instances: + apiRateLimiter: + limit-for-period: 100 + limit-refresh-period: 1s + timeout-duration: 0 + + loginRateLimiter: + limit-for-period: 5 + limit-refresh-period: 1m + timeout-duration: 0 +``` + +**IP 黑名单**: +```java +@Component +public class IpBlacklistFilter implements Filter { + + @Autowired + private RedisTemplate redisTemplate; + + @Override + public void doFilter(ServletRequest request, + ServletResponse response, + FilterChain chain) { + String ip = getClientIp(request); + + if (isBlacklisted(ip)) { + ((HttpServletResponse) response).sendError(403); + return; + } + + chain.doFilter(request, response); + } +} +``` + +--- + +## 五、输入验证与输出编码 + +### 5.1 输入验证 + +**请求体验证**: +```java +public class CreateMemberRequest { + + @NotBlank(message = "手机号不能为空") + @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") + private String phone; + + @NotBlank(message = "姓名不能为空") + @Size(min = 1, max = 50, message = "姓名长度不能超过 50 个字符") + private String name; + + @Email(message = "邮箱格式不正确") + private String email; + + @Min(value = 0, message = "年龄不能小于 0") + @Max(value = 150, message = "年龄不能大于 150") + private Integer age; +} +``` + +**SQL 注入防护**: +```java +// ❌ 错误示例 +@Query("SELECT m FROM Member m WHERE m.phone = :phone") +Member findByPhone(@Param("phone") String phone); + +// ✅ 正确示例(使用参数化查询) +@Query("SELECT m FROM Member m WHERE m.phone = :phone") +Member findByPhone(@Param("phone") String phone); +``` + +**XSS 防护**: +```java +@Component +public class XssFilter implements Filter { + + @Override + public void doFilter(ServletRequest request, + ServletResponse response, + FilterChain chain) + throws IOException, ServletException { + XssHttpServletRequestWrapper xssRequest = + new XssHttpServletRequestWrapper( + (HttpServletRequest) request); + chain.doFilter(xssRequest, response); + } +} +``` + +### 5.2 输出编码 + +**HTML 编码**: +```java +public class HtmlUtil { + + public static String escapeHtml(String html) { + return StringEscapeUtils.escapeHtml4(html); + } +} +``` + +--- + +## 六、安全审计 + +### 6.1 审计日志 + +**审计内容**: +- 登录/登出 +- 创建/更新/删除操作 +- 数据导出 +- 权限变更 + +**日志格式**: +```json +{ + "timestamp": "2026-03-08T10:30:00Z", + "userId": 1, + "action": "CREATE_MEMBER", + "resource": "member", + "resourceId": 123, + "ip": "192.168.1.100", + "userAgent": "Mozilla/5.0...", + "result": "SUCCESS", + "details": {...} +} +``` + +**审计注解**: +```java +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface AuditLog { + String action(); + String resource(); +} +``` + +**使用示例**: +```java +@AuditLog(action = "CREATE", resource = "member") +public Mono createMember(CreateMemberRequest request) { + return memberRepository.save(request.toEntity()); +} +``` + +### 6.2 日志存储 + +**存储策略**: +- 热存储:最近 30 天,Elasticsearch +- 冷存储:30-180 天,对象存储 +- 归档:180 天以上,磁带库 + +**日志保护**: +- 完整性:数字签名 +- 机密性:加密存储 +- 可用性:多副本备份 + +--- + +## 七、安全监控 + +### 7.1 监控指标 + +**认证监控**: +- 登录成功率 +- 登录失败次数 +- Token 刷新率 +- 异常登录行为 + +**授权监控**: +- 权限拒绝次数 +- 越权访问尝试 +- 敏感操作频率 + +**数据监控**: +- 敏感数据访问 +- 大批量数据导出 +- 异常数据修改 + +### 7.2 告警规则 + +**告警级别**: +- P0(紧急):系统被入侵、数据泄露 +- P1(严重):大规模认证失败、DDOS 攻击 +- P2(警告):异常登录行为、权限异常 +- P3(提示):配置变更、版本升级 + +**告警渠道**: +- 短信:P0、P1 +- 邮件:P1、P2 +- 钉钉/企业微信:P2、P3 + +--- + +## 八、合规性 + +### 8.1 GDPR 合规 + +**数据主体权利**: +- 知情权:明确告知数据收集目的 +- 访问权:用户可查询个人数据 +- 更正权:用户可修改个人数据 +- 删除权:用户可申请删除数据 +- 可携带权:支持数据导出 + +**数据保护措施**: +- 数据最小化:只收集必要数据 +- 目的限制:仅用于声明的目的 +- 存储限制:到期自动删除 +- 安全保障:加密、访问控制 + +### 8.2 等保 2.0 合规 + +**技术要求**: +- 身份鉴别:多因素认证 +- 访问控制:最小权限原则 +- 安全审计:操作可追溯 +- 入侵防范:实时监测告警 +- 数据完整性:校验和验证 +- 数据保密性:加密传输存储 + +**管理要求**: +- 安全管理制度 +- 安全管理机构 +- 人员安全管理 +- 系统建设管理 +- 系统运维管理 + +--- + +**文档结束**