业务系统认证集成(简化版)
适用场景:某客户业务系统希望让自家用户直接通过 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 | 实现这个方法 |
其他全不动,包括:MldongFilter、AuthServiceImpl、LoginUserHolder、工作流所有模块、sys_config、sys_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.execute 里 LoginUserHolder.getUserId().toString() 自动拿到业务方用户 ID,审批走起。
六、对工作流模块的影响
0 改动。
mldong-wf-core 引擎所有方法都以 String operator 为入参,业务方用户经 toLoginUser 产出标准 LoginUser 注入 LoginUserHolder,wf-core 里那 3 处硬编码 LoginUserHolder 的 handler/interceptor 对业务方用户同样适用。
七、接入清单(给客户那边)
- 新增
BusinessGranter.java(伪代码直接复制第 §三 节) - 新增
BusinessUserInfo.javaDTO(放granter包下) - 改
UserService.java加一行User createUserByBusiness(BusinessUserInfo info); - 改
UserServiceImpl.java加createUserByBusiness实现(伪代码直接复制第 §四 节) - 业务方提供:一个能用
userName + (password 或 mobilePhone)换用户信息的接口(HTTP/RPC/DB 任意),返回内容必须包含userName + realName + mobilePhone三个字段,把调用方式填到fetchUserInfoFromBusiness方法里
完事。
八、安全注意(接入方自己保证)
内部系统对接,场景简单,基本的安全由业务方系统自身保证:
- 网络层:走内网 / VPC,默认就是受信网络,HTTPS 不是必须
- 鉴权:调业务方接口时按需加共享密钥 Header(业务方系统给 mldong 一个 appId/appSecret,业务方校验 mldong 身份)
- 业务方用户锁定:业务方系统自己负责"密码错误 N 次锁定",mldong 这边的
User.is_locked只在管理后台手动操作
HMAC 签名这类增强措施按需加,内网环境通常不需要。
