docs: 创建安全设计文档

This commit is contained in:
张翔
2026-03-08 21:47:09 +08:00
parent 7c1c9e5fe5
commit 383aaacf5b
+626
View File
@@ -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<ApiResponse<TokenResponse>> 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<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 输入验证
**请求体验证**
```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<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 合规
**技术要求**
- 身份鉴别:多因素认证
- 访问控制:最小权限原则
- 安全审计:操作可追溯
- 入侵防范:实时监测告警
- 数据完整性:校验和验证
- 数据保密性:加密传输存储
**管理要求**
- 安全管理制度
- 安全管理机构
- 人员安全管理
- 系统建设管理
- 系统运维管理
---
**文档结束**