Skip to content

User & Permission Domain - Deep Dive Analysis

1. Domain Overview

1.1 领域职责描述

User & Permission 领域是整个 Speaker Platform 的安全基础设施,负责:

  • 用户身份管理:统一用户账户(UnifiedUser)的创建、更新、激活/停用
  • 多层级认证:支持密码认证、SSO/SAML 认证、Token 认证三种方式
  • 多维度 RBAC 权限模型:用户在 Instance/Company/Product 三个维度分别拥有不同角色和权限
  • 三套独立认证流程:Plannerview (Instance)、Salesview (Product)、Speakerview (Company) 各有不同的登录和授权逻辑
  • 用户数量限制管理:按视图(SalesView/SpeakerView/PlannerView)控制活跃用户上限
  • 密码策略管理:密码历史检查、重置密码、忘记密码流程
  • SSO 双服务并存:pharmagin-sso(生产)和 pharmagin-login(待部署)两套 SSO 服务

1.2 涉及的后端模块和包

模块/包路径职责
user modulepharmagin-web/.../modules/v1/user/用户 CRUD、认证、角色管理、用户限制
securitypharmagin-web/.../common/security/认证提供者、过滤器、权限表达式
security configpharmagin-web/.../common/config/SecurityConfig.javaSpring Security 配置
persistence entitiespharmagin-web/.../common/persistence/entity/10+ 用户/角色/权限实体
pharmagin-ssopharmagin-api/pharmagin-sso/生产 SSO/SAML 2.0 服务
pharmagin-loginpharmagin-api/pharmagin-login/下一代 SSO 服务(未上线)

2. Data Model Analysis

2.1 Entity Overview Table

Entity表名主键关键字段文件位置
UnifiedUsert_unified_useruser_id (auto)username, password, legacy_password, email, first_name, last_name, office_phone, mobile_phone, fax, status(int), created_at/by, updated_at/by, last_login_dtcommon/persistence/entity/UnifiedUser.java
UserInstancet_user_instanceuser_instance_id (auto)user_id, instance_id, statuscommon/persistence/entity/UserInstance.java
UserCompanyt_user_companyuser_company_id (auto)user_id, company_id, status, product_ids(jsonb)common/persistence/entity/UserCompany.java
UserProductt_user_productuser_product_id (auto)user_id, product_id, status, sales_force_id, team_id, region_id, district_id, territory_id, employee_id, city, state, street, zip_codecommon/persistence/entity/UserProduct.java
UserRolet_user_rolerole_id (auto)role_name, display_namecommon/persistence/entity/UserRole.java
RoleGroupt_role_grouprole_group_id (auto)role_group_namecommon/persistence/entity/RoleGroup.java
Permissiont_permissionpermission_id (auto)permission_namecommon/persistence/entity/Permission.java
RolePermissiont_role_permission(role_id, permission_id) 复合主键role_id, permission_idcommon/persistence/entity/RolePermission.java
ProductRolet_product_rolerole_id (auto)product_id, role_name, display_namecommon/persistence/entity/ProductRole.java
ProductRolePermissiont_product_role_permission(role_id, permission_id) 复合主键role_id, permission_idcommon/persistence/entity/ProductRolePermission.java
UserProductRolet_user_product_role无主键user_product_id, role_idcommon/persistence/entity/UserProductRole.java
UserAuthTokent_user_auth_token(email, token) 复合主键email, tokencommon/persistence/entity/UserAuthToken.java
UserPasswordHistoryt_user_password_historyid (auto)user_id, password, legacy_password, created_atcommon/persistence/entity/UserPasswordHistory.java
UserQuantityLimitt_user_quantity_limitid (auto)object_id, type(0/1/2), users_limitcommon/persistence/entity/UserQuantityLimit.java
UserQuantityLimitChangeLogt_user_quantity_limit_change_logid (auto)object_id, type, old_value, new_value, created_at, created_bycommon/persistence/entity/UserQuantityLimitChangeLog.java
UserQuantityLimitWhitelistt_user_quantity_limit_whitelistid (auto)domaincommon/persistence/entity/UserQuantityLimitWhitelist.java

2.2 Table Relationships (ER Diagram - ASCII)

                                  +-----------------+
                                  |  t_role_group   |
                                  |-----------------|
                                  | role_group_id   |
                                  | role_group_name |
                                  +--------+--------+
                                           |
                                  t_role_group_role (隐式)
                                           |
+-------------------+            +-------------------+            +------------------+
| t_unified_user    |            |  t_user_role      |            |  t_permission    |
|-------------------|            |-------------------|            |------------------|
| user_id      [PK] |            | role_id      [PK] |            | permission_id [PK]|
| username          |            | role_name         |            | permission_name   |
| password          |            | display_name      |            +--------+---------+
| legacy_password   |            +--------+----------+                     |
| email             |                     |                                |
| first/last_name   |            t_role_permission                         |
| status            |            (role_id, permission_id) ----------------+
| ...               |
+---+----+-----+----+
    |    |     |
    |    |     +--------------------------------+
    |    |                                      |
    |    +-------------------+                  |
    |                        |                  |
