业务系统认证集成(简化版)

适用场景:某客户业务系统希望让自家用户直接通过 mldong 工作流模块审批流程 改造量:1 个 Granter + 1 个 Service 方法,其他 0 改动 工作流模块:0 改动


一、思路(一句话)

新增一个 BusinessGranter implements ILoginGranter,业务方登录时传 userName + (password 或 mobilePhone),granter 调业务方接口验证并拿到用户信息(必须包含 userName + realName + mobilePhone),查到本地用户就返回、查不到就建一个,然后走标准 toLoginUser 流程产出 LoginUser,工作流模块无感使用

关键认知:业务方用户不会真正登录 mldong 系统。他们的身份验证完全发生在业务方那边,mldong 这里只是给一个"token"以方便后续调用 mldong 接口。所以 sys_user.password 字段对业务方用户来说是一个永远不会被读取的占位符——填什么都行,mldong 永远不会用它做密码校验。


二、改造内容(就两个文件)

文件改动
mldong-core/mldong-sys-core/src/main/java/com/mldong/modules/sys/auth/granter/BusinessGranter.java新建
mldong-core/mldong-sys-core/src/main/java/com/mldong/modules/sys/service/UserService.java新增 1 个方法 createUserByBusiness(BusinessUserInfo)
mldong-core/mldong-sys-core/src/main/java/com/mldong/modules/sys/service/impl/UserServiceImpl.java实现这个方法

其他全不动,包括:MldongFilterAuthServiceImplLoginUserHolder、工作流所有模块、sys_configsys_user 表结构、菜单枚举。


三、BusinessGranter 伪代码

路径:mldong-core/mldong-sys-core/src/main/java/com/mldong/modules/sys/auth/granter/BusinessGranter.java

package com.mldong.modules.sys.auth.granter;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
import com.mldong.auth.ILoginGranter;
import com.mldong.auth.err.AuthErrEnum;
import com.mldong.base.YesNoEnum;
import com.mldong.exception.ServiceException;
import com.mldong.modules.sys.entity.User;
import com.mldong.modules.sys.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

/**
 * 业务方账号授权登录
 * <p>
 * Bean 名:businessGranter
 * grantType:business
 * <p>
 * 登录参数(password 和 mobilePhone 至少传一个):
 *   - userName       业务方用户名(必填)
 *   - password       业务方密码(跟 mobilePhone 至少传一个)
 *   - mobilePhone    业务方手机号(跟 password 至少传一个,适合没有密码的场景)
 *   - businessSystem 业务方系统编码(可选,比如 "ERP")
 *
 * 接入方需要做的:
 *   1. 实现 fetchUserInfoFromBusiness():调业务方接口,返回用户信息(userName/realName/mobilePhone 必返)
 *   2. 实现 UserService.createUserByBusiness():把业务方用户信息存到 mldong 本地
 *
 * @author mldong
 */
@Component
@RequiredArgsConstructor
public class BusinessGranter implements ILoginGranter {

    private final UserService userService;

    @Override
    public Dict grant(Dict param) {
        String userName = param.getStr("userName");
        String password = param.getStr("password");
        String mobilePhone = param.getStr("mobilePhone");
        String businessSystem = param.getStr("businessSystem");

        // userName 必填,password / mobilePhone 至少传一个
        if (StrUtil.isEmpty(userName)
                || (StrUtil.isEmpty(password) && StrUtil.isEmpty(mobilePhone))) {
            ServiceException.throwBiz(AuthErrEnum.USER_NOT_EXIST);
        }

        // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 接入方实现 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
        // 1. 调业务方接口,验证并换取用户信息
        //    比如:HTTP POST 业务方 /api/auth/verify
        //    请求:{userName, password?, mobilePhone?, businessSystem}
        //    响应:{code:0, data:{userName, realName, mobilePhone, deptCode, roleCodes, ...}}
        //    验证失败时返回 null
        //    业务方自己决定用 password 还是 mobilePhone 做匹配(常见做法:
        //      - 有 password:userName+password 精确匹配
        //      - 没 password:userName+mobilePhone 精确匹配,适合初次接入或无密码场景)
        BusinessUserInfo info = fetchUserInfoFromBusiness(userName, password, mobilePhone, businessSystem);
        if (info == null) {
            ServiceException.throwBiz(AuthErrEnum.USER_NOT_EXIST);
        }
        // ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑

        // 2. 查/建本地用户
        User user = userService.getByUserName(info.getUserName());
        if (user == null) {
            // 本地没有 → 首次登录,自动建账号
            user = userService.createUserByBusiness(info);
        }
        // 本地已有 → 直接用(密码已在第 1 步业务方校验过)

        // 3. 锁定校验
        if (YesNoEnum.YES.getCode().equals(user.getIsLocked())) {
            ServiceException.throwBiz(AuthErrEnum.USER_IS_LOCKED);
        }

        // 4. 返回标准 User Dict,后续由 AuthServiceImpl.toLoginUser() 处理
        return BeanUtil.toBean(user, Dict.class);
    }

