✨ 小程序用户独立为 blade_mp_user,并兼容司机/车辆 OCR 宽松日期

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-12 13:02:08 +08:00
parent 42ca5ee7bc
commit e19c3b100a
34 changed files with 1892 additions and 170 deletions
@@ -51,6 +51,7 @@ import org.springblade.core.oauth2.service.OAuth2UserService;
import org.springblade.system.feign.IAuthLockClient;
import org.springblade.system.feign.IAuthLogClient;
import org.springblade.core.tenant.BladeTenantProperties;
import org.springblade.system.feign.IMpUserClient;
import org.springblade.system.feign.IUserClient;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -117,8 +118,8 @@ public class BladeAuthConfiguration {
}
@Bean
public OAuth2UserService oAuth2UserService(IUserClient userClient) {
return new BladeUserDetailService(userClient);
public OAuth2UserService oAuth2UserService(IUserClient userClient, IMpUserClient mpUserClient) {
return new BladeUserDetailService(userClient, mpUserClient);
}
@Primary
@@ -30,9 +30,11 @@ import lombok.extern.slf4j.Slf4j;
import org.springblade.core.oauth2.endpoint.OAuth2TokenEndPoint;
import org.springblade.core.secure.BladeUser;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.support.Kv;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.feign.ISysClient;
import org.springblade.system.pojo.enums.MpUserIdentity;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
@@ -72,10 +74,26 @@ public class OAuth2UserInfoResponseAdvice implements ResponseBodyAdvice<Object>
if (userInfo == null) {
return body;
}
userInfo.setPermission(loadPermission(bladeUser.getRoleId()));
if (isMpUser(bladeUser)) {
// 小程序用户:按身份直接生成权限,不走系统角色菜单
userInfo.setPermission(MpUserIdentity.toPermissions(bladeUser.getRoleName()));
} else {
userInfo.setPermission(loadPermission(bladeUser.getRoleId()));
}
return userInfo;
}
private boolean isMpUser(BladeUser bladeUser) {
Kv detail = bladeUser.getDetail();
if (detail != null && MpUserIdentity.DETAIL_TYPE.equalsIgnoreCase(Func.toStr(detail.get("type")))) {
return true;
}
// 兼容:无系统角色且 roleName 为小程序身份
return Func.isBlank(bladeUser.getRoleId())
&& Func.isNotBlank(bladeUser.getRoleName())
&& !MpUserIdentity.split(bladeUser.getRoleName()).isEmpty();
}
private List<String> loadPermission(String roleId) {
if (Func.isBlank(roleId)) {
return Collections.emptyList();
@@ -2,10 +2,12 @@ package org.springblade.auth.granter;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springblade.auth.utils.TokenUtil;
import org.springblade.core.oauth2.exception.UserInvalidException;
import org.springblade.core.oauth2.granter.AbstractTokenGranter;
import org.springblade.core.oauth2.handler.PasswordHandler;
import org.springblade.core.oauth2.provider.OAuth2Request;
import org.springblade.core.oauth2.service.OAuth2Client;
import org.springblade.core.oauth2.service.OAuth2ClientService;
import org.springblade.core.oauth2.service.OAuth2User;
import org.springblade.core.oauth2.service.OAuth2UserService;
@@ -13,7 +15,8 @@ import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.core.tool.utils.WebUtil;
import org.springblade.system.feign.IUserClient;
import org.springblade.system.feign.IMpUserClient;
import org.springblade.system.pojo.entity.MpUserInfo;
import org.springblade.thirdparty.wechat.constant.WechatMiniConstant;
import org.springblade.thirdparty.wechat.exception.WechatMiniException;
import org.springblade.thirdparty.wechat.pojo.vo.WechatPhoneVO;
@@ -25,7 +28,8 @@ import org.springframework.stereotype.Component;
* 微信小程序手机号一键登录。
* <p>
* grant_type=wechat_applet,参数:loginCode(wx.login)、phoneCode(getPhoneNumber)。
* 流程:换 openid + 手机号 → 按手机号查用户(不存在则拒绝)→ 记录 openid → 发令牌(与密码登录一致)。
* 流程:换 openid + 手机号 → 按手机号查小程序用户(blade_mp_user,不存在则拒绝)
* → 记录 openid → 按身份发令牌(不依赖系统角色)。
*/
@Slf4j
@Component
@@ -33,17 +37,17 @@ public class WechatMiniTokenGranter extends AbstractTokenGranter {
private final OAuth2UserService userService;
private final IWechatMiniService wechatMiniService;
private final IUserClient userClient;
private final IMpUserClient mpUserClient;
public WechatMiniTokenGranter(OAuth2ClientService clientService,
OAuth2UserService userService,
PasswordHandler passwordHandler,
IWechatMiniService wechatMiniService,
IUserClient userClient) {
IMpUserClient mpUserClient) {
super(clientService, userService, passwordHandler);
this.userService = userService;
this.wechatMiniService = wechatMiniService;
this.userClient = userClient;
this.mpUserClient = mpUserClient;
}
@Override
@@ -54,7 +58,7 @@ public class WechatMiniTokenGranter extends AbstractTokenGranter {
@Override
public OAuth2User user(OAuth2Request request) {
// 先校验客户端是否允许该授权类型,避免先调微信再因客户端配置失败
var oauthClient = client(request);
OAuth2Client oauthClient = client(request);
HttpServletRequest httpRequest = WebUtil.getRequest();
String loginCode = httpRequest.getParameter("loginCode");
@@ -78,16 +82,21 @@ public class WechatMiniTokenGranter extends AbstractTokenGranter {
throw new UserInvalidException("未获取到微信手机号");
}
OAuth2User user = userService.loadByPhone(phone, request);
if (!userService.validateUser(user)) {
R<MpUserInfo> mpUserResult = mpUserClient.userInfoByPhone(request.getTenantId(), phone);
if (mpUserResult == null || !mpUserResult.isSuccess() || mpUserResult.getData() == null
|| mpUserResult.getData().getUser() == null) {
throw new UserInvalidException("用户不存在,无法登录");
}
R<Boolean> bindResult = userClient.bindWxMiniOpenId(
OAuth2User user = TokenUtil.convertMpUser(mpUserResult.getData());
if (!userService.validateUser(user)) {
throw new UserInvalidException("用户身份无效,无法登录");
}
R<Boolean> bindResult = mpUserClient.bindOpenId(
request.getTenantId(),
Func.toLong(user.getUserId()),
session.getOpenid(),
phone
session.getOpenid()
);
if (bindResult == null || !bindResult.isSuccess()) {
String msg = bindResult != null ? bindResult.getMsg() : "绑定 openid 失败";
@@ -18,9 +18,6 @@
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.auth.service;
@@ -35,8 +32,11 @@ import org.springblade.core.oauth2.utils.OAuth2ExceptionUtil;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.system.feign.IMpUserClient;
import org.springblade.system.feign.IUserClient;
import org.springblade.system.pojo.entity.MpUserInfo;
import org.springblade.system.pojo.entity.UserInfo;
import org.springblade.system.pojo.enums.MpUserIdentity;
import org.springblade.system.pojo.enums.UserType;
import java.util.Optional;
@@ -49,6 +49,7 @@ import java.util.Optional;
@RequiredArgsConstructor
public class BladeUserDetailService implements OAuth2UserService {
private final IUserClient userClient;
private final IMpUserClient mpUserClient;
@Override
public OAuth2User loadByUserId(String userId, OAuth2Request request) {
@@ -57,13 +58,29 @@ public class BladeUserDetailService implements OAuth2UserService {
.filter(s -> !StringUtil.isBlank(s))
.orElse(UserType.WEB.getName());
// 获取用户信息
R<UserInfo> result = userClient.userInfo(Func.toLong(userId), userType);
if (!result.isSuccess()) {
OAuth2ExceptionUtil.throwFromCode(OAuth2ErrorCode.INVALID_USER);
// 小程序刷新/鉴权:直接查 blade_mp_user
if (MpUserIdentity.DETAIL_TYPE.equalsIgnoreCase(userType)) {
OAuth2User mpUser = loadMpUserById(userId);
if (mpUser == null) {
OAuth2ExceptionUtil.throwFromCode(OAuth2ErrorCode.INVALID_USER);
}
return mpUser;
}
// 构建oauth2用户信息
return TokenUtil.convertUser(result.getData(), request);
// 系统用户
R<UserInfo> result = userClient.userInfo(Func.toLong(userId), userType);
if (result != null && result.isSuccess() && result.getData() != null
&& result.getData().getUser() != null) {
return TokenUtil.convertUser(result.getData(), request);
}
// 兼容:未带 userType=mp 时,回落查询小程序用户(令牌刷新)
OAuth2User mpUser = loadMpUserById(userId);
if (mpUser != null) {
return mpUser;
}
OAuth2ExceptionUtil.throwFromCode(OAuth2ErrorCode.INVALID_USER);
return null;
}
@Override
@@ -74,6 +91,15 @@ public class BladeUserDetailService implements OAuth2UserService {
.orElse(UserType.WEB.getName());
String tenantId = request.getTenantId();
// 小程序用户:按账号查 blade_mp_user
if (MpUserIdentity.DETAIL_TYPE.equalsIgnoreCase(userType)) {
R<MpUserInfo> mpResult = mpUserClient.userInfoByAccount(tenantId, username);
if (mpResult == null || !mpResult.isSuccess() || mpResult.getData() == null) {
return null;
}
return TokenUtil.convertMpUser(mpResult.getData());
}
// 获取用户信息
R<UserInfo> result = userClient.userInfo(tenantId, username, userType);
if (!result.isSuccess()) {
@@ -91,6 +117,11 @@ public class BladeUserDetailService implements OAuth2UserService {
.orElse(UserType.WEB.getName());
String tenantId = request.getTenantId();
// 小程序用户类型:仅查 blade_mp_user
if (MpUserIdentity.DETAIL_TYPE.equalsIgnoreCase(userType)) {
return loadMpUserByPhone(tenantId, phone);
}
// 获取用户信息
R<UserInfo> result = userClient.userInfoByPhone(tenantId, phone, userType);
if (!result.isSuccess()) {
@@ -108,4 +139,28 @@ public class BladeUserDetailService implements OAuth2UserService {
.isPresent(); // 如果上述条件都满足,则返回true,否则返回false
}
private OAuth2User loadMpUserById(String userId) {
if (Func.isBlank(userId)) {
return null;
}
try {
R<MpUserInfo> result = mpUserClient.userInfoById(Func.toLong(userId));
if (result != null && result.isSuccess() && result.getData() != null
&& result.getData().getUser() != null) {
return TokenUtil.convertMpUser(result.getData());
}
} catch (Exception exception) {
// 非小程序用户场景忽略,继续走系统用户
}
return null;
}
private OAuth2User loadMpUserByPhone(String tenantId, String phone) {
R<MpUserInfo> result = mpUserClient.userInfoByPhone(tenantId, phone);
if (result == null || !result.isSuccess() || result.getData() == null) {
return null;
}
return TokenUtil.convertMpUser(result.getData());
}
}
@@ -28,12 +28,14 @@ package org.springblade.auth.utils;
import org.springblade.core.oauth2.provider.OAuth2Request;
import org.springblade.core.oauth2.service.OAuth2User;
import org.springblade.core.oauth2.service.impl.OAuth2UserDetail;
import org.springblade.core.tool.support.Kv;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.SysCache;
import org.springblade.system.pojo.entity.MpUser;
import org.springblade.system.pojo.entity.MpUserInfo;
import org.springblade.system.pojo.entity.User;
import org.springblade.system.pojo.entity.UserInfo;
import java.util.Collections;
import org.springblade.system.pojo.enums.MpUserIdentity;
/**
* 认证工具类
@@ -75,6 +77,7 @@ public class TokenUtil {
userDetail.setRealName(user.getRealName());
userDetail.setAccount(user.getAccount());
userDetail.setPassword(user.getPassword());
userDetail.setPhone(user.getPhone());
userDetail.setDeptId(user.getDeptId());
userDetail.setPostId(user.getPostId());
userDetail.setRoleId(user.getRoleId());
@@ -85,4 +88,37 @@ public class TokenUtil {
return userDetail;
}
/**
* 小程序用户转换为OAuth2标准用户(身份直接写入 authorities,不走系统角色)
*
* @param userInfo 小程序用户信息
* @return OAuth2User
*/
public static OAuth2User convertMpUser(MpUserInfo userInfo) {
if (userInfo == null || userInfo.getUser() == null) {
return null;
}
MpUser user = userInfo.getUser();
OAuth2UserDetail userDetail = new OAuth2UserDetail();
userDetail.setUserId(String.valueOf(user.getId()));
userDetail.setTenantId(user.getTenantId());
userDetail.setName(user.getName());
userDetail.setRealName(user.getRealName());
userDetail.setAccount(user.getAccount());
userDetail.setPassword(user.getPassword());
userDetail.setPhone(user.getPhone());
userDetail.setDeptId(user.getDeptId());
userDetail.setPostId(user.getPostId());
// 小程序用户不绑定系统角色,roleId 留空;身份放入 roleName/authorities
userDetail.setRoleId("");
userDetail.setRoleName(Func.join(userInfo.getIdentities()));
userDetail.setAvatar(user.getAvatar());
userDetail.setAuthorities(userInfo.getIdentities());
userDetail.setPermissions(userInfo.getPermissions());
userDetail.setDetail(userInfo.getDetail() == null
? Kv.create().set("type", MpUserIdentity.DETAIL_TYPE)
: userInfo.getDetail());
return userDetail;
}
}