+---v---------------+  +----v--------------+  +v-------------------+
| t_user_instance   |  | t_user_company    |  | t_user_product     |
|-------------------|  |-------------------|  |--------------------|
| user_instance_id  |  | user_company_id   |  | user_product_id    |
| user_id      [FK] |  | user_id      [FK] |  | user_id       [FK] |
| instance_id       |  | company_id        |  | product_id         |
| status            |  | status            |  | status             |
+-------------------+  | product_ids(jsonb)|  | sales_force_id     |
    |                   +-------------------+  | team_id            |
    |                        |                 | region_id          |
    |  t_user_instance_role  | t_user_company_role| district_id     |
    |  (隐式关联)             | (隐式关联)         | territory_id     |
    |                        |                 | employee_id        |
    |                        |                 +--------+-----------+
    |                        |                          |
    +--- 通过 Mapper 关联 t_user_role                    |
         (INSTANCE-{id}-{permission})          +--------v-----------+
                                               | t_user_product_role|
                                               |--------------------|
                                               | user_product_id    |
                                               | role_id            |
                                               +--------------------+
                                                        |
                                               +--------v----------+
                                               | t_product_role    |
                                               |-------------------|
                                               | role_id      [PK] |
                                               | product_id        |
                                               | role_name         |
                                               | display_name      |
                                               +--------+----------+
                                                        |
                                               t_product_role_permission
                                               (role_id, permission_id)
                                                        |
                                               +--------v----------+
                                               |  t_permission     |
                                               +-------------------+

+---------------------------+     +------------------------------------+
| t_user_auth_token         |     | t_user_password_history            |
|---------------------------|     |------------------------------------|
| email             [PK]    |     | id                   [PK]          |
| token             [PK]    |     | user_id              [FK]          |
+---------------------------+     | password                           |
                                  | legacy_password                    |
                                  | created_at                         |
                                  +------------------------------------+

+---------------------------+     +------------------------------------+
| t_user_quantity_limit     |     | t_user_quantity_limit_change_log   |
|---------------------------|     |------------------------------------|
| id                [PK]    |     | id                   [PK]          |
| object_id                 |     | object_id                          |
| type (0/1/2)              |     | type (0/1/2)                       |
| users_limit               |     | old_value / new_value              |
+---------------------------+     | created_at / created_by            |
                                  +------------------------------------+
+---------------------------+
| t_user_quantity_limit_    |
|   whitelist               |
|---------------------------|
| id               [PK]    |
| domain                    |
+---------------------------+

2.3 Data Model Issues

Issue D1: 双重角色体系并存(严重)

  • t_user_role + t_role_permission 是全局角色体系(Instance/Company 维度使用)
  • t_product_role + t_product_role_permission 是 Product 维度的独立角色体系
  • 两套体系共享同一张 t_permission 表,但角色定义完全独立
  • 导致权限管理代码大量重复,且容易出现不一致

Issue D2: UserProductRole 实体缺少主键

  • t_user_product_role 没有 @Id 注解,只有 user_product_idrole_id 两个字段
  • 位置:UserProductRole.java:21-28
  • 无法安全地执行单条记录的更新或删除

