客户与分层员工角色应共用 User,还是拆分为 Customer/Employee?
共用账户,分别建模客户资料和员工任职
按目前的需求,建议保留一个共享的 Account(也可以继续叫 User),统一保存登录凭据和账户状态。客户资料、员工资料和员工任职关系分别建模,JWT 校验、token 签发和密码认证基础设施继续共用。
客户不参与员工的 rank / department 判断,客户和员工仍可共用登录实体、密码认证和相关代码。
模型关系可以是:
Account
├── CustomerProfile(可选,关联购物车、订单等)
└── EmployeeProfile(可选,关联员工任职信息)
同一个人既是员工又会购物时,可以同时拥有两种业务身份。如果业务要求员工账号与购物账号隔离,也可以使用不同的 Account,认证代码仍然可以复用。
ROLE_CUSTOMER 可以与员工角色放在同一张角色表中,员工的级别和部门规则只用于员工角色。
区分账户、业务身份和授权规则
当前 User 同时负责登录账户、客户购物关系、员工职位,以及与 Spring Security 的适配。功能继续增加后,这些职责混在一起会更难维护。
建议按职责拆分:
| 模型 | 负责的内容 |
|---|---|
Account | 登录标识、密码哈希、启用状态 |
CustomerProfile | 客户资料,以及购物车、订单的归属 |
EmployeeProfile | 员工资料、在职状态 |
StaffAssignment | 员工职位、部门、任职有效性 |
Role / Authority | 授权所需的角色或能力标识 |
StaffAssignment 可以先作为 EmployeeProfile 中的一组字段,不必立即单独建表。需要支持跨部门任职、多职位或任职历史时,再拆成独立的关联实体。
业务实体也不必实现 UserDetails。安全模块可以把账户转换成专门的 AccountPrincipal,或 Spring Security 的 UserDetails 实现。这样修改客户字段时,就不需要连带修改认证接口。
ROLE_CUSTOMER 是授权标识,不是认证成功的条件。在用户名和密码认证中,UserDetailsService 为 DaoAuthenticationProvider 提供账户及凭据信息;账户的角色用于后续授权。Spring Security 的 UserDetailsService 文档
客户角色可以共表,员工属性单独存放
共享的角色表可以只保存稳定的角色标识:
roles
-----
role_id
role_name
account_roles
-------------
account_id
role_id
表中可以同时存在:
ROLE_CUSTOMER
ROLE_EMPLOYEE
ROLE_SUPER_ADMIN
员工的职位和部门放在任职信息中:
staff_assignments
-----------------
employee_id
level
department
这是一种可选设计,现有的 ROLE_PRODUCT_MANAGER 等角色不必全部改名。如果部门角色本身就定义为“商品部门经理”,也可以把部门保留在员工专用角色模型中。要避免两处记录互相矛盾,例如角色是商品经理,任职部门却是订单部门。
对于小项目或迁移阶段,让 ROLE_CUSTOMER.rank 和 ROLE_CUSTOMER.department 为 null 也可以接受,但需要满足以下条件:
- 员工授权逻辑先确认参与者具有有效员工身份。
- 不把
null隐式解释为最低级别或任意部门。 - 通用角色处理代码不默认所有角色都有级别和部门。
- 对员工角色所需的字段设置明确的数据校验。
是否拆表,不能只看有没有空字段。如果大量逻辑都在反复判断“这个角色是不是客户角色”,就需要重新考虑模型边界。
用角色表达能力,用部门和归属限制范围
这个需求适合结合 RBAC 和基于属性的授权检查:
- 角色或权限决定能否执行某项操作,例如管理员工、处理订单。
- 部门、职位和目标身份决定操作范围。
- 客户通过资源归属关系访问自己的购物车和订单。
员工管理规则如下:
| 操作者 | 目标 | 员工管理权限 |
|---|---|---|
| Super Admin | 任意员工 | 允许,但仍受系统业务约束限制 |
| 部门经理 | 本部门普通员工 | 允许 |
| 部门经理 | 其他部门员工、任意经理、Super Admin | 拒绝 |
| 普通员工或纯客户 | 任意员工 | 拒绝 |
客户资源使用独立的归属策略。Super Admin 如果需要管理客户账号,可以另外授予客户管理权限,无须将客户纳入员工级别比较。
Spring Security 的 RoleHierarchy 表达角色所包含的权限,例如经理包含某些员工权限。判断经理能删除哪些员工,还需要目标对象、部门和具体操作的信息,单靠角色继承无法得出结果。
不要用 getPrimaryRole() 代替完整授权
按最高 rank 选取一个角色,只有在每个员工恰好拥有一个有效职位,且该职位完整决定其管理范围时,才比较容易成立。
如果某人同时担任商品经理和订单部门员工,只保留最高职位,就会丢失订单部门的任职信息。多个角色同级且分属不同部门时,也无法仅凭最高级别唯一选出一个角色。
职位高低也不自动决定所有业务权限。商品经理可以管理本部门员工,但不一定拥有所有商品、订单或财务操作权限。
对于目前固定的三层结构,职位枚举通常比任意整数更清楚。以下示例假设每个员工只有一个有效任职,Super Admin 不绑定部门,其他职位必须绑定部门:
enum StaffLevel {
SUPER_ADMIN, MANAGER, EMPLOYEE
}
enum Department {
PRODUCT, CATEGORY, ORDER
}
record StaffAssignment(
StaffLevel level,
Department department
) {
StaffAssignment {
Objects.requireNonNull(level);
if (level != StaffLevel.SUPER_ADMIN && department == null) {
throw new IllegalArgumentException("Department is required");
}
}
}
final class StaffPolicy {
static boolean canManage(
StaffAssignment actor,
StaffAssignment target
) {
if (actor == null || target == null) {
return false;
}
if (actor.level() == StaffLevel.SUPER_ADMIN) {
return true;
}
return actor.level() == StaffLevel.MANAGER
&& target.level() == StaffLevel.EMPLOYEE
&& actor.department() == target.department();
}
}
示例省略了包名和 import。该函数只判断员工职位及部门范围,调用方还要检查账户是否启用、任职是否有效,以及是否允许执行当前操作。
以后支持跨部门兼职时,需要根据完整任职集合定义策略,并明确部门经理能否管理拥有多个职位的目标员工,不能简单地改成“任意一个低级职位匹配就允许”。
创建、删除和角色变更需要分别检查
原来的 canAct(actor, target) 可以辅助判断已有员工的管理范围。创建员工时尚无目标用户,需要检查准备写入的任职信息:
boolean allowed = StaffPolicy.canManage(
actorAssignment,
requestedAssignment
);
部门经理应当只能创建本部门普通员工。服务端还应按允许字段列表构造实体,不能直接保存客户端提交的角色集合、账户状态或其他敏感字段。
更新员工时,要同时检查当前状态和变更后的状态。例如,商品经理可以管理本部门普通员工,但不能通过更新接口将其提升为经理。
建议将这些操作分别定义为 canCreate、canDelete、canChangeAssignment。它们可以复用范围判断,同时保留各自的业务限制。
在服务层接入权限检查
可以继续使用 PermissionEvaluator,也可以先用普通的 Spring Bean 实现策略:
@PreAuthorize(
"@employeeAccess.canDelete(authentication, #p0)"
)
public void deleteEmployee(String employeeId) {
// 执行员工删除或离职处理
}
employeeAccess.canDelete 应从可信的认证身份获取账户 ID,再从服务端数据中加载操作者的有效任职和目标员工信息。查询不到目标、身份无效或操作不受支持时,都应拒绝访问。
使用现代 Spring Security 方法安全配置时,需要启用 @EnableMethodSecurity。@PreAuthorize 可以调用 Spring Bean,hasPermission 则是接入 PermissionEvaluator 的入口。Spring Security 方法安全文档
如果保留原来的写法:
@PreAuthorize("hasPermission(#p0, 'Employee', 'DELETE')")
就需要将自定义 PermissionEvaluator 配置到方法安全的表达式处理器中,并正确处理根据目标 ID、目标类型和操作名称执行检查的重载。仅实现接口并不会自动补全业务策略。
客户资源直接按当前账户限定查询
购物车和订单不参与员工级别判断。访问订单时,可以同时用资源 ID 和当前账户 ID 限定查询。
假设实体属性关系为 Order.customer.account.accountId,Spring Data JPA 仓库方法可以写成:
Optional<Order> findByOrderIdAndCustomer_Account_AccountId(
String orderId,
String accountId
);
调用时,accountId 必须来自已认证的身份,不能直接用客户端提交的 customerId 或 userId 判断归属。
确认归属后,还要检查订单状态和具体操作。客户拥有订单,也不能任意修改支付状态或订单金额。
员工管理订单时,则应检查订单管理权限和订单业务规则。两类入口可以共用订单领域服务,但各自需要的授权检查都要执行。
拆分业务实体后,认证基础设施仍可共用
认证代码建议通过组合和委托复用:
identity/
Account
AccountUserDetailsService
AccountPrincipal
LoginService
TokenService
security/
SecurityConfig
JWT 校验与 authority 转换
customer/
CustomerProfile
客户资料、购物车相关服务
employee/
EmployeeProfile
StaffAssignment
StaffPolicy
EmployeeAccess
order/
product/
包结构可以按项目规模调整。共享认证模块负责验证身份、建立 principal 和处理 token,员工管理及订单归属策略放在对应业务模块中。
共享账户时
一个 AccountUserDetailsService 就足够。即使客户和员工使用不同的登录页面或登录端点,也可以委托同一个登录服务和 token 签发服务。
员工登录入口可以额外要求有效员工身份,JWT 签名、验签和过期处理代码仍可共用。
两套账户存储时
如果客户和员工有独立的账户生命周期、登录命名空间或身份来源,可以分别提供加载适配器,再统一转换为安全层使用的 principal。
认证流程必须明确选择账户域。如果同一邮箱允许同时存在于两套账户中,不能通过“先查客户表,找不到再查员工表”决定身份。可以根据明确的登录入口或认证域,选择对应的 AuthenticationManager / AuthenticationProvider。
JWT 的主体标识也要避免碰撞。可以使用全局唯一主体 ID,或结合受信任的 issuer、账户域和主体 ID 定位身份。refresh token 同样需要绑定正确的账户及认证上下文。
JWT 校验可以使用现成支持
通过 Authorization: Bearer 传递 JWT 的 API,可以使用 Spring Security 的 OAuth2 Resource Server 支持,自己签发的 JWT 也适用。它负责请求中的 Bearer Token 处理和 JWT 认证,因此客户和员工不必各写一个类似的过滤器。登录和 token 签发仍由认证服务负责。Spring Security 对自定义 JWT 的支持
假设 token 中的 roles 格式如下:
{
"sub": "全局唯一的账户ID",
"roles": ["CUSTOMER"]
}
可以统一转换为 ROLE_CUSTOMER:
@Bean
JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter authorities =
new JwtGrantedAuthoritiesConverter();
authorities.setAuthoritiesClaimName("roles");
authorities.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter converter =
new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(authorities);
return converter;
}
再将该 converter 接入资源服务器配置:
http.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(
jwtAuthenticationConverter()
)
)
);
这段配置还需要相应依赖、JwtDecoder 和完整的请求访问规则。JWT 校验应覆盖签名、时间,以及适用的 issuer、audience 约束。默认 JWT 认证下,principal 是 Jwt,不会自动转换成你的 UserDetails。需要额外资料时,可以通过主体 ID 查询。Spring Security JWT 配置文档
什么时候需要两套 SecurityFilterChain
如果客户和员工使用相同的 Bearer Token 机制和信任配置,一条过滤链配合不同路径规则、方法权限检查就可以工作,即使它们使用不同的业务实体。
后台采用独立身份提供方、不同 token audience、不同登录方式或会话策略时,再考虑多条链。配置多条链时,要明确 securityMatcher 和顺序,并提供兜底保护。请求会使用按顺序找到的第一条匹配链。Spring Security 多过滤链配置文档
实现时还需明确的约束
建议保留现有账户表和认证流程,先独立员工权限策略,再逐步迁移客户、员工资料,避免同时重写实体、登录和 token 处理。
实现时还需要处理以下约束:
- “只有一名 Super Admin”是跨账户约束。可以通过单例配置记录关联唯一管理员账户等方式实现,并在事务中处理交接。仅在创建前查询一次数量,无法防止并发创建。
- Super Admin 的管理权限也要遵守唯一性规则。删除、停用或降级唯一管理员,需要专门的处理流程。
- 员工离职时,通常应撤销其任职或后台访问资格。如果同一账户还有客户身份,应避免直接删除账户及订单关系。
- 角色、部门或账户状态变更后,已签发 JWT 中的旧权限可能继续有效。需要根据撤权时效要求,选择短期 token、在线状态校验或其他失效机制。
- 授权检查与写入之间可能发生并发变化。敏感的任职修改应在事务中校验,并按需要使用版本控制或锁。
- 列表、批量操作、导出和搜索接口也必须执行相同的数据范围限制。
针对当前规则,至少应验证:经理不能跨部门删除员工、不能创建经理、不能通过更新字段提升职位,客户不能读取其他账户的订单。
备注:内容仅供参考。