    /**
     * 调业务方"用户名+凭证(密码或手机号)换用户信息"接口
     * <p>
     * 接入方自己实现,可以是 HTTP / RPC / 数据库直查,任意方式
     * <p>
     * 业务方接口约定(参考):
     *   POST {业务方URL}/api/auth/verify
     *   body: {"userName":"zhangsan", "password":"xxx"}    // 或 mobilePhone
     *   resp: {
     *     "code": 0,
     *     "data": {
     *       "userName": "zhangsan",       // ← 必返
     *       "realName": "张三",            // ← 必返(mldong User.realName NOT NULL)
     *       "mobilePhone": "13800138000",  // ← 必返(mldong User.mobilePhone NOT NULL)
     *       "deptCode": "TECH",
     *       "roleCodes": ["wf_approver"]
     *     }
     *   }
     */
    private BusinessUserInfo fetchUserInfoFromBusiness(
            String userName,
            String password,      // 可空
            String mobilePhone,   // 可空
            String businessSystem) {
        // ↓↓↓ 接入方实现 ↓↓↓
        // 方式 A:HTTP 调用业务方接口
        //   String url = "http://erp.example.com/api/auth/verify";
        //   String resp = HttpRequest.post(url).body(...).execute().body();
        //   ... 解析 resp 失败返回 null
        //
        // 方式 B:对方给你开放了 DB 视图,直接查
        //   UserInfoPO po = bizUserDao.selectByAccount(userName, password);
        //   if (po == null) return null;
        //
        // 方式 C:对方给你 RPC 接口
        //   return erpRpcService.verifyAndGetUser(userName, password);
        // ↑↑↑ 接入方实现 ↑↑↑
        return null;
    }
}

BusinessUserInfo DTO(接入方自己定义)

@Data
public class BusinessUserInfo {
    // ↓↓↓ 下面三个是 mldong sys_user 的 NOT NULL 字段,业务方接口必须返回 ↓↓↓
    private String userName;        // 用于登录的用户名(必填)
    private String realName;        // 真实姓名(必填)
    private String mobilePhone;     // 手机号(必填)
    // ↑↑↑ 上面三个是 mldong sys_user 的 NOT NULL 字段,业务方接口必须返回 ↑↑↑

    private String email;           // 邮箱(可选)
    private String deptCode;        // 部门编码(可选)
    private List<String> roleCodes; // 角色编码列表(可选)
}

四、UserService.createUserByBusiness 伪代码

// 接口新增
public interface UserService extends IService<User> {
    // ... 原有方法 ...
    User createUserByBusiness(BusinessUserInfo info);
}

