14 KiB
14 KiB
健身房管理系统安全设计文档
文档编号: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 安全原则
- 纵深防御:多层安全防护
- 最小权限:只授予必要权限
- 默认安全:默认配置即安全
- 零信任:始终验证,永不信任
- 安全审计:所有操作可追溯
二、认证与授权
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 合规
技术要求:
- 身份鉴别:多因素认证
- 访问控制:最小权限原则
- 安全审计:操作可追溯
- 入侵防范:实时监测告警
- 数据完整性:校验和验证
- 数据保密性:加密传输存储
管理要求:
- 安全管理制度
- 安全管理机构
- 人员安全管理
- 系统建设管理
- 系统运维管理
文档结束