随着数字化转型的深入,分布式微服务架构已成为企业系统建设的主流选择,而《网络安全等级保护条例》的落地,让等保2.0成为所有非涉密信息系统的强制合规要求。分布式系统节点分散、链路复杂、边界动态变化的特性,与等保2.0“一个中心、三重防护”的纵深防御体系存在天然的适配难点,很多企业在合规落地中出现“表面过审、实际裸奔”的问题,甚至因为合规不达标面临行政处罚。
一、等保2.0核心框架与分布式系统合规痛点
1.1 等保2.0核心技术体系
等保2.0的核心技术体系是“一个中心、三重防护”的纵深防御模型,所有合规要求均围绕该模型展开:
- 安全管理中心:对全系统的安全策略、审计日志、安全事件、风险状况进行统一管控,是整个安全体系的大脑
- 安全通信网络:保障数据在传输过程中的保密性、完整性,防止传输链路中的窃听、篡改、重放攻击
- 安全区域边界:明确系统安全边界,对跨边界的访问进行精准管控,阻断非法入侵与越权访问
- 安全计算环境:保障服务器、应用、数据等计算资源的安全,是等保合规的核心控制点,覆盖70%以上的测评项
1.2 分布式系统的合规核心痛点
相比单体系统,分布式架构在等保合规落地中面临四大核心挑战:
- 安全边界模糊化:微服务多节点、跨集群、跨云部署,传统基于物理边界的防护模式完全失效,横向渗透风险大幅提升
- 身份链路碎片化:用户请求从终端到网关、再到多个微服务、最终到数据库,经过多跳节点,身份传递过程易出现伪造、冒用问题
- 审计日志分散化:每个微服务、中间件、数据库都有独立的日志体系,无法实现全链路操作的可追溯、不可篡改,无法满足审计要求
- 防护规则静态化:容器化、云原生环境下,服务实例动态扩缩容,传统静态的IP、端口防护规则无法适配动态的架构变化
二、分布式等保2.0合规架构核心设计原则
基于等保2.0的标准要求,结合分布式架构的特性,合规架构设计需遵循五大核心原则:
- 纵深防御原则:构建从网络边界到应用、再到数据的多层防护体系,单点防护失效不会导致整个系统被突破
- 最小权限原则:对每个服务账号、用户账号、运维账号,仅授予完成业务所需的最小权限,严格禁止超权限分配
- 全链路可审计原则:所有用户操作、服务调用、系统事件都必须有完整的审计记录,记录可追溯、不可篡改,保存时间不少于6个月
- 内生安全原则:将安全能力嵌入分布式架构的设计阶段,而非业务上线后的补丁式防护,实现安全与业务的深度融合
- 零信任动态防护原则:默认不信任任何内部或外部的访问请求,所有访问都必须经过身份认证、权限校验、加密传输,适配分布式架构的动态变化
三、分布式系统等保2.0合规架构分层落地
3.1 安全通信网络层:全链路传输安全防护
安全通信网络层是等保2.0的第一道防线,核心目标是保障数据在整个传输链路中的保密性与完整性,防止传输过程中的安全风险。
3.1.1 合规核心要求
GB/T 22239-2019对通信网络的核心合规要求包括:
- 通信双方必须经过双向身份鉴别,防止身份伪造
- 传输过程中的重要数据必须采用密码技术保证保密性与完整性
- 必须具备通信会话的抗重放能力,防止重放攻击
- 必须能够检测到通信过程中的异常行为,并进行告警与阻断
3.1.2 架构设计
全链路加密传输架构如下:
3.1.3 落地实现方案
- 外网接入层强制TLS 1.3加密 外网接入必须禁用SSL、TLS 1.0/1.1等不安全协议,强制使用TLS 1.3,配置强加密套件,示例Nginx配置如下:
server {
listen 443 ssl http2;
server_name demo.jam.com;
ssl_protocols TLSv1.3;
ssl_ciphers TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_stapling on;
ssl_stapling_verify on;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}
2. 数据库通信强制加密 MySQL 8.0开启强制SSL连接,配置如下:
SET GLOBAL require_secure_transport = ON;
CREATE USER 'app_user'@'%' IDENTIFIED BY 'Your_Strong_Password_123!' REQUIRE SSL;
GRANT SELECT,INSERT,UPDATE,DELETE ON jam_demo.* TO 'app_user'@'%';
FLUSH PRIVILEGES;
Spring Boot项目中MySQL连接配置如下:
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/jam_demo?useSSL=true&requireSSL=true&verifyServerCertificate=true
username: app_user
password: Your_Strong_Password_123!
driver-class-name: com.mysql.cj.jdbc.Driver
3. 服务间通信mTLS双向认证 微服务间通信采用mTLS双向认证,防止服务间的身份伪造,Spring Boot服务TLS配置示例如下:
server:
ssl:
enabled: true
key-store: classpath:server.p12
key-store-password: ${KEY_STORE_PASSWORD}
key-store-type: PKCS12
key-alias: server
trust-store: classpath:truststore.p12
trust-store-password: ${TRUST_STORE_PASSWORD}
trust-store-type: PKCS12
client-auth: need
4. 通信会话抗重放保护 基于请求签名、时间戳、nonce随机数实现抗重放攻击,请求签名工具类实现如下:
package com.jam.demo.common.utils;
import com.alibaba.fastjson2.JSON;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.encoders.Hex;
import org.springframework.util.StringUtils;
import com.jam.demo.common.exception.BusinessException;
import java.nio.charset.StandardCharsets;
import java.security.Security;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* 请求签名工具类
* @author ken
*/
public class SignUtils {
static {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
private static final long VALID_TIME_WINDOW = 5 * 60 * 1000L;
/**
* 生成请求签名
* @param params 请求参数
* @param timestamp 时间戳
* @param nonce 随机数
* @param appSecret 应用密钥
* @return 签名值
*/
public static String generateSign(SortedMap<String, Object> params, long timestamp, String nonce, String appSecret) {
if (!StringUtils.hasText(nonce)) {
throw new BusinessException("nonce不能为空");
}
if (!StringUtils.hasText(appSecret)) {
throw new BusinessException("appSecret不能为空");
}
SortedMap<String, Object> sortedMap = new TreeMap<>(params);
sortedMap.put("timestamp", timestamp);
sortedMap.put("nonce", nonce);
StringBuilder signContent = new StringBuilder();
for (SortedMap.Entry<String, Object> entry : sortedMap.entrySet()) {
if (!StringUtils.hasText(entry.getKey()) || entry.getValue() == null) {
continue;
}
signContent.append(entry.getKey()).append("=");
if (entry.getValue() instanceof String) {
signContent.append(entry.getValue());
} else {
signContent.append(JSON.toJSONString(entry.getValue()));
}
signContent.append("&");
}
signContent.append("appSecret=").append(appSecret);
return sm3Hash(signContent.toString());
}
/**
* 校验请求签名
* @param params 请求参数
* @param timestamp 时间戳
* @param nonce 随机数
* @param appSecret 应用密钥
* @param sign 待校验的签名
* @return 校验结果
*/
public static boolean verifySign(SortedMap<String, Object> params, long timestamp, String nonce, String appSecret, String sign) {
long currentTime = System.currentTimeMillis();
if (Math.abs(currentTime - timestamp) > VALID_TIME_WINDOW) {
return false;
}
String generateSign = generateSign(params, timestamp, nonce, appSecret);
return generateSign.equalsIgnoreCase(sign);
}
/**
* 国密SM3哈希算法
* @param content 待哈希内容
* @return 哈希结果
*/
private static String sm3Hash(String content) {
try {
org.bouncycastle.crypto.digests.SM3Digest digest = new org.bouncycastle.crypto.digests.SM3Digest();
byte[] bytes = content.getBytes(StandardCharsets.UTF_8);
digest.update(bytes, 0, bytes.length);
byte[] hashResult = new byte[digest.getDigestSize()];
digest.doFinal(hashResult, 0);
return Hex.toHexString(hashResult);
} catch (Exception e) {
throw new BusinessException("SM3哈希计算失败", e);
}
}
}
3.2 安全区域边界层:精准访问控制与入侵防护
安全区域边界层是分布式系统的第二道防线,核心目标是明确系统的安全边界,对所有跨边界的访问进行严格管控,阻断非法入侵与越权访问。
3.2.1 合规核心要求
GB/T 22239-2019对区域边界的核心合规要求包括:
- 必须明确划分安全区域,对跨越边界的数据流进行访问控制
- 必须对进出边界的网络流量进行攻击检测,阻断常见的网络攻击行为
- 必须对无线网络、远程接入、第三方接口接入进行专项安全管控
- 必须对边界的安全事件进行完整审计,实现可追溯
3.2.2 架构设计
分布式系统安全区域划分与边界防护架构如下:
3.2.3 落地实现方案
- 安全区域划分与最小权限ACL规则 按照业务功能、数据敏感级别划分安全区域,每个区域之间采用默认拒绝的ACL规则,仅开放业务必需的端口与通信链路:
- DMZ区域:仅对外开放443端口,仅允许HTTPS流量进入
- API网关区域:仅允许DMZ区域的443流量进入,仅开放网关服务端口
- 业务微服务区域:仅允许API网关区域的流量进入,禁止外网直接访问
- 数据存储区域:仅允许业务微服务区域的对应端口访问,禁止其他区域直接访问
- 运维管理区域:仅通过堡垒机访问其他区域,禁止直接跨区域访问
- API网关边界防护 API网关是分布式系统的核心入口,必须实现全量请求的身份认证、权限校验、攻击检测、流量控制,Spring Cloud Gateway自定义XSS攻击过滤过滤器实现如下:
package com.jam.demo.gateway.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.util.regex.Pattern;
/**
* XSS攻击检测全局过滤器
* @author ken
*/
@Slf4j
@Component
public class XssAttackFilter implements GlobalFilter, Ordered {
private static final Pattern XSS_PATTERN = Pattern.compile(
"<script.*?>.*?</script>|javascript:|onerror=|onclick=|onload=|alert\(|confirm\(|prompt\(",
Pattern.CASE_INSENSITIVE
);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String requestPath = request.getPath().value();
if (isWhiteList(requestPath)) {
return chain.filter(exchange);
}
for (String paramName : request.getQueryParams().keySet()) {
String paramValue = request.getQueryParams().getFirst(paramName);
if (hasXssRisk(paramValue)) {
return blockRequest(exchange, "请求参数包含XSS攻击内容");
}
}
if (request.getHeaders().getContentType() != null && request.getHeaders().getContentType().includes(org.springframework.http.MediaType.APPLICATION_JSON)) {
return DataBufferUtils.join(request.getBody())
.flatMap(dataBuffer -> {
byte[] content = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(content);
DataBufferUtils.release(dataBuffer);
String bodyContent = new String(content, StandardCharsets.UTF_8);
if (hasXssRisk(bodyContent)) {
return blockRequest(exchange, "请求体包含XSS攻击内容");
}
DataBuffer newBuffer = exchange.getResponse().bufferFactory().wrap(content);
ServerHttpRequest newRequest = request.mutate().body(Flux.just(newBuffer)).build();
return chain.filter(exchange.mutate().request(newRequest).build());
});
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 1;
}
private boolean isWhiteList(String path) {
return path.startsWith("/health") || path.startsWith("/actuator");
}
private boolean hasXssRisk(String content) {
if (!StringUtils.hasText(content)) {
return false;
}
return XSS_PATTERN.matcher(content).find();
}
private Mono<Void> blockRequest(ServerWebExchange exchange, String message) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.BAD_REQUEST);
response.getHeaders().setContentType(org.springframework.http.MediaType.APPLICATION_JSON);
String responseBody = "{"code":400,"message":"" + message + ""}";
DataBuffer buffer = response.bufferFactory().wrap(responseBody.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
}
}
3. 微服务间零信任访问控制 摒弃“内网即可信”的传统理念,微服务间调用必须进行身份认证与权限校验,基于Spring Security实现服务间调用的Token校验拦截器如下:
package com.jam.demo.common.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import com.jam.demo.common.utils.JwtUtils;
import com.jam.demo.common.exception.BusinessException;
/**
* 服务间调用身份认证拦截器
* @author ken
*/
@Slf4j
public class ServiceAuthInterceptor implements HandlerInterceptor {
private final JwtUtils jwtUtils;
public ServiceAuthInterceptor(JwtUtils jwtUtils) {
this.jwtUtils = jwtUtils;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String token = request.getHeader("X-Service-Token");
if (!StringUtils.hasText(token)) {
throw new BusinessException("服务调用令牌不能为空");
}
if (!jwtUtils.validateToken(token)) {
throw new BusinessException("服务调用令牌无效");
}
String serviceId = jwtUtils.getServiceIdFromToken(token);
request.setAttribute("callerServiceId", serviceId);
return true;
}
}
3.3 安全计算环境层:核心合规控制点落地
安全计算环境层是等保2.0合规的核心,覆盖70%以上的测评项,核心目标是保障服务器、应用、数据等计算资源的全生命周期安全。
3.3.1 合规核心要求
GB/T 22239-2019对安全计算环境的核心控制点包括:身份鉴别、访问控制、安全审计、入侵防范、数据完整性、数据保密性、数据备份恢复、剩余信息保护、资源控制。
3.3.2 身份鉴别落地实现
身份鉴别是安全计算环境的第一道关卡,等保2.0要求必须实现用户身份的唯一性标识、强密码策略、登录失败处理、多因素认证。
- 数据库表设计
CREATE TABLE sys_user (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户ID',
username VARCHAR(64) NOT NULL COMMENT '用户名',
password VARCHAR(128) NOT NULL COMMENT '密码哈希',
full_name VARCHAR(64) NOT NULL COMMENT '用户姓名',
phone VARCHAR(32) COMMENT '手机号',
email VARCHAR(64) COMMENT '邮箱',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态 0-禁用 1-正常',
login_fail_count INT NOT NULL DEFAULT 0 COMMENT '登录失败次数',
lock_time DATETIME COMMENT '锁定时间',
last_login_time DATETIME COMMENT '最后登录时间',
last_login_ip VARCHAR(64) COMMENT '最后登录IP',
pwd_update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '密码更新时间',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE INDEX uk_username (username)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='系统用户表';
2. 用户登录服务实现
package com.jam.demo.system.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jam.demo.system.entity.SysUser;
import com.jam.demo.system.mapper.SysUserMapper;
import com.jam.demo.system.service.SysUserService;
import com.jam.demo.common.utils.PasswordUtils;
import com.jam.demo.common.utils.JwtUtils;
import com.jam.demo.common.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
/**
* 系统用户服务实现类
* @author ken
*/
@Slf4j
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
private static final int MAX_LOGIN_FAIL_COUNT = 5;
private static final int LOCK_DURATION_MINUTES = 30;
private static final int PWD_VALID_DAYS = 90;
private final TransactionTemplate transactionTemplate;
private final PasswordUtils passwordUtils;
private final JwtUtils jwtUtils;
public SysUserServiceImpl(TransactionTemplate transactionTemplate, PasswordUtils passwordUtils, JwtUtils jwtUtils) {
this.transactionTemplate = transactionTemplate;
this.passwordUtils = passwordUtils;
this.jwtUtils = jwtUtils;
}
/**
* 用户登录
* @param username 用户名
* @param password 密码
* @param ipAddress 登录IP地址
* @return 登录结果,包含token与用户信息
*/
@Override
public Map<String, Object> login(String username, String password, String ipAddress) {
if (!StringUtils.hasText(username)) {
throw new BusinessException("用户名不能为空");
}
if (!StringUtils.hasText(password)) {
throw new BusinessException("密码不能为空");
}
SysUser user = this.getOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, username));
if (ObjectUtils.isEmpty(user)) {
throw new BusinessException("用户名或密码错误");
}
if (user.getStatus() == 0) {
throw new BusinessException("账号已被禁用");
}
if (user.getLockTime() != null && user.getLockTime().isAfter(LocalDateTime.now())) {
throw new BusinessException("账号已被锁定,请" + LOCK_DURATION_MINUTES + "分钟后重试");
}
if (!passwordUtils.matches(password, user.getPassword())) {
handleLoginFail(user);
throw new BusinessException("用户名或密码错误,连续错误" + MAX_LOGIN_FAIL_COUNT + "次将锁定账号");
}
if (user.getPwdUpdateTime().plusDays(PWD_VALID_DAYS).isBefore(LocalDateTime.now())) {
throw new BusinessException("密码已过期,请修改密码后重新登录");
}
resetLoginFailCount(user);
updateLoginInfo(user, ipAddress);
String token = jwtUtils.generateToken(user.getId(), user.getUsername());
Map<String, Object> result = new HashMap<>();
result.put("token", token);
result.put("userId", user.getId());
result.put("username", user.getUsername());
result.put("fullName", user.getFullName());
return result;
}
/**
* 处理登录失败
* @param user 用户实体
*/
private void handleLoginFail(SysUser user) {
transactionTemplate.execute(new TransactionCallback<Void>() {
@Override
public Void doInTransaction(TransactionStatus status) {
int newFailCount = user.getLoginFailCount() + 1;
user.setLoginFailCount(newFailCount);
if (newFailCount >= MAX_LOGIN_FAIL_COUNT) {
user.setLockTime(LocalDateTime.now().plusMinutes(LOCK_DURATION_MINUTES));
}
updateById(user);
return null;
}
});
}
/**
* 重置登录失败次数
* @param user 用户实体
*/
private void resetLoginFailCount(SysUser user) {
if (user.getLoginFailCount() > 0) {
transactionTemplate.execute(new TransactionCallback<Void>() {
@Override
public Void doInTransaction(TransactionStatus status) {
user.setLoginFailCount(0);
user.setLockTime(null);
updateById(user);
return null;
}
});
}
}
/**
* 更新登录信息
* @param user 用户实体
* @param ipAddress 登录IP地址
*/
private void updateLoginInfo(SysUser user, String ipAddress) {
transactionTemplate.execute(new TransactionCallback<Void>() {
@Override
public Void doInTransaction(TransactionStatus status) {
user.setLastLoginTime(LocalDateTime.now());
user.setLastLoginIp(ipAddress);
updateById(user);
return null;
}
});
}
}
3.3.3 访问控制落地实现
等保2.0要求必须实现最小权限的访问控制,采用RBAC+ABAC的访问控制模型,实现功能权限与数据权限的精准管控。
- RBAC模型数据库表设计
CREATE TABLE sys_role (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '角色ID',
role_name VARCHAR(64) NOT NULL COMMENT '角色名称',
role_code VARCHAR(64) NOT NULL COMMENT '角色编码',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态 0-禁用 1-正常',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE INDEX uk_role_code (role_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='系统角色表';
CREATE TABLE sys_permission (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '权限ID',
parent_id BIGINT NOT NULL DEFAULT 0 COMMENT '父权限ID',
permission_name VARCHAR(64) NOT NULL COMMENT '权限名称',
permission_code VARCHAR(128) NOT NULL COMMENT '权限编码',
permission_type TINYINT NOT NULL COMMENT '权限类型 1-菜单 2-按钮 3-接口',
url VARCHAR(256) COMMENT '接口URL',
method VARCHAR(16) COMMENT '请求方法',
sort INT NOT NULL DEFAULT 0 COMMENT '排序',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE INDEX uk_permission_code (permission_code),
INDEX idx_parent_id (parent_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='系统权限表';
CREATE TABLE sys_user_role (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
user_id BIGINT NOT NULL COMMENT '用户ID',
role_id BIGINT NOT NULL COMMENT '角色ID',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (id),
UNIQUE INDEX uk_user_role (user_id, role_id),
INDEX idx_user_id (user_id),
INDEX idx_role_id (role_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户角色关联表';
CREATE TABLE sys_role_permission (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
role_id BIGINT NOT NULL COMMENT '角色ID',
permission_id BIGINT NOT NULL COMMENT '权限ID',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (id),
UNIQUE INDEX uk_role_permission (role_id, permission_id),
INDEX idx_role_id (role_id),
INDEX idx_permission_id (permission_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色权限关联表';
2. 权限校验拦截器实现
package com.jam.demo.common.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import com.jam.demo.common.utils.JwtUtils;
import com.jam.demo.system.service.SysPermissionService;
import com.jam.demo.common.exception.BusinessException;
/**
* 权限校验拦截器
* @author ken
*/
@Slf4j
public class PermissionInterceptor implements HandlerInterceptor {
private final JwtUtils jwtUtils;
private final SysPermissionService sysPermissionService;
public PermissionInterceptor(JwtUtils jwtUtils, SysPermissionService sysPermissionService) {
this.jwtUtils = jwtUtils;
this.sysPermissionService = sysPermissionService;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String token = request.getHeader("Authorization");
if (!StringUtils.hasText(token) || !token.startsWith("Bearer ")) {
throw new BusinessException("用户未登录");
}
token = token.substring(7);
if (!jwtUtils.validateToken(token)) {
throw new BusinessException("登录令牌无效");
}
Long userId = jwtUtils.getUserIdFromToken(token);
request.setAttribute("userId", userId);
String requestUrl = request.getRequestURI();
String requestMethod = request.getMethod();
boolean hasPermission = sysPermissionService.hasPermission(userId, requestUrl, requestMethod);
if (!hasPermission) {
throw new BusinessException("无操作权限");
}
return true;
}
}
3.3.4 安全审计落地实现
等保2.0要求必须实现全量操作的安全审计,审计记录必须可追溯、不可篡改,保存时间不少于6个月。
- 审计日志表设计
CREATE TABLE sys_audit_log (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
trace_id VARCHAR(64) NOT NULL COMMENT '全链路追踪ID',
operator_id BIGINT COMMENT '操作人ID',
operator_name VARCHAR(64) COMMENT '操作人姓名',
operation_type VARCHAR(32) NOT NULL COMMENT '操作类型',
operation_desc VARCHAR(256) NOT NULL COMMENT '操作描述',
request_url VARCHAR(512) NOT NULL COMMENT '请求URL',
request_method VARCHAR(16) NOT NULL COMMENT '请求方法',
request_params TEXT COMMENT '请求参数',
response_result TEXT COMMENT '响应结果',
ip_address VARCHAR(64) NOT NULL COMMENT '操作IP地址',
operation_status TINYINT NOT NULL COMMENT '操作状态 0-失败 1-成功',
error_message TEXT COMMENT '错误信息',
operation_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
PRIMARY KEY (id),
INDEX idx_trace_id (trace_id),
INDEX idx_operator_id (operator_id),
INDEX idx_operation_time (operation_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='系统审计日志表';
CREATE USER 'audit_user'@'%' IDENTIFIED BY 'Audit_Strong_Password_123!' REQUIRE SSL;
GRANT INSERT ON jam_demo.sys_audit_log TO 'audit_user'@'%';
FLUSH PRIVILEGES;
2. 审计日志AOP切面实现
package com.jam.demo.common.aspect;
import com.alibaba.fastjson2.JSON;
import com.jam.demo.common.annotation.AuditLog;
import com.jam.demo.common.utils.TraceIdUtils;
import com.jam.demo.system.entity.SysAuditLog;
import com.jam.demo.system.service.SysAuditLogService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import java.util.Arrays;
/**
* 审计日志切面
* @author ken
*/
@Slf4j
@Aspect
@Component
public class AuditLogAspect {
private final SysAuditLogService sysAuditLogService;
public AuditLogAspect(SysAuditLogService sysAuditLogService) {
this.sysAuditLogService = sysAuditLogService;
}
@Around("@annotation(auditLog)")
public Object around(ProceedingJoinPoint joinPoint, AuditLog auditLog) throws Throwable {
SysAuditLog auditLogEntity = new SysAuditLog();
auditLogEntity.setTraceId(TraceIdUtils.getTraceId());
auditLogEntity.setOperationType(auditLog.operationType());
auditLogEntity.setOperationDesc(auditLog.operationDesc());
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (!ObjectUtils.isEmpty(attributes)) {
HttpServletRequest request = attributes.getRequest();
auditLogEntity.setRequestUrl(request.getRequestURI());
auditLogEntity.setRequestMethod(request.getMethod());
auditLogEntity.setIpAddress(getIpAddress(request));
Long userId = (Long) request.getAttribute("userId");
auditLogEntity.setOperatorId(userId);
String operatorName = (String) request.getAttribute("username");
auditLogEntity.setOperatorName(operatorName);
}
auditLogEntity.setRequestParams(Arrays.toString(joinPoint.getArgs()));
long startTime = System.currentTimeMillis();
Object result = null;
try {
result = joinPoint.proceed();
auditLogEntity.setOperationStatus(1);
if (!ObjectUtils.isEmpty(result)) {
auditLogEntity.setResponseResult(JSON.toJSONString(result));
}
} catch (Throwable e) {
auditLogEntity.setOperationStatus(0);
auditLogEntity.setErrorMessage(e.getMessage());
throw e;
} finally {
long endTime = System.currentTimeMillis();
log.info("操作:{}, 耗时:{}ms", auditLog.operationDesc(), (endTime - startTime));
sysAuditLogService.saveAuditLog(auditLogEntity);
}
return result;
}
private String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (StringUtils.hasText(ip) && !"unknown".equalsIgnoreCase(ip)) {
int index = ip.indexOf(',');
if (index != -1) {
return ip.substring(0, index).trim();
}
return ip.trim();
}
ip = request.getHeader("X-Real-IP");
if (StringUtils.hasText(ip) && !"unknown".equalsIgnoreCase(ip)) {
return ip.trim();
}
return request.getRemoteAddr();
}
}
3.3.5 数据保密性落地实现
等保2.0要求必须对敏感数据进行存储加密,采用国密SM4算法实现敏感字段的自动加解密,MyBatisPlus类型处理器实现如下:
- 国密SM4加密工具类
package com.jam.demo.common.utils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.encoders.Hex;
import org.springframework.util.StringUtils;
import com.jam.demo.common.exception.BusinessException;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.Security;
/**
* 国密SM4加密工具类
* @author ken
*/
public class Sm4Utils {
static {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
private static final String ALGORITHM_NAME = "SM4";
private static final String ALGORITHM_NAME_CBC_PADDING = "SM4/CBC/PKCS7Padding";
private static final int KEY_SIZE = 128;
private static final byte[] DEFAULT_IV = new byte[16];
private static final String SM4_KEY = "${SM4_ENCRYPT_KEY}";
/**
* SM4 CBC加密
* @param plainText 明文
* @return 加密后的十六进制字符串
*/
public static String encrypt(String plainText) {
if (!StringUtils.hasText(plainText)) {
return plainText;
}
try {
Cipher cipher = Cipher.getInstance(ALGORITHM_NAME_CBC_PADDING, BouncyCastleProvider.PROVIDER_NAME);
SecretKeySpec secretKeySpec = new SecretKeySpec(Hex.decode(SM4_KEY), ALGORITHM_NAME);
IvParameterSpec ivParameterSpec = new IvParameterSpec(DEFAULT_IV);
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
return Hex.toHexString(encrypted);
} catch (Exception e) {
throw new BusinessException("SM4加密失败", e);
}
}
/**
* SM4 CBC解密
* @param cipherText 加密后的十六进制字符串
* @return 解密后的明文
*/
public static String decrypt(String cipherText) {
if (!StringUtils.hasText(cipherText)) {
return cipherText;
}
try {
Cipher cipher = Cipher.getInstance(ALGORITHM_NAME_CBC_PADDING, BouncyCastleProvider.PROVIDER_NAME);
SecretKeySpec secretKeySpec = new SecretKeySpec(Hex.decode(SM4_KEY), ALGORITHM_NAME);
IvParameterSpec ivParameterSpec = new IvParameterSpec(DEFAULT_IV);
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
byte[] decrypted = cipher.doFinal(Hex.decode(cipherText));
return new String(decrypted, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new BusinessException("SM4解密失败", e);
}
}
/**
* 生成SM4密钥
* @return 十六进制格式的密钥
*/
public static String generateKey() {
try {
KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM_NAME, BouncyCastleProvider.PROVIDER_NAME);
keyGenerator.init(KEY_SIZE);
SecretKey secretKey = keyGenerator.generateKey();
return Hex.toHexString(secretKey.getEncoded());
} catch (Exception e) {
throw new BusinessException("SM4密钥生成失败", e);
}
}
}
2. MyBatisPlus敏感字段类型处理器
package com.jam.demo.common.handler;
import com.jam.demo.common.utils.Sm4Utils;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.springframework.util.StringUtils;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* 敏感字段SM4加解密类型处理器
* @author ken
*/
@MappedTypes(String.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class SensitiveFieldTypeHandler extends BaseTypeHandler<String> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
String encryptedValue = Sm4Utils.encrypt(parameter);
ps.setString(i, encryptedValue);
}
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
String cipherText = rs.getString(columnName);
return Sm4Utils.decrypt(cipherText);
}
@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String cipherText = rs.getString(columnIndex);
return Sm4Utils.decrypt(cipherText);
}
@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String cipherText = cs.getString(columnIndex);
return Sm4Utils.decrypt(cipherText);
}
}
3.4 安全管理中心:统一安全管控体系
安全管理中心是整个等保2.0合规架构的大脑,核心目标是实现对全系统安全策略、安全事件、审计日志、风险状况的统一管控。
3.4.1 合规核心要求
GB/T 22239-2019对安全管理中心的核心要求包括:
- 必须实现对全系统安全设备与安全策略的统一管理
- 必须实现对全系统安全事件的统一收集、分析与告警
- 必须实现对全系统审计数据的集中管理与关联分析
- 必须实现对全系统安全风险的统一评估与处置
3.4.2 架构设计
安全管理中心架构如下:
3.4.3 落地实现方案
- 安全策略统一管理:建立统一的策略管理平台,实现对防火墙规则、WAF规则、访问控制策略、加密策略的统一配置、下发与生命周期管理,避免策略分散导致的合规漏洞。
- 安全事件统一监控:采用SIEM系统实现全系统安全日志的统一收集、关联分析,对异常登录、越权访问、SQL注入、暴力破解等安全事件进行实时告警,告警流程如下:
- 统一审计分析:建立统一的审计日志中心,实现全链路审计日志的集中存储、查询、分析,审计日志必须采用不可篡改的存储方式,保存时间不少于6个月。
- 风险评估与管控:建立定期的风险评估机制,每季度对系统进行全面的安全风险评估,对发现的高风险漏洞必须在72小时内完成整改,中风险漏洞在15个工作日内完成整改。
四、等保2.0合规验证与常见问题整改
4.1 合规自查核心清单
基于GB/T 22239-2019标准,分布式系统等保2.0合规自查核心清单如下:
- 安全通信网络:是否实现全链路传输加密、双向身份认证、抗重放保护
- 安全区域边界:是否明确划分安全区域、实现最小权限ACL规则、API网关攻击防护、服务间访问控制
- 安全计算环境:是否实现强身份鉴别、最小权限访问控制、全量操作审计、敏感数据加密、数据备份恢复
- 安全管理中心:是否实现安全策略统一管理、安全事件统一监控、审计日志集中管理、风险统一管控
4.2 常见测评不符合项整改方案
- 不符合项:用户密码复杂度不符合要求,未定期更换 整改方案:强制密码复杂度要求(长度不少于8位,包含大小写字母、数字、特殊字符),密码有效期90天,禁止重复使用最近5次的密码
- 不符合项:审计日志保存时间不足6个月 整改方案:建立审计日志集中存储平台,采用不可篡改的存储方式,日志保存时间设置为180天以上
- 不符合项:敏感数据明文存储 整改方案:采用国密SM4算法对身份证号、手机号、银行卡号等敏感数据进行存储加密,实现敏感字段的自动加解密
- 不符合项:未实现通信过程中的双向身份认证 整改方案:外网接入采用TLS 1.3双向认证,服务间通信采用mTLS双向认证,数据库连接强制SSL加密
- 不符合项:访问控制粒度不足,未实现最小权限 整改方案:采用RBAC模型实现功能权限的精准管控,数据权限实现行级管控,数据库账号仅授予业务必需的最小权限
五、项目核心依赖配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version>
<relativePath/>
</parent>
<groupId>com.jam</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>jam-demo</name>
<description>等保2.0合规分布式系统Demo</description>
<properties>
<java.version>17</java.version>
<mybatis-plus.version>3.5.7</mybatis-plus.version>
<fastjson2.version>2.0.52</fastjson2.version>
<bouncycastle.version>1.78.1</bouncycastle.version>
<springdoc.version>2.6.0</springdoc.version>
<guava.version>33.1.0-jre</guava.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>4.1.3</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.40</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
结尾
等保2.0合规不是一次性的测评工作,而是持续的安全建设过程。分布式系统的等保合规落地,必须摒弃“为了过审而合规”的错误理念,基于“一个中心、三重防护”的纵深防御体系,将安全能力嵌入到分布式架构的全生命周期中,构建既符合国家合规要求、又具备实际防护能力的安全架构,真正实现业务与安全的协同发展。