// 实现
@Override
@Transactional(rollbackFor = Exception.class)
public User createUserByBusiness(BusinessUserInfo info) {
    // ↓↓↓ 接入方实现 ↓↓↓

    // 1. 处理部门(可选):有 deptCode 就 upsert 一个部门
    Long deptId = null;
    if (StrUtil.isNotEmpty(info.getDeptCode())) {
        Dept dept = deptMapper.selectOne(
            Wrappers.lambdaQuery(Dept.class).eq(Dept::getCode, info.getDeptCode()));
        if (dept == null) {
            dept = new Dept();
            dept.setCode(info.getDeptCode());
            dept.setName(info.getDeptCode());
            dept.setEnabled(YesNoEnum.YES.getCode());
            deptMapper.insert(dept);
        }
        deptId = dept.getId();
    }

    // 2. 创建用户(复用现有 UserParam + createBaseUser,自动加盐 MD5)
    //    userName / realName / mobilePhone 是 mldong sys_user NOT NULL 字段,业务方必须提供
    UserParam param = new UserParam();
    param.setUserName(info.getUserName());
    param.setRealName(info.getRealName());         // 必填
    param.setMobilePhone(info.getMobilePhone());   // 必填
    param.setEmail(info.getEmail());
    param.setDeptId(deptId);
    // 密码占位:业务方用户不会真正登录 mldong,这个密码永远不会被校验,填啥都行
    param.setPassword("PLACEHOLDER_NO_LOGIN");
    param.setAdminType(AdminTypeEnum.COMMON_ADMIN.getCode());
    User user = createBaseUser(param, true); // ← 复用,内部已经处理 salt + MD5

    // 3. 角色授权(可选):有 roleCodes 就绑角色
    if (CollectionUtil.isNotEmpty(info.getRoleCodes())) {
        List<Role> roles = roleMapper.selectList(
            Wrappers.lambdaQuery(Role.class).in(Role::getCode, info.getRoleCodes()));
        if (CollectionUtil.isNotEmpty(roles)) {
            grantRole(user.getId(), roles.stream().map(Role::getId).collect(Collectors.toList()));
        }
    }

    return user;
    // ↑↑↑ 接入方实现 ↑↑↑
}

接入方只需要在这个方法里写"业务方字段 → mldong 字段"的映射,其他安全、加密、登录态全都不用管。


五、调用方式

业务方调 mldong 登录:

POST /sys/login
Content-Type: application/json

# 方式 A:用户名 + 密码
{
  "grantType": "business",
  "userName": "zhangsan",
  "password": "xxx",
  "businessSystem": "ERP"
}

# 方式 B:用户名 + 手机号(适合没密码或首次接入)
{
  "grantType": "business",
  "userName": "zhangsan",
  "mobilePhone": "13800138000",
  "businessSystem": "ERP"
}

返回:

{ "code": 0, "msg": "成功", "data": { "userId": 10086, "token": "eyJ..." } }

业务方拿 token 调工作流审批,和 mldong 原生用户完全一样:

POST /wf/processTask/execute
Authorization: Bearer eyJ...
Content-Type: application/json

{
  "processTaskId": 12345,
  "submitType": "agree"
}

ProcessTaskController.executeLoginUserHolder.getUserId().toString() 自动拿到业务方用户 ID,审批走起。


六、对工作流模块的影响

0 改动

mldong-wf-core 引擎所有方法都以 String operator 为入参,业务方用户经 toLoginUser 产出标准 LoginUser 注入 LoginUserHolder,wf-core 里那 3 处硬编码 LoginUserHolder 的 handler/interceptor 对业务方用户同样适用。


七、接入清单(给客户那边)

  1. 新增 BusinessGranter.java(伪代码直接复制第 §三 节)
  2. 新增 BusinessUserInfo.java DTO(放 granter 包下)
  3. UserService.java 加一行 User createUserByBusiness(BusinessUserInfo info);
  4. UserServiceImpl.javacreateUserByBusiness 实现(伪代码直接复制第 §四 节)
  5. 业务方提供:一个能用 userName + (password 或 mobilePhone) 换用户信息的接口(HTTP/RPC/DB 任意),返回内容必须包含 userName + realName + mobilePhone 三个字段,把调用方式填到 fetchUserInfoFromBusiness 方法里

完事。


八、安全注意(接入方自己保证)

内部系统对接,场景简单,基本的安全由业务方系统自身保证:

  • 网络层:走内网 / VPC,默认就是受信网络,HTTPS 不是必须
  • 鉴权:调业务方接口时按需加共享密钥 Header(业务方系统给 mldong 一个 appId/appSecret,业务方校验 mldong 身份)
  • 业务方用户锁定:业务方系统自己负责"密码错误 N 次锁定",mldong 这边的 User.is_locked 只在管理后台手动操作

HMAC 签名这类增强措施按需加,内网环境通常不需要。