627 lines
14 KiB
Markdown
627 lines
14 KiB
Markdown
# 健身房管理系统安全设计文档
|
||
|
||
> 文档编号: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 合规
|
||
|
||
**技术要求**:
|
||
- 身份鉴别:多因素认证
|
||
- 访问控制:最小权限原则
|
||
- 安全审计:操作可追溯
|
||
- 入侵防范:实时监测告警
|
||
- 数据完整性:校验和验证
|
||
- 数据保密性:加密传输存储
|
||
|
||
**管理要求**:
|
||
- 安全管理制度
|
||
- 安全管理机构
|
||
- 人员安全管理
|
||
- 系统建设管理
|
||
- 系统运维管理
|
||
|
||
---
|
||
|
||
**文档结束**
|