Spring RESTful服务中特定用户数据隐藏的实现方案咨询
嘿,针对你遇到的这个脱敏数据同时还要保证后续接口调用正常的问题,我给你几个更合理的实现思路,比你之前加flag参数的方案更优雅也更安全:
你之前的思路是在JDBC层直接处理数据,这会把数据访问和业务逻辑混在一起,后期维护起来很麻烦。更好的做法是把数据分为「实体层」和「DTO层」:
- 实体类(比如
EmployeeEntity)严格对应数据库表,只负责存储从JDBC查询到的原始数据 - 针对不同场景做两个DTO:
EmployeePublicDTO(返回原始数据)和EmployeeMaskedDTO(返回脱敏数据),关键是脱敏DTO要保留empId这类能唯一标识用户的真实参数,这样UI点击行时可以用这些参数调用新接口,不会因为脱敏值失败。
示例代码:
// 数据库实体类 public class EmployeeEntity { private Long empId; private String firstName; private String lastName; private Date dob; // 省略getter/setter } // 公开场景DTO(返回真实数据) public class EmployeePublicDTO { private Long empId; private String fullName; private String dob; // 从实体转换的方法 public static EmployeePublicDTO fromEntity(EmployeeEntity entity) { EmployeePublicDTO dto = new EmployeePublicDTO(); dto.setEmpId(entity.getEmpId()); dto.setFullName(entity.getFirstName() + ", " + entity.getLastName()); dto.setDob(new SimpleDateFormat("MM/dd/yyyy").format(entity.getDob())); return dto; } } // 脱敏场景DTO(返回隐藏格式数据) public class EmployeeMaskedDTO { private Long empId; // 保留真实ID,供UI调用接口用 private String fullName; private String dob; public static EmployeeMaskedDTO fromEntity(EmployeeEntity entity) { EmployeeMaskedDTO dto = new EmployeeMaskedDTO(); dto.setEmpId(entity.getEmpId()); dto.setFullName("####,####"); dto.setDob("##/##/####"); return dto; } }
然后在Service层根据当前用户的身份(比如从Spring Security获取当前用户角色,而不是前端传的flag)来选择返回哪种DTO:
@Service public class EmployeeService { @Autowired private JdbcTemplate jdbcTemplate; public List<?> getEmployees(boolean needMasking) { List<EmployeeEntity> entities = jdbcTemplate.query("SELECT emp_id, first_name, last_name, dob FROM employees", (rs, rowNum) -> { EmployeeEntity entity = new EmployeeEntity(); entity.setEmpId(rs.getLong("emp_id")); entity.setFirstName(rs.getString("first_name")); entity.setLastName(rs.getString("last_name")); entity.setDob(rs.getDate("dob")); return entity; }); if (needMasking) { return entities.stream().map(EmployeeMaskedDTO::fromEntity).collect(Collectors.toList()); } else { return entities.stream().map(EmployeePublicDTO::fromEntity).collect(Collectors.toList()); } } }
这种方式把JDBC层的职责简化为只查数据,脱敏逻辑放在DTO转换层,而且用服务器端的用户身份判断是否脱敏,比前端传flag更安全(避免用户篡改参数获取真实数据)。
Spring 4.2.5已经支持ResponseBodyAdvice,可以在响应返回给客户端之前统一处理数据脱敏,不用修改每个Controller的代码。核心思路是:返回包含所有字段的DTO,然后在全局增强器里根据用户身份修改需要脱敏的字段,保留关键参数。
示例代码:
@ControllerAdvice public class EmployeeMaskingAdvice implements ResponseBodyAdvice<Object> { // 只处理Employee相关的DTO @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { return EmployeeDTO.class.isAssignableFrom(returnType.getParameterType()); } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { // 判断当前用户是否需要脱敏(这里可以从Spring Security获取用户角色,或者请求头里的标识) boolean needMasking = isRestrictedUser(request); if (needMasking && body instanceof EmployeeDTO) { EmployeeDTO dto = (EmployeeDTO) body; // 复制一个新对象,避免修改原始数据 EmployeeDTO maskedDto = new EmployeeDTO(); BeanUtils.copyProperties(dto, maskedDto); // 脱敏处理显示字段 maskedDto.setFirstName("XXXXX"); maskedDto.setLastName("XXXXX"); maskedDto.setQuantity("####"); // empId等关键参数保留原值 return maskedDto; } return body; } private boolean isRestrictedUser(ServerHttpRequest request) { // 示例:从请求头获取用户角色,根据角色判断是否需要脱敏 String userRole = request.getHeaders().getFirst("X-User-Role"); return "RESTRICTED".equals(userRole); } } // 通用DTO,包含所有字段 public class EmployeeDTO { private Long empId; private String firstName; private String lastName; private String quantity; private String dob; // 省略getter/setter }
这个方案的好处是全局统一处理,所有返回EmployeeDTO的接口都会自动根据用户身份脱敏,不用重复写逻辑,而且同样保留了真实的empId,不影响后续接口调用。
如果你的数据不是特别敏感,或者允许客户端看到原始数据(不推荐敏感数据这么做),可以在jqxWidget渲染的时候做脱敏显示,但保留原始数据在组件里。比如:
// jqxGrid列配置,渲染时替换为脱敏值 $("#employeeGrid").jqxGrid({ columns: [ { text: 'First Name', datafield: 'firstName', cellsrenderer: () => '<div>XXXXX</div>' }, { text: 'Last Name', datafield: 'lastName', cellsrenderer: () => '<div>XXXXX</div>' }, { text: 'Quantity', datafield: 'quantity', cellsrenderer: () => '<div>####</div>' }, // 其他列... ] }); // 点击行时获取原始数据调用接口 $("#employeeGrid").on('rowclick', function(event) { const rowData = event.args.row; // rowData里的firstName、lastName都是原始值,直接用来调用新接口 $.get('/api/employee/details', { firstName: rowData.firstName, lastName: rowData.lastName }, function(data) { // 处理详情数据 }); });
这个方案的缺点是原始数据会暴露在浏览器的网络请求里,敏感数据存在泄露风险,所以只适合非敏感场景。
你之前想加flag参数在JDBC层处理的问题在于:
- JDBC层职责变复杂,混合了数据访问和业务逻辑
- 前端传flag容易被篡改,存在安全风险
- 多个接口需要脱敏的话,会重复写很多判断逻辑
而上面的方案都是从服务器端判断用户身份来决定是否脱敏,更安全,而且逻辑分层更清晰,也解决了后续接口调用的问题。
内容的提问来源于stack exchange,提问作者Coder

