权限管理软件字段权限配置实战:当普通员工误看客户手机号、HR误查薪资表、外包人员删除财务数据时,字段级权限如何精确控制不同角色对不同字段的查看修改删除权限并防止数据越权访问
那个让技术负责人失眠的周五下午
你有没有遇到过这种场景?周一早上刚来公司,就被客户部老张堵在工位旁边:”小刘啊,我们销售系统的客户手机号怎么全出来了?上周刚入职的小王,连客户电话都能批量导出,这要是客户打过来问,他连客户长啥样都不知道啊!”
技术负责人老李这时候通常只会苦笑——因为这个功能他明明配置过权限,结果上线三个月才发现数据全泄露了。
再比如,财务部门的小王有一天发现她辛辛苦苦做的季度报表被一个外包公司的临时人员给删了,而且删除记录显示的是”系统自动清理”。更诡异的是,HR部门的同事竟然能查询到所有员工的薪资明细,包括他们自己的。
这些问题背后的根本原因,就是字段级权限配置缺失或配置错误。
在传统的权限管理方案里,大多数公司只做到”表级权限”——你要么是销售部员工,要么是HR,能访问哪个数据表一目了然。但问题在于,同一个表里,有的字段是敏感的,有的字段是非敏感的。比如客户表里,客户姓名可能谁都能看,但手机号、邮箱就是敏感信息,薪资表里,基本工资大家可能都能看到,但奖金、提成就是敏感字段。
先说说字段级权限到底是个什么概念
字段级权限,说白了就是在同一个数据表里,针对不同的字段,给不同角色设置不同的操作权限。
举个例子,一个”客户信息表”里可能包含以下字段:
| 字段名 | 说明 | 敏感程度 |
|---|---|---|
| customer_id | 客户ID | 低 |
| customer_name | 客户姓名 | 低 |
| customer_phone | 客户手机号 | 高 |
| customer_email | 客户邮箱 | 中 |
| customer_address | 客户地址 | 中 |
| customer_source | 客户来源 | 低 |
| customer_value | 客户价值评估 | 高 |
| customer_remark | 备注 | 低 |
在这个表里,普通销售可能只需要看姓名和手机号,但看不到客户来源和备注;而销售经理可以看到所有字段;HR系统里可能连手机号都不应该出现在普通员工面前。
这就是字段级权限要解决的问题:同一个表,不同角色看到的字段不同,能做的操作也不同。
字段权限控制的三种核心能力
在实际项目中,字段级权限通常需要支持以下三种核心操作:
查看权限:决定某个角色能看到哪些字段。比如普通销售能看到客户姓名、手机号,但看不到客户来源和备注。
修改权限:决定某个角色能修改哪些字段。比如客服能修改客户地址,但不能修改客户价值评估。
删除权限:决定某个角色能否删除某个字段的值(置空或删除整行数据)。一般只有管理员才有删除权限。
这三种能力组合起来,就构成了完整的字段级权限矩阵。
数据库层面的设计方案
在数据库层面,实现字段级权限通常需要以下几个核心表:
-- 角色表
CREATE TABLE sys_role (
role_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '角色ID',
role_name VARCHAR(50) NOT NULL COMMENT '角色名称',
role_code VARCHAR(50) NOT NULL COMMENT '角色编码',
description VARCHAR(255) COMMENT '角色描述',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_role_code (role_code)
);
-- 权限表(字段级权限)
CREATE TABLE sys_field_permission (
permission_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '权限ID',
role_id INT NOT NULL COMMENT '角色ID',
table_name VARCHAR(100) NOT NULL COMMENT '表名',
field_name VARCHAR(100) NOT NULL COMMENT '字段名',
permission_type ENUM('read', 'write', 'delete') NOT NULL COMMENT '权限类型',
permission_value ENUM('allow', 'deny') NOT NULL DEFAULT 'allow' COMMENT '权限值',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_role_table_field (role_id, table_name, field_name, permission_type),
FOREIGN KEY (role_id) REFERENCES sys_role(role_id) ON DELETE CASCADE
);
-- 创建索引优化查询
CREATE INDEX idx_field_permission_role ON sys_field_permission(role_id);
CREATE INDEX idx_field_permission_table ON sys_field_permission(table_name);
这个设计的核心思想是:不是黑名单,而是白名单。
大多数公司在实现字段权限时会犯一个错误:采用黑名单模式,默认开放所有字段,然后对敏感字段单独设置拒绝。这种方式的问题是,一旦漏配一个字段,所有角色都能看到。
正确的方式是白名单模式:默认所有角色看不到任何敏感字段,只有明确授权的字段才能访问。这样可以最大程度降低配置遗漏带来的风险。
一个真实的字段权限配置示例
假设我们有以下角色:
sales:普通销售sales_manager:销售经理hr:HR专员finance:财务人员admin:系统管理员
对应的字段权限配置如下:
-- 客户信息表的字段权限配置
INSERT INTO sys_field_permission (role_id, table_name, field_name, permission_type, permission_value) VALUES
-- 普通销售的客户表权限
(1, 'customer', 'customer_id', 'read', 'allow'),
(1, 'customer', 'customer_name', 'read', 'allow'),
(1, 'customer', 'customer_phone', 'read', 'allow'),
(1, 'customer', 'customer_email', 'read', 'allow'),
(1, 'customer', 'customer_source', 'read', 'allow'),
(1, 'customer', 'customer_address', 'write', 'allow'),
(1, 'customer', 'customer_remark', 'write', 'allow'),
-- 销售经理的完整权限
(2, 'customer', 'customer_id', 'read', 'allow'),
(2, 'customer', 'customer_name', 'read', 'allow'),
(2, 'customer', 'customer_phone', 'read', 'allow'),
(2, 'customer', 'customer_email', 'read', 'allow'),
(2, 'customer', 'customer_source', 'read', 'allow'),
(2, 'customer', 'customer_value', 'read', 'allow'),
(2, 'customer', 'customer_address', 'write', 'allow'),
(2, 'customer', 'customer_remark', 'write', 'allow'),
(2, 'customer', 'customer_value', 'write', 'allow'),
-- HR的权限(只能看脱敏后的手机号)
(3, 'customer', 'customer_id', 'read', 'allow'),
(3, 'customer', 'customer_name', 'read', 'allow'),
(3, 'customer', 'customer_phone', 'read', 'deny'),
(3, 'customer', 'customer_email', 'read', 'allow'),
(3, 'customer', 'customer_source', 'read', 'allow'),
-- 财务人员的权限
(4, 'customer', 'customer_id', 'read', 'allow'),
(4, 'customer', 'customer_name', 'read', 'allow'),
(4, 'customer', 'customer_phone', 'read', 'deny'),
(4, 'customer', 'customer_email', 'read', 'deny'),
(4, 'customer', 'customer_source', 'read', 'allow'),
(4, 'customer', 'customer_value', 'read', 'allow'),
-- 管理员的完整权限
(5, 'customer', 'customer_id', 'read', 'allow'),
(5, 'customer', 'customer_name', 'read', 'allow'),
(5, 'customer', 'customer_phone', 'read', 'allow'),
(5, 'customer', 'customer_email', 'read', 'allow'),
(5, 'customer', 'customer_source', 'read', 'allow'),
(5, 'customer', 'customer_value', 'read', 'allow'),
(5, 'customer', 'customer_address', 'write', 'allow'),
(5, 'customer', 'customer_remark', 'write', 'allow'),
(5, 'customer', 'customer_value', 'write', 'allow');
注意这里的设计:HR能看到客户姓名,但看不到手机号;财务人员能看到客户价值评估,但看不到手机号和邮箱。这些细粒度的控制,正是字段级权限的核心价值。
后端代码实现:动态查询字段过滤
现在说说最核心的部分——后端如何根据字段权限动态过滤查询结果。
以Java Spring Boot为例,我提供一个完整的实现方案:
第一步:创建一个字段权限解析器
/**
* 字段权限解析器
* 根据当前用户的角色和请求的表名,返回允许访问的字段列表
*/
@Component
public class FieldPermissionParser {
@Autowired
private FieldPermissionRepository fieldPermissionRepository;
/**
* 获取指定角色对指定表的指定操作类型允许访问的字段
*/
public Set<String> getAllowedFields(String tableName, String roleName, String operationType) {
// 查询该角色对该表该操作类型的所有允许字段
List<FieldPermission> permissions = fieldPermissionRepository.findByRoleAndTableAndOperation(
roleName, tableName, operationType
);
Set<String> allowedFields = new HashSet<>();
for (FieldPermission perm : permissions) {
if ("allow".equals(perm.getPermissionValue())) {
allowedFields.add(perm.getFieldName());
}
}
return allowedFields;
}
/**
* 获取指定角色的完整字段权限映射
* 返回Map结构:表名 -> (字段名 -> 操作权限列表)
*/
public Map<String, Map<String, List<String>>> getAllowedFieldsForRole(String roleName) {
List<FieldPermission> allPermissions = fieldPermissionRepository.findByRole(roleName);
Map<String, Map<String, List<String>>> resultMap = new HashMap<>();
for (FieldPermission perm : allPermissions) {
resultMap
.computeIfAbsent(perm.getTableName(), k -> new HashMap<>())
.computeIfAbsent(perm.getFieldName(), k -> new ArrayList<>())
.add(perm.getPermissionType());
}
return resultMap;
}
}
第二步:创建动态SQL查询拦截器
/**
* 字段权限查询拦截器
* 在SQL执行前,根据当前用户权限动态修改SELECT字段列表
*/
@Interceptor
public class FieldPermissionInterceptor implements StatementInterceptor {
@Autowired
private FieldPermissionParser fieldPermissionParser;
@Override
public boolean intercept(StatementContext context, SqlQuery sqlQuery) {
// 获取当前用户信息(从ThreadLocal或SecurityContext获取)
UserContext user = UserContextHolder.getCurrentUser();
if (user == null) {
return true;
}
String tableName = sqlQuery.getTableName();
if (tableName == null || tableName.isEmpty()) {
return true;
}
// 检查是否需要字段权限过滤
if (!fieldPermissionParser.hasPermission(tableName)) {
return true;
}
// 构建允许查询的字段列表
Set<String> allowedFields = fieldPermissionParser.getAllowedFields(
tableName, user.getRoleCode(), "read"
);
// 如果没有权限,直接拒绝
if (allowedFields.isEmpty()) {
throw new FieldPermissionDeniedException(
String.format("用户[%s]无权访问表[%s]的任何字段", user.getUsername(), tableName)
);
}
// 动态修改SQL,只查询允许的字段
String originalSql = sqlQuery.getSql();
String modifiedSql = modifySelectFields(originalSql, allowedFields);
context.setSql(modifiedSql);
return true;
}
/**
* 修改SQL中的SELECT字段列表
*/
private String modifySelectFields(String sql, Set<String> allowedFields) {
// 解析SQL,替换SELECT子句
Pattern selectPattern = Pattern.compile(
"SELECT\\s+(.*?)\\s+FROM",
Pattern.CASE_INSENSITIVE | Pattern.DOTALL
);
Matcher matcher = selectPattern.matcher(sql);
if (matcher.find()) {
String selectClause = matcher.group(1).trim();
// 检查是否是SELECT *
if ("*".equals(selectClause)) {
// 替换为允许的字段列表
String allowedFieldsStr = allowedFields.stream()
.map(field -> "`" + field + "`")
.collect(Collectors.joining(", "));
return sql.replaceFirst(
"(?is)SELECT\\s+.*?\\s+FROM",
"SELECT " + allowedFieldsStr + " FROM"
);
}
// 检查SELECT子句中的每个字段
String[] fields = selectClause.split(",");
List<String> filteredFields = new ArrayList<>();
for (String field : fields) {
String fieldName = field.trim();
// 如果是聚合函数或表达式,保留
if (fieldName.matches(".*\\(.*\\).*")) {
filteredFields.add(fieldName);
} else if (allowedFields.contains(fieldName)) {
filteredFields.add(fieldName);
}
// 否则过滤掉
}
if (filteredFields.isEmpty()) {
throw new FieldPermissionDeniedException(
String.format("用户无权访问表中的任何字段")
);
}
String newSelectClause = String.join(", ", filteredFields);
return sql.replaceFirst(
"(?is)SELECT\\s+.*?\\s+FROM",
"SELECT " + newSelectClause + " FROM"
);
}
return sql;
}
}
第三步:针对写操作的权限校验
/**
* 字段权限写操作校验器
* 在处理INSERT/UPDATE/DELETE操作前,校验用户是否有权限操作相关字段
*/
@Component
public class FieldPermissionWriteValidator {
@Autowired
private FieldPermissionParser fieldPermissionParser;
/**
* 校验INSERT操作
*/
public void validateInsert(String tableName, Map<String, Object> data, UserContext user) {
Set<String> allowedFields = fieldPermissionParser.getAllowedFields(
tableName, user.getRoleCode(), "write"
);
for (String field : data.keySet()) {
if (!allowedFields.contains(field)) {
throw new FieldPermissionDeniedException(
String.format("用户[%s]无权写入字段[%s]", user.getUsername(), field)
);
}
}
}
/**
* 校验UPDATE操作
*/
public void validateUpdate(String tableName, Map<String, Object> data, UserContext user) {
Set<String> allowedFields = fieldPermissionParser.getAllowedFields(
tableName, user.getRoleCode(), "write"
);
for (String field : data.keySet()) {
if (!allowedFields.contains(field)) {
throw new FieldPermissionDeniedException(
String.format("用户[%s]无权修改字段[%s]", user.getUsername(), field)
);
}
}
}
/**
* 校验DELETE操作(通常是整个记录的删除,这里主要是校验是否有权限)
*/
public void validateDelete(String tableName, UserContext user) {
Set<String> allowedFields = fieldPermissionParser.getAllowedFields(
tableName, user.getRoleCode(), "delete"
);
// DELETE操作需要至少有一个字段的删除权限
if (allowedFields.isEmpty()) {
throw new FieldPermissionDeniedException(
String.format("用户[%s]无权删除表[%s]的数据", user.getUsername(), tableName)
);
}
}
}
第四步:MyBatis拦截器的完整实现
/**
* MyBatis字段权限拦截器
*/
@Interceptor
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
@Signature(type = ParameterizedStatement.class, method = "parameterize", args = {Statement.class})
})
public class MyBatisFieldPermissionInterceptor implements Interceptor {
private static final Logger log = LoggerFactory.getLogger(MyBatisFieldPermissionInterceptor.class);
@Autowired
private FieldPermissionParser fieldPermissionParser;
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取StatementHandler
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
// 获取MetaObject
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
// 获取BoundSql
BoundSql boundSql = statementHandler.getBoundSql();
String originalSql = boundSql.getSql();
// 获取当前用户上下文
UserContext user = UserContextHolder.getCurrentUser();
if (user == null) {
return invocation.proceed();
}
// 从SQL中解析表名(简化实现,实际项目可能需要更复杂的解析)
String tableName = parseTableName(originalSql);
if (tableName == null) {
return invocation.proceed();
}
// 判断SQL操作类型
String operationType = parseOperationType(originalSql);
// 根据操作类型获取允许的字段
Set<String> allowedFields;
if ("read".equals(operationType)) {
allowedFields = fieldPermissionParser.getAllowedFields(
tableName, user.getRoleCode(), "read"
);
} else {
// 写操作不做字段过滤,只做校验
return invocation.proceed();
}
// 如果没有权限,拒绝访问
if (allowedFields.isEmpty()) {
log.warn("用户[{}]无权访问表[{}]的任何字段,拒绝查询", user.getUsername(), tableName);
throw new FieldPermissionDeniedException(
String.format("您无权访问此数据")
);
}
// 修改SQL,只查询允许的字段
String modifiedSql = modifySqlFields(originalSql, allowedFields);
// 更新BoundSql
metaObject.setValue("delegate.boundSql.sql", modifiedSql);
log.debug("用户[{}]查询表[{}],原SQL: {}, 修改后SQL: {}", user.getUsername(), tableName, originalSql, modifiedSql);
return invocation.proceed();
}
/**
* 从SQL中解析表名(简化实现)
*/
private String parseTableName(String sql) {
// 简化实现:从SQL中提取表名
// 实际项目中可能需要使用SQL解析器如JSqlParser
Pattern fromPattern = Pattern.compile(
"\\bFROM\\s+([a-zA-Z_][a-zA-Z0-9_]*)",
Pattern.CASE_INSENSITIVE
);
Matcher matcher = fromPattern.matcher(sql);
if (matcher.find()) {
return matcher.group(1);
}
return null;
}
/**
* 从SQL中解析操作类型
*/
private String parseOperationType(String sql) {
String upperSql = sql.toUpperCase().trim();
if (upperSql.startsWith("SELECT")) {
return "read";
} else if (upperSql.startsWith("INSERT")) {
return "write";
} else if (upperSql.startsWith("UPDATE")) {
return "write";
} else if (upperSql.startsWith("DELETE")) {
return "delete";
}
return null;
}
/**
* 修改SQL中的SELECT字段
*/
private String modifySqlFields(String sql, Set<String> allowedFields) {
// 使用正则表达式替换SELECT子句
Pattern selectPattern = Pattern.compile(
"(?is)(SELECT\\s+)(.*?)(\\s+FROM)",
Pattern.CASE_INSENSITIVE | Pattern.DOTALL
);
Matcher matcher = selectPattern.matcher(sql);
if (matcher.find()) {
String selectContent = matcher.group(2).trim();
// 处理SELECT * 的情况
if (selectContent.equals("*")) {
String allowedFieldsStr = allowedFields.stream()
.map(field -> "`" + field + "`")
.collect(Collectors.joining(", "));
return matcher.replaceFirst("$1" + allowedFieldsStr + "$3");
}
// 处理普通字段列表的情况
String[] fields = selectContent.split(",");
List<String> filteredFields = new ArrayList<>();
for (String field : fields) {
String trimmedField = field.trim();
// 保留聚合函数、表达式和允许的字段
if (trimmedField.matches(".*\\(.*\\).*") || allowedFields.contains(trimmedField)) {
filteredFields.add(trimmedField);
}
}
if (filteredFields.isEmpty()) {
throw new FieldPermissionDeniedException("您无权访问此数据");
}
String newSelectContent = String.join(", ", filteredFields);
return matcher.replaceFirst("$1" + newSelectContent + "$3");
}
return sql;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 初始化配置
}
}
前端展示层面的字段控制
除了后端的数据过滤,前端还需要根据字段权限动态展示表单字段。
比如一个客户信息编辑页面,不同角色看到的编辑表单应该不一样:
/**
* 前端字段权限配置
* 根据当前用户角色动态生成表单字段
*/
const FIELD_PERMISSION_CONFIG = {
'customer': {
'sales': {
'readonlyFields': ['customer_id', 'customer_value'],
'hiddenFields': ['customer_source'],
'editFields': ['customer_name', 'customer_phone', 'customer_email', 'customer_address', 'customer_remark']
},
'sales_manager': {
'readonlyFields': [],
'hiddenFields': [],
'editFields': ['customer_name', 'customer_phone', 'customer_email', 'customer_address', 'customer_remark', 'customer_value']
},
'hr': {
'readonlyFields': ['customer_phone', 'customer_value'],
'hiddenFields': [],
'editFields': ['customer_name', 'customer_email', 'customer_address', 'customer_source']
},
'admin': {
'readonlyFields': [],
'hiddenFields': [],
'editFields': ['customer_name', 'customer_phone', 'customer_email', 'customer_address', 'customer_source', 'customer_value', 'customer_remark']
}
}
};
/**
* 根据用户角色获取可编辑字段列表
*/
function getEditableFields(tableName, roleName) {
const config = FIELD_PERMISSION_CONFIG[tableName];
if (!config || !config[roleName]) {
return [];
}
return config[roleName].editFields || [];
}
/**
* 渲染表单字段(前端示例)
*/
function renderForm(tableName, data, roleName) {
const editableFields = getEditableFields(tableName, roleName);
const readonlyFields = data ? Object.keys(data).filter(f => !editableFields.includes(f)) : [];
let html = '<form>';
// 渲染可编辑字段
editableFields.forEach(field => {
const value = data ? data[field] : '';
html += `
<div class="form-group">
<label>${getFieldLabel(field)}</label>
<input type="text" name="${field}" value="${value}" />
</div>
`;
});
// 渲染只读字段(隐藏或显示为只读)
readonlyFields.forEach(field => {
const value = data ? data[field] : '';
html += `
<div class="form-group readonly">
<label>${getFieldLabel(field)}</label>
<span>${value || '未填写'}</span>
</div>
`;
});
html += '</form>';
return html;
}
防越权访问的核心机制
有了字段级权限配置,还有一个关键问题:如何防止用户通过API绕过前端限制,直接调用接口获取敏感数据?
这里需要多层防护:
第一层:接口级别校验
/**
* 字段权限校验注解
* 用于在Controller层对接口进行字段权限校验
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldPermissionCheck {
String tableName() default "";
String operationType() default "read";
}
/**
* 字段权限校验拦截器
*/
@Component
public class FieldPermissionAspect {
@Autowired
private FieldPermissionParser fieldPermissionParser;
@Around("@annotation(fieldPermissionCheck)")
public Object around(ProceedingJoinPoint joinPoint, FieldPermissionCheck fieldPermissionCheck) throws Throwable {
UserContext user = UserContextHolder.getCurrentUser();
if (user == null) {
throw new UnauthorizedException("未登录");
}
String tableName = fieldPermissionCheck.tableName();
String operationType = fieldPermissionCheck.operationType();
Set<String> allowedFields = fieldPermissionParser.getAllowedFields(
tableName, user.getRoleCode(), operationType
);
if (allowedFields.isEmpty()) {
throw new FieldPermissionDeniedException("您无权访问此数据");
}
// 将允许访问的字段列表注入到请求上下文
RequestContextHolder.setAllowedFields(tableName, allowedFields);
return joinPoint.proceed();
}
}
第二层:API响应过滤
/**
* API响应过滤器
* 在返回数据前,根据用户权限过滤敏感字段
*/
@Component
public class ApiResponseFilter implements HandlerInterceptor {
@Autowired
private FieldPermissionParser fieldPermissionParser;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
// 如果不是JSON响应,直接跳过
String contentType = response.getContentType();
if (contentType == null || !contentType.contains("application/json")) {
return;
}
UserContext user = UserContextHolder.getCurrentUser();
if (user == null) {
return;
}
// 获取当前请求的表名和操作类型(从ThreadLocal或请求属性获取)
String tableName = (String) request.getAttribute("currentTable");
String operationType = (String) request.getAttribute("currentOperationType");
if (tableName == null || operationType == null) {
return;
}
Set<String> allowedFields = fieldPermissionParser.getAllowedFields(
tableName, user.getRoleCode(), operationType
);
// 将允许字段列表设置到响应中,供后续过滤使用
response.setAttribute("allowedFields", allowedFields);
}
}
第三层:数据序列化时的字段过滤
/**
* 数据序列化过滤器
* 在将数据转换为JSON时,根据用户权限过滤敏感字段
*/
@Component
public class FieldSensitiveSerializer extends JsonSerializer<Object> {
@Autowired
private FieldPermissionParser fieldPermissionParser;
@Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
UserContext user = UserContextHolder.getCurrentUser();
if (user == null) {
gen.writeObject(value);
return;
}
// 获取请求上下文中的表名和允许字段
String tableName = (String) gen.getConfig().getSerializerProvider().getProperty();
// 简化实现,实际项目中需要从请求上下文获取
String allowedFieldsJson = (String) gen.getConfig().getSerializerProvider().getProperty();
if (allowedFieldsJson != null && !allowedFieldsJson.isEmpty()) {
Set<String> allowedFields = new HashSet<>(Arrays.asList(allowedFieldsJson.split(",")));
// 过滤字段后序列化
Map<String, Object> filteredValue = filterFields(value, allowedFields);
gen.writeObject(filteredValue);
} else {
gen.writeObject(value);
}
}
private Map<String, Object> filterFields(Object value, Set<String> allowedFields) {
if (value instanceof Map) {
Map<String, Object> resultMap = new HashMap<>();
Map<String, Object> sourceMap = (Map<String, Object>) value;
for (Map.Entry<String, Object> entry : sourceMap.entrySet()) {
if (allowedFields.contains(entry.getKey())) {
resultMap.put(entry.getKey(), entry.getValue());
}
}
return resultMap;
}
return Collections.emptyMap();
}
}
实际部署中的常见问题和解决方案
问题一:权限配置遗漏导致数据泄露
这是最常见的问题。一个字段配置了权限,但另一个字段忘了配置,结果这个字段对所有角色都开放了。
解决方案:采用”默认拒绝”策略。所有字段默认都是拒绝访问的,只有明确配置了”允许”的字段才能访问。同时,定期运行权限审计脚本,检查是否有未配置的字段。
-- 权限审计查询:检查未配置权限的字段
SELECT
t.TABLE_NAME,
t.COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS t
LEFT JOIN sys_field_permission p ON
p.table_name = t.TABLE_NAME
AND p.field_name = t.COLUMN_NAME
WHERE t.TABLE_SCHEMA = 'your_database'
AND p.permission_id IS NULL
ORDER BY t.TABLE_NAME, t.COLUMN_NAME;
问题二:权限变更后的实时生效问题
配置了权限后,已登录的用户可能还持有旧的权限数据。
解决方案:在权限变更时,清除相关用户的缓存,或者在每次请求时实时查询权限配置。对于高频访问的场景,可以采用短TTL缓存(如5分钟)。
/**
* 权限缓存管理
*/
@Component
public class FieldPermissionCacheManager {
private final Cache<String, Set<String>> permissionCache;
public FieldPermissionCacheManager() {
this.permissionCache = CacheBuilder.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.maximumSize(10000)
.build();
}
/**
* 清除指定用户的权限缓存
*/
public void invalidatePermission(String username) {
permissionCache.invalidateAll();
}
/**
* 清除指定角色的权限缓存
*/
public void invalidatePermissionForRole(String roleName) {
permissionCache.asMap().entrySet().removeIf(
entry -> entry.getKey().contains(roleName)
);
}
}
问题三:复杂的权限组合场景
有时候权限规则比较复杂,比如”销售经理可以看所有客户的手机号,但普通销售只能看自己负责的客户的手机号”。
解决方案:字段权限和数据权限分开处理。字段权限控制能看到哪些字段,数据权限控制能看到哪些数据行。两者结合才能实现完整的权限控制。
/**
* 数据权限拦截器
* 在查询数据时,根据用户权限过滤数据行
*/
@Interceptor
public class DataPermissionInterceptor implements StatementInterceptor {
@Override
public boolean intercept(StatementContext context, SqlQuery sqlQuery) {
UserContext user = UserContextHolder.getCurrentUser();
if (user == null) {
return true;
}
String tableName = sqlQuery.getTableName();
if (tableName == null) {
return true;
}
// 获取数据权限条件
String dataPermissionCondition = dataPermissionParser.getDataPermissionCondition(
tableName, user.getRoleCode()
);
if (dataPermissionCondition != null && !dataPermissionCondition.isEmpty()) {
// 将权限条件追加到SQL的WHERE子句
String modifiedSql = appendWhereCondition(sqlQuery.getSql(), dataPermissionCondition);
context.setSql(modifiedSql);
}
return true;
}
}
问题四:外包人员的权限控制
外包人员通常需要访问系统,但权限范围要严格限制。比如外包人员可以查看客户信息,但不能导出;可以录入数据,但不能删除。
解决方案:为外包人员创建特殊的角色,并配置严格的字段权限和操作权限。同时,在外包人员的登录会话中设置更短的超时时间,并记录所有操作日志。
/**
* 外包人员权限配置
*/
@Component
public class OutsourcerPermissionConfig {
/**
* 外包人员的权限规则
*/
public Map<String, Set<String>> getOutsourcerPermissions() {
Map<String, Set<String>> permissions = new HashMap<>();
// 客户表:只能查看和录入,不能删除
permissions.put("customer", Set.of("customer_id", "customer_name", "customer_phone",
"customer_email", "customer_address", "customer_source"));
// 薪资表:完全不可见
permissions.put("salary", Set.of());
// 财务表:只能查看汇总数据,不能查看明细
permissions.put("finance", Set.of("summary_amount", "summary_date"));
return permissions;
}
}
总结
字段级权限配置听起来复杂,但其实核心思路很简单:先定义好每个角色能看什么字段、能改什么字段、能删什么字段,然后在代码层面严格执行这个配置。
实际落地时,需要注意的是:
- 权限配置要遵循最小权限原则——能给少的不给多的
- 采用白名单模式——默认拒绝,明确允许
- 前后端双重校验——不要信任前端传来的任何权限信息
- 定期审计权限配置——发现未配置的字段及时补上
- 记录权限操作日志——方便追踪和排查问题
当你把这套机制跑起来之后,那些”员工误看客户手机号”、”HR误查薪资表”、”外包删除财务数据”的问题,就再也不会发生了。
