数据安全
介绍
RuoYi-Plus 提供完整的数据安全解决方案,涵盖数据加密存储、敏感数据脱敏、数据权限控制、SQL注入防护、XSS攻击防护等多层次安全机制。
核心特性:
- 多算法数据加密 - 支持 AES、RSA、SM2、SM4 等加密算法
- 字段级加密 -
@EncryptField注解实现自动加解密 - 敏感数据脱敏 - 15 种内置脱敏策略,支持动态脱敏
- 数据权限控制 - 6 种数据权限类型,精细化访问控制
- SQL注入防护 - 关键字过滤和参数校验
- XSS攻击防护 - 多模式 XSS 检测
数据加密
加密算法
java
public enum AlgorithmType {
BASE64, // 编码转换,非加密
AES, // 对称加密,速度快,密钥16/24/32字节
RSA, // 非对称加密,安全性高
SM2, // 国密非对称,政务金融场景
SM4 // 国密对称,密钥16字节
}加密配置
yaml
mybatis-encryptor:
enable: true
algorithm: AES
password: "abcdefghijklmnop" # 16位密钥
public-key: "MIIBIjANBgkqhkiG9..."
private-key: "MIIEvQIBADANBg..."
encode: BASE64字段加密
java
public class SysUser {
@EncryptField
private String phonenumber;
@EncryptField(algorithm = AlgorithmType.AES)
private String idCard;
@EncryptField(algorithm = AlgorithmType.SM4)
private String bankCard;
@EncryptField(
algorithm = AlgorithmType.RSA,
publicKey = "MIIBIjAN...",
privateKey = "MIIEvQI..."
)
private String email;
}加密工具类
java
// AES加密
String encrypted = EncryptUtils.encryptByAes(plainText, password);
String decrypted = EncryptUtils.decryptByAes(encrypted, password);
// RSA加密
Map<String, String> rsaKeys = EncryptUtils.generateRsaKey();
String rsaEncrypted = EncryptUtils.encryptByRsa(plainText, publicKey);
String rsaDecrypted = EncryptUtils.decryptByRsa(rsaEncrypted, privateKey);
// SM2国密
Map<String, String> sm2Keys = EncryptUtils.generateSm2Key();
String sm2Encrypted = EncryptUtils.encryptBySm2(plainText, sm2PublicKey);
String sm2Decrypted = EncryptUtils.decryptBySm2(sm2Encrypted, sm2PrivateKey);
// SM4国密
String sm4Encrypted = EncryptUtils.encryptBySm4(plainText, sm4Password);
String sm4Decrypted = EncryptUtils.decryptBySm4(sm4Encrypted, sm4Password);
// 哈希算法
String md5Hash = EncryptUtils.encryptByMd5(plainText);
String sha256Hash = EncryptUtils.encryptBySha256(plainText);
String sm3Hash = EncryptUtils.encryptBySm3(plainText);API传输加密
java
@ApiEncrypt
@PostMapping("/login")
public R<LoginVo> login(@RequestBody LoginBody loginBody) {
// 请求体自动解密,响应体自动加密
return R.ok(loginService.login(loginBody));
}
@ApiEncrypt(request = false, response = true)
@GetMapping("/info")
public R<UserInfoVo> getUserInfo() {
// 仅响应加密
return R.ok(userService.getCurrentUserInfo());
}敏感数据脱敏
脱敏策略
java
public enum SensitiveStrategy {
ID_CARD(s -> DesensitizedUtil.idCardNum(s, 3, 4)), // 110***********1234
PHONE(DesensitizedUtil::mobilePhone), // 138****5678
ADDRESS(s -> DesensitizedUtil.address(s, 8)), // 北京市朝阳区***
EMAIL(DesensitizedUtil::email), // t**t@example.com
BANK_CARD(DesensitizedUtil::bankCard), // 6222***********0123
CHINESE_NAME(DesensitizedUtil::chineseName), // 张**
FIXED_PHONE(DesensitizedUtil::fixedPhone), // 010-****5678
USER_ID(s -> String.valueOf(DesensitizedUtil.userId())), // 随机数字
PASSWORD(DesensitizedUtil::password), // ***********
IPV4(DesensitizedUtil::ipv4), // 192.168.*.*
IPV6(DesensitizedUtil::ipv6), // 2001:0db8:85a3:*:*:*:*:*
CAR_LICENSE(DesensitizedUtil::carLicense), // 京A1***5
FIRST_MASK(DesensitizedUtil::firstMask), // T*******
CLEAR(s -> ""), // 空字符串
CLEAR_TO_NULL(s -> null); // null
}使用脱敏注解
java
public class UserInfoVo {
@Sensitive(strategy = SensitiveStrategy.PHONE)
private String phonenumber;
// admin/manager角色可查看原数据
@Sensitive(
strategy = SensitiveStrategy.ID_CARD,
roleKey = {"admin", "manager"}
)
private String idCard;
// 有指定权限可查看原数据
@Sensitive(
strategy = SensitiveStrategy.EMAIL,
perms = {"user:detail", "user:export"}
)
private String email;
// 角色或权限满足其一即可查看
@Sensitive(
strategy = SensitiveStrategy.BANK_CARD,
roleKey = {"finance"},
perms = {"user:finance"}
)
private String bankCard;
@Sensitive(strategy = SensitiveStrategy.ADDRESS)
private String address;
@Sensitive(strategy = SensitiveStrategy.CHINESE_NAME)
private String realName;
}脱敏权限判断
java
@Service
public class SensitiveServiceImpl implements SensitiveService {
@Override
public boolean isSensitive(String[] roleKey, String[] perms) {
LoginUser loginUser = LoginHelper.getLoginUser();
if (loginUser == null) return true;
if (LoginHelper.isSuperAdmin()) return false;
// 检查角色(OR关系)
if (ArrayUtil.isNotEmpty(roleKey)) {
Set<String> userRoles = loginUser.getRolePermission();
for (String role : roleKey) {
if (userRoles.contains(role)) return false;
}
}
// 检查权限(OR关系)
if (ArrayUtil.isNotEmpty(perms)) {
Set<String> userPerms = loginUser.getMenuPermission();
for (String perm : perms) {
if (userPerms.contains(perm)) return false;
}
}
return true;
}
}数据权限控制
权限类型
java
public enum DataScopeType {
ALL("1", "", ""), // 全部数据
CUSTOM("2", // 自定义部门列表
" #{#deptName} IN ( #{@sdss.getRoleCustom( #user.roleId )} ) ",
" 1 = 0 "),
DEPT("3", // 仅本部门
" #{#deptName} = #{#user.deptId} ",
" 1 = 0 "),
DEPT_AND_CHILD("4", // 本部门及以下
" #{#deptName} IN ( #{@sdss.getDeptAndChild( #user.deptId )} ) ",
" 1 = 0 "),
SELF("5", // 仅本人
" #{#userName} = #{#user.userId} ",
" 1 = 0 "),
DEPT_AND_CHILD_OR_SELF("6", // 本部门及以下或本人
" #{#deptName} IN ( #{@sdss.getDeptAndChild( #user.deptId )} ) " +
" OR #{#userName} = #{#user.userId} ",
" 1 = 0 ");
}使用数据权限
java
@Mapper
public interface SysUserMapper extends BaseMapper<SysUser> {
@DataPermission({
@DataColumn(key = "deptName", value = "d.dept_id"),
@DataColumn(key = "userName", value = "u.user_id")
})
List<SysUserVo> selectUserList(@Param("user") SysUserBo user);
@DataPermission({
@DataColumn(key = "deptName", value = "dept_id")
})
List<SysUser> selectDeptUserList(@Param("deptId") Long deptId);
// 禁用数据权限
@DataPermission({})
SysUser selectUserById(@Param("userId") Long userId);
}数据范围服务
java
@Service("sdss")
public class SysDataScopeServiceImpl implements ISysDataScopeService {
@Cacheable(cacheNames = CacheNames.SYS_ROLE_CUSTOM, key = "#roleId")
public String getRoleCustom(Long roleId) {
List<Long> deptIds = roleDeptService.selectDeptIdsByRoleId(roleId);
if (CollUtil.isEmpty(deptIds)) return "-1";
return CollUtil.join(deptIds, ",");
}
@Cacheable(cacheNames = CacheNames.SYS_DEPT_AND_CHILD, key = "#deptId")
public String getDeptAndChild(Long deptId) {
List<Long> deptIds = deptService.selectDeptIdsByParentId(deptId);
if (CollUtil.isEmpty(deptIds)) return "-1";
deptIds.add(0, deptId);
return CollUtil.join(deptIds, ",");
}
}SQL注入防护
SQL过滤工具
java
public class SqlUtil {
private static final String SQL_REGEX =
"and|extractvalue|updatexml|sleep|exec|insert|select|delete|update|" +
"drop|count|chr|mid|master|truncate|char|declare|or|union|like|\\+|/\\*|user\\(\\)";
private static final Pattern SQL_PATTERN = Pattern.compile("^[a-zA-Z0-9_\\ \\,\\.]+$");
// ORDER BY安全校验
public static String escapeOrderBySql(String value) {
if (StrUtil.isBlank(value)) return value;
if (!isValidOrderBySql(value)) {
throw new IllegalArgumentException("ORDER BY子句包含非法字符");
}
return value;
}
public static boolean isValidOrderBySql(String value) {
return SQL_PATTERN.matcher(value).matches();
}
// SQL关键字过滤
public static void filterKeyword(String value) {
if (StrUtil.isBlank(value)) return;
String lowerValue = value.toLowerCase();
String[] keywords = SQL_REGEX.split("\\|");
for (String keyword : keywords) {
if (lowerValue.contains(keyword)) {
throw new IllegalArgumentException("检测到SQL注入风险");
}
}
}
}使用参数化查询
xml
<!-- 安全: 使用#{}占位符 -->
<select id="selectUserByName" resultType="SysUser">
SELECT * FROM sys_user WHERE user_name = #{userName}
</select>
<!-- 危险: ${}直接拼接,必须做白名单校验 -->
<select id="selectByTable" resultType="Map">
SELECT * FROM ${tableName} WHERE id = #{id}
</select>XSS攻击防护
XSS检测模式
java
public @interface Xss {
String message() default "参数包含非法字符";
Mode mode() default Mode.BASIC;
enum Mode {
BASIC, // 基础模式:script标签、事件处理器、伪协议、危险标签
STRICT, // 严格模式:增加编码绕过、函数调用、data协议检测
LENIENT // 宽松模式:仅检测script、iframe/object/embed
}
}使用XSS注解
java
public class RegisterBody {
@Xss(message = "用户名不能包含脚本代码")
@NotBlank(message = "用户名不能为空")
private String userName;
// 严格模式
@Xss(mode = Xss.Mode.STRICT, message = "内容存在安全风险")
private String content;
// 宽松模式(富文本)
@Xss(mode = Xss.Mode.LENIENT, message = "内容包含危险脚本")
private String richText;
}安全检查清单
数据存储
- [ ] 敏感数据(身份证、银行卡)加密存储
- [ ] 密码使用 BCrypt 不可逆加密
- [ ] 加密密钥未硬编码
- [ ] 密钥有定期轮换机制
数据传输
- [ ] 使用 HTTPS 协议
- [ ] 敏感 API 使用
@ApiEncrypt - [ ] 响应中敏感数据已脱敏
数据访问
- [ ] 实现数据权限控制
- [ ] 敏感操作有审计日志
- [ ] 异常信息不暴露敏感数据
输入验证
- [ ] 用户输入进行 XSS 检测
- [ ] 动态 SQL 参数进行注入检查
- [ ] 使用参数化查询
常见问题
1. 加密后数据长度变化
解决方案:
- AES 加密后约 1.33 倍
- RSA 2048 输出 256 字节
- 数据库字段长度预留 1.5-2 倍
2. 加密字段无法模糊查询
解决方案:
java
// 方案1: 存储哈希索引用于精确查询
@EncryptField
private String phonenumber;
private String phonenumberHash; // MD5哈希
// 方案2: 存储部分明文用于模糊查询
private String phonenumberPrefix; // 前3位
private String phonenumberSuffix; // 后4位3. 脱敏数据导出问题
解决方案:
java
// 使用独立的导出VO,不带@Sensitive注解
public static class UserExportVo {
private String userName;
private String phonenumber; // 无脱敏注解
}
@PreAuthorize("hasPermission('user:export')")
public void exportUsers(HttpServletResponse response) {
List<UserExportVo> exportList = userMapper.selectExportList();
ExcelUtil.exportExcel(exportList, "用户数据", UserExportVo.class, response);
}4. 数据权限影响统计查询
解决方案:
java
// 使用空的@DataPermission跳过数据权限
@DataPermission({})
@Select("SELECT COUNT(*) FROM sys_user WHERE del_flag = '0'")
long countAllUsers();