Files
gym-manage/docs/design/technical/SEC-安全设计.md
T
2026-03-08 21:47:09 +08:00

14 KiB
Raw Blame History

健身房管理系统安全设计文档

文档编号: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 生成

@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 验证

@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 天

刷新流程

@PostMapping("/refresh")
public Mono<ApiResponse<TokenResponse>> refreshToken(
        @RequestBody RefreshTokenRequest request) {
    return authService.refreshToken(request.getRefreshToken())
        .map(tokens -> ApiResponse.success(tokens));
}

2.2 授权机制

2.2.1 基于角色的访问控制 (RBAC)

角色定义

public enum Role {
    SUPER_ADMIN,      // 超级管理员
    TENANT_ADMIN,     // 租户管理员
    STORE_MANAGER,    // 店长
    COACH,           // 教练
    MEMBER           // 会员
}

权限配置

@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 数据权限隔离

租户隔离

@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

加密工具类

@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 加密

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(12);
}

3.2 数据脱敏

3.2.1 脱敏规则

手机号脱敏

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 序列化脱敏

自定义注解

@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     // 地址
}

使用示例

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 强制

配置

server:
  ssl:
    enabled: true
    key-store: classpath:keystore.p12
    key-store-password: ${SSL_KEY_PASSWORD}
    key-store-type: PKCS12

HTTP 重定向

@Bean
public SecurityWebFilterFilterChain securityFilterChain(
        ServerHttpSecurity http) {
    http
        .redirectHttpsRedirect(Customizer.withDefaults());
    return http.build();
}

4.2 CORS 配置

跨域配置

@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

限流配置

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 黑名单

@Component
public class IpBlacklistFilter implements Filter {

    @Autowired
    private RedisTemplate<String, String> 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 输入验证

请求体验证

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 注入防护

// ❌ 错误示例
@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 防护

@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 编码

public class HtmlUtil {

    public static String escapeHtml(String html) {
        return StringEscapeUtils.escapeHtml4(html);
    }
}

六、安全审计

6.1 审计日志

审计内容

  • 登录/登出
  • 创建/更新/删除操作
  • 数据导出
  • 权限变更

日志格式

{
  "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": {...}
}

审计注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuditLog {
    String action();
    String resource();
}

使用示例

@AuditLog(action = "CREATE", resource = "member")
public Mono<Member> 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 合规

技术要求

  • 身份鉴别:多因素认证
  • 访问控制:最小权限原则
  • 安全审计:操作可追溯
  • 入侵防范:实时监测告警
  • 数据完整性:校验和验证
  • 数据保密性:加密传输存储

管理要求

  • 安全管理制度
  • 安全管理机构
  • 人员安全管理
  • 系统建设管理
  • 系统运维管理

文档结束