Issue D3: UserCompany.productIds 使用 Object 类型

  • UserCompany.java:43private Object productIds; 存储 JSONB 数据
  • 类型不安全,多处强制转换为 List<Integer>(如 UserService.java:258, 425, 737
  • 应使用具体类型如 List<Integer> 配合 JSONB TypeHandler

Issue D4: legacy_password 遗留字段

  • UnifiedUser.legacy_passwordUserPasswordHistory.legacy_password 字段仍存在
  • UserPasswordAuthenticationProvider.java:65-76 仍有明文密码比对逻辑
  • 安全隐患:遗留密码以明文存储

Issue D5: Instance/Company 角色关联表不透明

  • UserInstanceUserCompany 的角色关联不通过实体类,而是通过 Mapper 的 addRole/removeRole 方法直接操作 SQL
  • 中间表(如 t_user_instance_rolet_user_company_role)没有对应的 JPA Entity
  • 只有 UserProductRole 有实体定义

Issue D6: status 字段类型不一致

  • UnifiedUser.status 使用 Integer(0/1)
  • UserStatus enum 同时有 getValue() 返回 int 和 getStatus() 返回 String
  • DTO 层 ProductUser.statusString 类型
  • listProductUsers() 中需要做 Integer.valueOf() 转换

3. Business Flow Analysis

3.1 Core Business Flows

3.1.1 认证流程(三种方式)

方式1: 密码认证 (Plannerview/Salesview/Speakerview 通用)
==========================================================

Frontend                   AuthenticationController        UserService          UserPasswordAuthProvider
   |                              |                            |                         |
   |-- POST /v1/authenticate ---->|                            |                         |
   |   {username, password}       |                            |                         |
   |                              |-- authenticate() --------->|                         |
   |                              |                            |-- authManager.auth() -->|
   |                              |                            |   (UsernamePassword     |
   |                              |                            |    AuthToken)           |
   |                              |                            |                         |-- loadUserByUsername()
   |                              |                            |                         |   (UserPasswordUDS)
   |                              |                            |                         |   查询 UnifiedUser
   |                              |                            |                         |   加载 Instance/Product/
   |                              |                            |                         |   Company 角色
   |                              |                            |                         |-- BCrypt 验证
   |                              |                            |                         |   失败则尝试 legacy 密码
   |                              |                            |<-- AuthInfo<UnifiedUser>|
   |                              |                            |                         |
   |                              |                            |-- 构建 User DTO        |
   |                              |                            |-- 生成 accessToken      |
   |                              |                            |   (UUID, 存入 Guava Cache)|
   |                              |                            |-- 更新 last_login_dt    |
   |                              |<-- AuthInfo<User> ---------|                         |
   |                              |                            |                         |
   |                              |-- 检查 productUser 状态 -->|                         |
   |                              |   (必须在当前 product 下   |                         |
   |                              |    有 active 状态)          |                         |
   |<-- {accessToken, user, ...}--|                            |                         |
   |                              |                            |                         |
   |== 后续请求 ========================================================                  |
   |                              |                            |                         |
   |-- GET /v1/xxx  ------------->|                            |                         |
   |   Header: Authorization:     |                            |                         |
   |   Bearer {accessToken}       |                            |                         |
   |                              |                            |                         |
   |   AuthInfoPreAuthFilter 从 header 提取 token              |                         |
   |   从 Guava Cache 获取 AuthInfo                            |                         |
   |   设置为 SecurityContext.principal                         |                         |


方式2: SSO/SAML Token 认证
==========================================================

Browser            pharmagin-sso (或 pharmagin-login)      DB(t_user_auth_token)     Frontend         pharmagin-web
   |                         |                                     |                    |                    |
   |-- 访问 /sso/ ---------->|                                     |                    |                    |
   |                         |-- SAML2 Login Flow (IDP) ---------> |                    |                    |
   |                         |   (Azure AD / Okta)                 |                    |                    |
   |                         |<-- SAML Response (email) -----------|                    |                    |
   |                         |                                     |                    |                    |
   |                         |-- generateAndStoreToken(email) ---->|                    |                    |
   |                         |   生成 UUID token                   |                    |                    |
   |                         |   INSERT t_user_auth_token          |                    |                    |
   |                         |                                     |                    |                    |
   |<-- Set-Cookie: ----------|                                    |                    |                    |
   |    pharmaginAuth=        |                                    |                    |                    |
   |    {email}|{token}       |                                    |                    |                    |
   |                         |                                     |                    |                    |
   |-- Redirect to Frontend -|------------------------------------>(读 cookie)          |                    |
   |                         |                                     |                    |                    |
   |                         |                                     |-- POST /v1/auth -->|                    |
   |                         |                                     |   {username: email, |                   |
   |                         |                                     |    token: token}    |                    |
   |                         |                                     |                    |-- UserTokenAuth -->|
   |                         |                                     |                    |   Provider         |
   |                         |                                     |                    |   查找 auth_token  |
   |                         |                                     |                    |   验证 email 匹配  |
   |                         |                                     |<-- AuthInfo -------|                    |


方式3: Pre-Auth 过滤器 (每个请求)
==========================================================

Request                AuthInfoPreAuthFilter              AuthManager (Guava Cache)
   |                         |                                     |
   |-- Authorization: ------>|                                     |
   |   Bearer {token}        |                                     |
   |                         |-- getPreAuthenticatedPrincipal() -->|
   |                         |   1. 检查 External-Authorization    |
   |                         |      (RSA JWT 验证)                 |
   |                         |   2. 从 Authorization header        |
   |                         |      提取 Bearer token              |
   |                         |   3. 从 apiKey header/param 获取    |
   |                         |                                     |
   |                         |-- AuthManager.get(token) ---------->|
   |                         |   查询 Guava Cache                  |
   |                         |<-- AuthInfo or null ----------------|
   |                         |                                     |
   |                         |-- set SecurityContext.principal     |

3.1.2 Speaker 独立认证流程

Speaker Browser        Speakerview Frontend              pharmagin-web API
   |                         |                                |
   |== 注册流程 =============|================================|
   |                         |                                |
   |-- 点击 Sign Up -------->|                                |
   |                         |-- GET /v1/speakers/            |
   |                         |   activation-code?email=xxx -->|
   |                         |   (无需认证, permitAll)         |
   |                         |                                |-- 发送激活码到邮箱
   |                         |<-- 200 OK ---------------------|
   |                         |                                |
   |-- 输入激活码 ---------->|                                |
   |                         |-- POST /v1/speakers/           |
   |                         |   activation-code/validation ->|
   |                         |                                |-- 验证激活码
   |                         |<-- 200 OK ---------------------|
   |                         |                                |
   |-- 设置密码 ------------>|                                |
   |                         |-- POST /v1/speakers/signup --->|
   |                         |   {email, password}            |
   |                         |                                |-- 创建 UnifiedUser
   |                         |                                |-- 创建 UserCompany
   |                         |                                |-- 关联 Speaker 记录
   |                         |                                |
   |== 登录流程 =============|================================|
   |                         |                                |
   |-- Login {userName, pwd}>|                                |
   |                         |-- POST /v1/authenticate ------>|
   |                         |   {username, password}         |
   |                         |                                |-- 同通用认证流程
   |                         |<-- {accessToken, user} --------|
   |                         |                                |
   |                         |-- sessionStorage.set(          |
   |                         |   'accessToken', token)        |
   |                         |-- sessionStorage.set(          |
   |                         |   'user', JSON) // 含 role     |
   |                         |                                |
   |== 路由权限 =============|================================|
   |                         |                                |
   |                         |  auth.js: isAllowedPath()      |
   |                         |  根据 user.role (Admin/        |
   |                         |  Speaker/Sales) 判断路由访问权限|
   |                         |  注意:这是简单的字符串角色     |
   |                         |  不是 RBAC 权限                |

3.1.3 多层级 RBAC 模型

权限格式: {TYPE}-{ID}-{PERMISSION}

示例:
  INSTANCE-1-ADMIN               -> Plannerview 管理员
  INSTANCE-1-MEETINGS            -> Plannerview 会议管理
  INSTANCE-1-COMPLIANCE          -> Plannerview 合规管理
  PRODUCT-25-USERS               -> SalesView Product 25 用户管理
  PRODUCT-25-PRODUCT_PROGRAMS    -> SalesView Product 25 全部项目
  PRODUCT-25-REGION_PROGRAMS     -> SalesView Product 25 区域项目
  COMPANY-1-EXTENDED_USER_PROFILE -> Speakerview Company 1 扩展用户

权限构建过程:
=============

UnifiedUser
    |
    +-- UserInstance (instance_id=1)
    |       |-- UserRole (via t_user_instance_role)
    |       |       |-- Permission (via t_role_permission)
    |       |       |-- 生成: INSTANCE-1-{permissionName}
    |       |
    +-- UserCompany (company_id=X)
    |       |-- UserRole (via t_user_company_role)
    |       |       |-- Permission (via t_role_permission)
    |       |       |-- 生成: COMPANY-X-{permissionName}
    |       |
    +-- UserProduct (product_id=Y)
            |-- ProductRole (via t_user_product_role + t_product_role)
            |       |-- Permission (via t_product_role_permission)
            |       |-- 生成: PRODUCT-Y-{permissionName}

权限检查: HasPermissionExpressionHandlerRoot
==============================================
  @PreAuthorize("hasProductPermission(#productId, 'users')")
      -> 检查 authorities 中是否存在 "PRODUCT-{productId}-USERS"

  @PreAuthorize("hasInstancePermission(1, 'MULTI_MODULE_USER_ADMIN')")
      -> 检查 authorities 中是否存在 "INSTANCE-1-MULTI_MODULE_USER_ADMIN"

  @PreAuthorize("hasProductPermission(#productId, '*')")
      -> 通配符检查: 是否有任何 PRODUCT-{productId}-xxx 权限

3.1.4 SalesView 预定义角色权限

ProductRoleEnum (ProductRoleEnum.java)
======================================

SALES_ADMIN        -> USERS, BUDGET_ALLOCATION, GEOGRAPHY, REASSIGN_PROGRAM_GEOGRAPHY,
                      PROGRAMS, SCHEDULED/COMPLETED/PENDING_APPROVAL/APPROVED_PROGRAMS,
                      ALL_PROGRAMS, CALENDAR, REPORTS (多种), PRODUCT_PROGRAMS,
                      ALL_TEAMS, ALL_SERVICE_TYPES, DOCTEMPLATE_DOWNLOAD, SPEAKER_LIST,
                      VIEW_ALL_PENDING_APPROVAL_PROGRAMS

UPPER_MANAGER      -> PROGRAMS, 各状态项目, CALENDAR, REPORTS (多种),
                      PRODUCT_PROGRAMS, ALL_TEAMS, ALL_SERVICE_TYPES,
                      VIEW_ALL_PENDING_APPROVAL_PROGRAMS

REGIONAL_MANAGER   -> PROGRAMS, 各状态项目, CALENDAR, REPORTS (多种),
                      REGION_PROGRAMS, VIEW_ALL_PENDING_APPROVAL_PROGRAMS
                      (+条件: REGION_BUDGET_ALLOCATION if brandBudgetAllocationEnabled)

DISTRICT_MANAGER   -> PROGRAMS, 各状态项目, CALENDAR, REPORTS (多种),
                      DISTRICT_PROGRAMS, VIEW_ALL_PENDING_APPROVAL_PROGRAMS
                      (+条件: DISTRICT_BUDGET_ALLOCATION if brandBudgetAllocationEnabled)

SALES_PERSON       -> PROGRAMS, SCHEDULED_PROGRAMS, COMPLETED_PROGRAMS, CALENDAR

PLANNER            -> PROGRAMS, ALL_PROGRAMS, CALENDAR, DOCTEMPLATE_DOWNLOAD

数据可见性层级:
  SALES_ADMIN / UPPER_MANAGER -> PRODUCT_PROGRAMS (全产品线)
  REGIONAL_MANAGER             -> REGION_PROGRAMS (区域)
  DISTRICT_MANAGER             -> DISTRICT_PROGRAMS (地区)
  SALES_PERSON                 -> 仅本人数据

3.2 Validation Rules

规则位置描述
用户名唯一UserService.java:288-291创建用户时检查 username 是否已存在
用户名更新唯一性UserService.java:390-394更新时捕获 DuplicateKeyException
密码历史检查UserService.java:1025-1039根据配置 passwordHistoryCheck 检查最近 N 个密码是否重复
用户数量限制UsersLimitService.java:279-286激活用户前检查是否超过配额
域名白名单UsersLimitService.java:263-277白名单域名的用户不受数量限制
Product 用户激活状态检查AuthenticationController.java:48-61登录时验证用户在当前 product 下是否 active
产品切换AuthenticationController.java:52-54如果 switchProductEnabled,允许在任意 active product 下登录
忘记密码链接有效期UserService.java:151-15424 小时 Guava Cache 过期
登录请求验证UserLoginRequest.java:9username 为 @NotBlank,password 和 token 可选(二选一)
创建用户验证CreateUserRequest.java:10-15username, password, email 均 @NotBlank
ProductUser 创建验证CreateProductUserRequest.javausername, password, firstName, lastName, email 均 @NotBlank

3.3 Business Logic Issues

Issue B1: Token 存储使用 Guava Cache 而非 JWT(严重)

  • AuthManager.java:27-28:使用 CacheBuilder.expireAfterAccess(2, TimeUnit.HOURS) 存储 token
  • Token 是随机 UUID,不是 JWT,没有自描述性
  • 代码注释 UserService.java:159-160 明确标注这是临时方案:TODO this is a temporary solution until we can convert all of pharamgin-web to oauth
  • 问题:服务重启后所有用户需要重新登录;无法做水平扩展(内存级缓存不共享)

Issue B2: SSO Token 未及时清理(严重)

  • UserTokenAuthenticationProvider.java:47-51:注释掉了 token 删除逻辑
  • 原因注释:FIXME: temp solution, it seems the token was deleted by salesview. when iPad get the token from Cookie then login, the token can't be found
  • 结果:t_user_auth_token 表中 token 会无限积累

Issue B3: SSO Cookie 安全设置缺失(严重)

  • CustomAuthenticationSuccessHandler.java:161-162HttpOnlySecure 均被注释掉
  • RedirectAuthController.java:75-76(pharmagin-login):Cookie maxAge 仅 15 秒
  • Cookie 值格式:email|token,以 URL 编码传输

Issue B4: Legacy 密码明文存储(安全)

  • UserPasswordAuthenticationProvider.java:66-67StringUtils.equals(user.getLegacyPassword(), (String)auth.getCredentials())
  • 明文密码直接比对
  • 转换后虽然会清除 legacy_password,但历史表中 legacy_password 字段仍然保留

Issue B5: 权限信息快照不更新

  • 认证时将用户全部角色和权限加载到 AuthInfo 中,缓存 2 小时
  • 如果管理员修改了用户权限,用户需要重新登录才能生效
  • AuthInfoUserDetailsService.java:44-48:每次请求从缓存的 AuthInfo 构建 UserDetails
  • 没有权限变更通知机制

Issue B6: Speakerview 的角色模型过于简单

  • speakerview/legacy/src/utils/auth.js:14-58:使用硬编码的字符串角色('Admin', 'Speaker', 'Sales')
  • 与后端 RBAC 模型不一致,前端路由权限控制是简单的 includes 检查
  • 没有使用后端返回的 authorities 列表

Issue B7: External-Authorization 过于宽松

  • AuthInfoPreAuthFilter.java:45-49:仅验证 JWT 签名有效,不检查任何 claim(subject, audience, expiry)
  • 成功后直接创建一个 accessToken="system" 的 AuthInfo
  • AuthInfoUserDetailsService.java:35-37:system token 绕过所有权限检查,赋予 SYSTEM 权限

Issue B8: forgetPassword 使用 Guava Cache 存储 UUID

  • UserService.java:151-154, 979:重置密码链接使用内存 Cache
  • 服务重启后所有未使用的重置链接失效
  • 与认证 token 相同的问题:不支持多实例部署

4. API Inventory

4.1 REST Endpoints Table

AuthenticationController (v1/authenticate)

MethodPath权限描述文件:行
POST/v1/authenticatepermitAll用户登录(密码或 SSO token)AuthenticationController.java:43-64
GET/v1/authenticate/logout用户登出(仅清 session)AuthenticationController.java:67-74

UserController (v1/users)

MethodPath权限描述文件:行
GET/v1/users/{username}authenticated按用户名获取用户UserController.java:37-40
GET/v1/usersauthenticated分页列表用户UserController.java:43-46
POST/v1/usersauthenticated创建新用户UserController.java:49-52
PUT/v1/users/{username}authenticated更新用户信息UserController.java:55-58
PUT/v1/users/{username}/activeauthenticated激活用户UserController.java:61-64
PUT/v1/users/{username}/deactiveauthenticated停用用户UserController.java:67-70
PUT/v1/users/{username}/passwordauthenticated更新密码UserController.java:73-76
POST/v1/users/{username}/instancesauthenticated添加 Instance 用户UserController.java:79-82
POST/v1/users/{username}/companiesauthenticated添加 Company 用户UserController.java:85-88
PUT/v1/users/{username}/companies/{companyId}/activateauthenticated激活 Company 用户UserController.java:91-94
PUT/v1/users/{username}/companies/{companyId}/deactivateauthenticated停用 Company 用户UserController.java:96-100
POST/v1/users/{username}/productsauthenticated添加 Product 用户UserController.java:103-106
PUT/v1/users/{username}/instancesauthenticated更新 Instance 用户UserController.java:109-112
PUT/v1/users/{username}/companiesauthenticated更新 Company 用户UserController.java:115-118
PUT/v1/users/{username}/productsauthenticated更新 Product 用户UserController.java:121-124
GET/v1/users/{username}/products/{productId}authenticated获取 Product 用户UserController.java:127-130
PUT/v1/users/{username}/instances/{instanceId}/roles/{roleName}authenticated添加 Instance 角色UserController.java:133-136
PUT/v1/users/{username}/companies/{companyId}/roles/{roleName}authenticated添加 Company 角色UserController.java:139-142
PUT/v1/users/{username}/products/{productId}/roles/{roleName}authenticated添加 Product 角色UserController.java:145-148
DELETE/v1/users/{username}/instances/{instanceId}/roles/{roleName}authenticated移除 Instance 角色UserController.java:151-154
DELETE/v1/users/{username}/companies/{companyId}/roles/{roleName}authenticated移除 Company 角色UserController.java:157-160
DELETE/v1/users/{username}/products/{productId}/roles/{roleName}authenticated移除 Product 角色UserController.java:163-166

ProductUserController (v1/users/products)

MethodPath权限描述文件:行
GET/v1/users/products/{productId}hasProductPermission(users) OR hasInstancePermission(1, MULTI_MODULE_USER_ADMIN)列表 Product 用户ProductUserController.java:44-48
GET/v1/users/products/{productId}/subordinateshasProductPermission(*, *)获取下属用户ProductUserController.java:51-55
POST/v1/users/products/{productId}hasProductPermission(users) OR ...创建 Product 用户ProductUserController.java:58-62
PUT/v1/users/products/{username}/{productId}hasProductPermission(users) OR ...更新 Product 用户ProductUserController.java:65-70
GET/v1/users/products/{username}/{productId}hasProductPermission(users) OR ...获取 Product 用户详情ProductUserController.java:73-77
PUT/v1/users/products/{username}/{productId}/activehasProductPermission(users) OR ...激活 Product 用户ProductUserController.java:80-84
PUT/v1/users/products/{username}/{productId}/deactivehasProductPermission(users) OR ...停用 Product 用户ProductUserController.java:87-91
POST/v1/users/products/{productId}/uploadhasProductPermission(users) OR ...批量上传 Product 用户ProductUserController.java:94-98
GET/v1/users/products/salesforce-membersauthenticated查询 Salesforce 成员ProductUserController.java:100-103
GET/v1/users/products/{username}/forget-passwordpermitAll忘记密码(发送邮件)ProductUserController.java:106-109
PUT/v1/users/products/reset-passwordpermitAll重置密码(通过 UUID)ProductUserController.java:111-114
PUT/v1/users/products/change-password#request.userId == principal.user.userId修改密码(当前用户)ProductUserController.java:117-122
GET/v1/users/products/{productId}/meetingsauthenticated查询用户的 SalesMeetingsProductUserController.java:124-127

RoleController (v1/)

MethodPath权限描述文件:行
GET/v1/roles/authenticated获取所有角色RoleController.java:39-42
GET/v1/permissions/authenticated获取所有权限RoleController.java:45-48
GET/v1/roleGroups/{roleGroupName}/rolesauthenticated按角色组获取角色RoleController.java:52-55
POST/v1/roles/authenticated创建角色RoleController.java:58-61
POST/v1/permissions/authenticated创建权限RoleController.java:64-67
PUT/v1/roles/{roleName}/permissions/{permissionName}authenticated为角色添加单个权限RoleController.java:70-73
POST/v1/roles/{roleName}/permissionsauthenticated批量分配权限RoleController.java:74-77

ProductRoleController (v1/products/{productId})

MethodPath权限描述文件:行
GET/v1/products/{productId}/rolesauthenticated列表 Product 角色ProductRoleController.java:36-39
POST/v1/products/{productId}/rolesauthenticated创建 Product 角色ProductRoleController.java:42-45
POST/v1/products/{productId}/roles/initializationauthenticated初始化默认角色模板ProductRoleController.java:48-51
PUT/v1/products/{productId}/roles/{roleName}authenticated更新 Product 角色ProductRoleController.java:54-57
DELETE/v1/products/{productId}/roles/{roleName}authenticated删除 Product 角色ProductRoleController.java:60-63
GET/v1/products/{productId}/roles/{roleName}/permissionsauthenticated获取角色权限ProductRoleController.java:66-69
POST/v1/products/{productId}/roles/{roleName}/permissionsauthenticated更新角色权限ProductRoleController.java:72-75
GET/v1/products/{productId}/permissionsauthenticated列表所有权限ProductRoleController.java:78-81

TestPermissionsController (v1/test/permissions)

MethodPath权限描述文件:行
GET/v1/test/permissions/instance/{instanceId}hasInstancePermission(read)测试 Instance 权限TestPermissionsController.java:26-30
GET/v1/test/permissions/product/{productId}hasProductPermission(read)测试 Product 权限TestPermissionsController.java:33-37
GET/v1/test/permissions/company/{companyId}hasCompanyPermission(read)测试 Company 权限TestPermissionsController.java:40-47

UsersLimitController (v1/users-limit)

MethodPath权限描述文件:行
GET/v1/users-limitauthenticated列表用户限制UsersLimitController.java:22-25
POST/v1/users-limitauthenticated保存用户限制UsersLimitController.java:27-30
GET/v1/users-limit/{type}/{objectId}authenticated检查用户限制UsersLimitController.java:32-36
POST/v1/users-limit/whitelistauthenticated保存白名单UsersLimitController.java:38-41

4.2 API Design Issues

Issue A1: UserController 缺少权限控制(严重)

  • UserController 的所有 18 个端点仅需 authenticated,无细粒度权限检查
  • 任何登录用户都可以创建/修改/激活/停用其他用户
  • 对比 ProductUserController@PreAuthorize 注解

Issue A2: RoleController 和 ProductRoleController 缺少权限控制

  • 角色和权限的管理 API 没有 @PreAuthorize 注解
  • 任何认证用户可以创建角色、分配权限

Issue A3: API 路径设计不一致

  • UserController: v1/users/{username}/products/{productId}
  • ProductUserController: v1/users/products/{username}/{productId}
  • 两个 Controller 有重叠的功能(如 getProductUser

Issue A4: RESTful 命名不规范

  • /v1/users/{username}/active/v1/users/{username}/deactive 应使用 PATCH 或更 RESTful 的设计
  • "deactive" 应为 "deactivate"(拼写错误)

Issue A5: TestPermissionsController 暴露在生产环境

  • v1/test/permissions/* 是测试端点,不应部署到生产环境

Issue A6: Swagger 注解中 httpMethod 冗余

  • @ApiOperation(value = "xxx", httpMethod = "GET")@GetMapping 重复定义

5. Frontend Analysis

5.1 Pages & Components

Plannerview (Admin User Management)

Component路径功能
Loginpharmagin-plannerview/legacy/src/components/Login/index.js密码登录页,含 "Forget Password"
User (Admin)pharmagin-plannerview/legacy/src/components/User/index.js用户管理列表(CRUD),含状态切换和密码修改
UserFormpharmagin-plannerview/legacy/src/components/User/UserForm.js用户创建/编辑表单
PasswordFormpharmagin-plannerview/legacy/src/components/User/PasswordForm.js修改密码表单
UsersLimitpharmagin-plannerview/legacy/src/components/UsersLimit/index.js用户数量限制配置
UsersLimitFormpharmagin-plannerview/legacy/src/components/UsersLimit/UsersLimitForm.js限制设置表单
UsersLimitWhitelistFormpharmagin-plannerview/legacy/src/components/UsersLimit/UsersLimitWhitelistForm.js域名白名单配置
ForgetPwdpharmagin-plannerview/legacy/src/components/ForgetPwd/忘记密码组件
auth.jspharmagin-plannerview/legacy/src/utils/auth.js登录状态和路由权限检查

Salesview (Product User Management)

Component路径功能
Loginpharmagin-salesview/src/pages/Login/index.js密码/SSO 登录页
Login sagapharmagin-salesview/src/pages/Login/saga.js调用 /api/v1/authenticate
Login Footerpharmagin-salesview/src/components/Footer/LoginFooter.js登录页底部
Users Listpharmagin-salesview/src/pages/Users/List/index.jsProduct 用户管理
ExternalContainerpharmagin-salesview/src/pages/ExternalContainer/saga.jsSSO 入口(读取 cookie token)
ForgotPwdpharmagin-salesview/src/pages/Login/ForgotPwd.js忘记密码

Speakerview (Speaker Auth)

Component路径功能
Loginpharmagin-speakerview/legacy/src/components/Login/index.js登录页(含 Sign Up 和 Forgot Password)
SignUppharmagin-speakerview/legacy/src/components/Login/SignUp.jsSpeaker 自注册流程(3步)
SignUpEmailpharmagin-speakerview/legacy/src/components/Login/SignUpEmail.js步骤1:输入邮箱获取激活码
SignUpActivationCodepharmagin-speakerview/legacy/src/components/Login/SignUpActivationCode.js步骤2:验证激活码
SignUpPasswordpharmagin-speakerview/legacy/src/components/Login/SignUpPassword.js步骤3:设置密码
ForgotPwdpharmagin-speakerview/legacy/src/components/Login/ForgotPwd.js忘记密码
auth.jspharmagin-speakerview/legacy/src/utils/auth.js角色路由权限(Admin/Speaker/Sales)

5.2 Redux State Structure

Plannerview

javascript
// store structure
{
  user: {
    loading: false,       // 加载状态
    error: '',            // 错误信息
    users: {              // 用户列表 (分页)
      list: [],
      pagination: {}
    },
    success: false
  }
}

// sessionStorage
sessionStorage.accessToken    // UUID token
sessionStorage.authorities    // JSON 字符串 ["INSTANCE-1-MEETINGS", ...]

Salesview

javascript
// Login saga 登录后存储
sessionStorage.accessToken    // UUID token
// user, productId, authorities 通过 Redux action 传入 App state

// ExternalContainer (SSO) 硬编码测试数据 (!)
// pharmagin-salesview/src/pages/ExternalContainer/saga.js
// 包含完整的 mock user/authorities 数据

Speakerview

javascript
// sessionStorage
sessionStorage.accessToken    // UUID token
sessionStorage.user           // JSON {role: 'Admin'|'Speaker'|'Sales', speakerId: ...}
sessionStorage.nav            // 导航状态
sessionStorage.opened         // 是否已打开

5.3 Frontend Issues

Issue F1: Plannerview 权限检查使用硬编码字符串

  • auth.js:16: authorities.includes('INSTANCE-1-MEETINGS') - 硬编码 instanceId=1
  • 如果有多 instance 部署(虽然当前不存在),这些权限检查全部失效
  • 路由名称和权限之间的映射是手动 switch-case

Issue F2: Speakerview 角色权限模型与后端完全不同

  • 后端使用 COMPANY-{id}-{permission} 格式的 authorities
  • 前端 auth.js 使用简单字符串 role ('Admin', 'Speaker', 'Sales')
  • 两套逻辑不一致,前端未使用后端返回的 authorities 列表

Issue F3: ExternalContainer (SSO) saga 包含硬编码 mock 数据

  • pharmagin-salesview/src/pages/ExternalContainer/saga.js:7-133
  • SSO 流程完全被 mock 数据替代,实际 API 调用被注释掉
  • 生产环境依赖 cookie 中的 token 调用 /v1/authenticate

Issue F4: 所有三个前端使用 sessionStorage 存储 accessToken

  • sessionStorage 不跨标签页共享
  • 每开一个新标签页需要重新登录
  • 安全性问题:XSS 攻击可直接读取 sessionStorage

Issue F5: 前端密码明文传输

  • 登录请求直接通过 HTTP POST 传递明文密码到 /v1/authenticate
  • 虽然生产环境有 HTTPS(Nginx),但开发环境可能裸跑 HTTP

6. Problem Summary

6.1 Critical Issues (must fix in rewrite)

#问题影响相关代码
C1Token 使用内存 Guava Cache,非 JWT不支持水平扩展,重启丢失所有会话AuthManager.java:27-28
C2UserController 18 个端点完全无权限控制任何登录用户可 CRUD 任何其他用户UserController.java 全文件
C3SSO Token 不删除导致数据积累t_user_auth_token 表无限增长UserTokenAuthenticationProvider.java:47-51
C4SSO Cookie 缺少 HttpOnly/SecureXSS 可窃取认证 cookieCustomAuthenticationSuccessHandler.java:161-162
C5Legacy 密码明文比对安全隐患UserPasswordAuthenticationProvider.java:66-67
C6External-Authorization 无 claim 验证JWT 签名有效即获 SYSTEM 权限AuthInfoPreAuthFilter.java:78-99
C7RoleController 角色/权限管理无权限控制任何用户可创建角色、分配权限RoleController.java 全文件
C8双重角色体系(UserRole + ProductRole)并行增加系统复杂度,权限模型不统一对比 t_user_role vs t_product_role

6.2 Design Defects (should improve)

#问题影响相关代码
D1权限信息缓存不更新管理员修改权限后不能实时生效AuthInfoUserDetailsService.java
D2Speakerview 前端角色模型与后端不一致前端权限控制不完整speakerview/legacy/src/utils/auth.js
D3forgetPassword 使用内存 Cache重启丢失所有未用重置链接UserService.java:151-154
D4UserCompany.productIds 使用 Object 类型类型不安全,多处强转UserCompany.java:43
D5SSO 双服务并存维护成本高,代码重复pharmagin-sso/ vs pharmagin-login/
D6API 路径设计不一致学习成本高,容易混淆UserController vs ProductUserController
D7UserProductRole 缺少主键无法安全操作单条记录UserProductRole.java:21-28
D8Instance/Company 角色中间表无 Entity关联逻辑全在 Mapper SQL 中UserInstanceMapper.addRole()
D9用户限制检查效率低每次都查全表计数再比较UsersLimitService.java:144-182
D10三个前端 Login 组件各自实现代码重复,行为不一致三个 Login/index.js

6.3 Technical Debt (nice to have)

#问题影响相关代码
T1TestPermissionsController 应移除测试代码不应在生产环境TestPermissionsController.java
T2"deactive" 拼写错误API 命名不规范多处 endpoint 路径
T3ExternalContainer saga 包含大量 mock 数据开发遗留物ExternalContainer/saga.js
T4status 字段 Integer/String 混用在 DTO 层和 Entity 层来回转换UserStatus enum
T5UserService 过于庞大 (1109 行)单一职责原则违反UserService.java
T6Swagger httpMethod 冗余定义代码噪音所有 Controller 的 @ApiOperation
T7Guava Cache 容量固定 initialCapacity(50)用户多时可能频繁淘汰AuthManager.java:27
T8ProductRoleEnum 硬编码默认权限修改默认权限需改代码重新部署ProductRoleEnum.java
T9sessionStorage 不跨标签页用户体验差三个前端的 auth 处理
T10UserMapper 注入两次userMapperunifiedUserMapper 相同UserService.java:97, 118

7. Rewrite Recommendations

7.1 认证系统重构

  1. 使用标准 JWT(JSON Web Token)替代 Guava Cache UUID

    • 生成 RS256 签名的 JWT,包含 userId, username, authorities, exp 等 claims
    • 支持无状态认证,可水平扩展
    • 使用 Refresh Token 机制延长会话
  2. 统一 SSO 服务

    • 合并 pharmagin-sso 和 pharmagin-login 为单一服务
    • 使用 Spring Security SAML2(5.x+),支持多 IDP 配置
    • SSO 成功后直接签发 JWT,而非依赖 Cookie + Token 中转
  3. 移除 Legacy 密码机制

    • 运行一次性迁移脚本,清除所有 legacy_password 字段
    • 未迁移用户使用 "忘记密码" 流程重新设置

7.2 权限模型统一

  1. 统一为单一 RBAC 模型

    • 合并 t_user_rolet_product_role 为统一的角色表
    • 角色可以是 scope-specific(Instance/Company/Product),通过 scope 字段区分
    • 消除双重角色体系的复杂性
  2. 所有 API 端点添加权限检查

    • UserController 所有端点添加 @PreAuthorize
    • RoleControllerProductRoleController 添加管理员权限检查
    • 使用统一的权限前缀 ADMIN:USER_MANAGEMENT
  3. 权限变更实时生效

    • 使用短有效期的 Access Token(如 15 分钟) + 长有效期的 Refresh Token
    • 或引入 Redis 存储权限快照,管理员修改后清除对应缓存

7.3 前端统一

  1. 统一认证 SDK

    • 提取公共 auth 模块,三个前端共享
    • 使用 httpOnly Cookie 存储 JWT,而非 sessionStorage
    • 实现自动 Token 刷新机制
  2. 统一 Speakerview 权限模型

    • 前端使用后端返回的 authorities 列表进行权限检查
    • 移除硬编码的 'Admin'/'Speaker'/'Sales' 字符串角色
  3. 密码重置链接使用数据库存储

    • 将重置 UUID 存储在数据库中,包含过期时间
    • 支持多实例部署

7.4 安全加固

  1. SSO Cookie 启用 HttpOnly=true, Secure=true, SameSite=Strict
  2. External-Authorization JWT 验证添加 audience, issuer, expiration 检查
  3. 密码重置链接使用加密 token 而非简单 UUID
  4. 登录接口添加 rate limiting 防暴力破解
  5. 移除 TestPermissionsController
  6. 用户密码历史应只存储 BCrypt hash,清除所有 legacy_password 记录