1、新增小程序相关接口

2、调整OA
This commit is contained in:
2026-09-18 16:31:37 +08:00
parent 0fa0eae43c
commit 01993779a7
71 changed files with 6463 additions and 213 deletions
+4
View File
@@ -75,6 +75,10 @@
<groupId>org.springblade</groupId>
<artifactId>blade-mk-api</artifactId>
</dependency>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-wechat-api</artifactId>
</dependency>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-resource-api</artifactId>
@@ -32,4 +32,10 @@ package org.springblade.auth.constant;
*/
public interface BladeAuthConstant {
/**
* 小程序/登录短信验证码资源编号(对应后台 /resource/sms 的 smsCode
*/
String LOGIN_SMS_CODE = "ali_reg";
}
@@ -0,0 +1,94 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* 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.endpoint;
import lombok.RequiredArgsConstructor;
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.utils.BeanUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.feign.ISysClient;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.util.Collections;
import java.util.List;
/**
* 增强 /oauth/user-info 响应,补充 permission 权限标识字段
*
* @author Chill
*/
@Slf4j
@RequiredArgsConstructor
@RestControllerAdvice(assignableTypes = OAuth2TokenEndPoint.class)
public class OAuth2UserInfoResponseAdvice implements ResponseBodyAdvice<Object> {
private final ISysClient sysClient;
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return returnType.getMethod() != null && "userInfo".equals(returnType.getMethod().getName());
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
if (!(body instanceof BladeUser bladeUser)) {
return body;
}
OAuth2UserInfoVO userInfo = BeanUtil.copyProperties(bladeUser, OAuth2UserInfoVO.class);
if (userInfo == null) {
return body;
}
userInfo.setPermission(loadPermission(bladeUser.getRoleId()));
return userInfo;
}
private List<String> loadPermission(String roleId) {
if (Func.isBlank(roleId)) {
return Collections.emptyList();
}
try {
R<List<String>> result = sysClient.getPermissions(roleId);
if (result != null && result.isSuccess() && result.getData() != null) {
return result.getData();
}
} catch (Exception exception) {
log.warn("加载用户权限标识失败, roleId={}", roleId, exception);
}
return Collections.emptyList();
}
}
@@ -0,0 +1,55 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* 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.endpoint;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springblade.core.secure.BladeUser;
import java.io.Serial;
import java.util.List;
/**
* OAuth用户信息(含权限标识,供小程序等客户端使用)
*
* @author Chill
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "OAuth用户信息")
public class OAuth2UserInfoVO extends BladeUser {
@Serial
private static final long serialVersionUID = 1L;
/**
* 权限标识集合(菜单按钮 code)
*/
@Schema(description = "权限标识集合")
private List<String> permission;
}
@@ -28,13 +28,14 @@ package org.springblade.auth.endpoint;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import org.springblade.auth.constant.BladeAuthConstant;
import org.springblade.core.oauth2.props.OAuth2Properties;
import org.springblade.core.oauth2.provider.OAuth2Request;
import org.springblade.core.oauth2.service.OAuth2User;
import org.springblade.core.oauth2.service.OAuth2UserService;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.SM2Util;
import org.springblade.core.tool.utils.StringPool;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.resource.feign.ISmsClient;
import org.springblade.resource.utils.SmsUtil;
@@ -73,11 +74,14 @@ public class Oauth2SmsEndpoint {
* 短信验证码发送
*
* @param tenantId 租户ID
* @param phone 手机号
* @param phone 手机号SM2 加密)
* @param code 短信资源编号,默认 ali_reg(后台 /resource/sms
*/
@SneakyThrows
@PostMapping("/oauth/sms/send-validate")
public R sendValidate(@RequestParam String tenantId, @RequestParam String phone) {
public R sendValidate(@RequestParam String tenantId,
@RequestParam String phone,
@RequestParam(required = false) String code) {
// 校验手机加密认证,防止恶意发送验证码
String decryptedPhone = SM2Util.decrypt(phone, properties.getPublicKey(), properties.getPrivateKey());
if (StringUtil.isBlank(decryptedPhone)) {
@@ -90,8 +94,9 @@ public class Oauth2SmsEndpoint {
if (oAuth2User == null) {
return R.fail(USER_PHONE_NOT_FOUND);
}
// 用户存在则发送验证码
R result = smsClient.sendValidate(tenantId, StringPool.EMPTY, decryptedPhone);
// 使用指定短信资源(默认 ali_reg)
String smsResourceCode = Func.toStr(code, BladeAuthConstant.LOGIN_SMS_CODE);
R result = smsClient.sendValidate(tenantId, smsResourceCode, decryptedPhone);
return result.isSuccess() ? R.data(result.getData(), SmsUtil.SEND_SUCCESS) : R.fail(SmsUtil.SEND_FAIL);
}
@@ -26,6 +26,7 @@
package org.springblade.auth.granter;
import jakarta.servlet.http.HttpServletRequest;
import org.springblade.auth.constant.BladeAuthConstant;
import org.springblade.core.oauth2.constant.OAuth2TokenConstant;
import org.springblade.core.oauth2.exception.UserInvalidException;
import org.springblade.core.oauth2.granter.AbstractTokenGranter;
@@ -37,8 +38,8 @@ import org.springblade.core.oauth2.service.OAuth2User;
import org.springblade.core.oauth2.service.OAuth2UserService;
import org.springblade.core.sms.model.SmsCode;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.SM2Util;
import org.springblade.core.tool.utils.StringPool;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.core.tool.utils.WebUtil;
import org.springblade.resource.feign.ISmsClient;
@@ -78,8 +79,10 @@ public class SmsTokenGranter extends AbstractTokenGranter {
if (StringUtil.isBlank(decryptedPhone)) {
throw new UserInvalidException(OAuth2TokenConstant.USER_PHONE_NOT_FOUND);
}
// 获取短信验证信息
R result = smsClient.validateMessage(tenantId, StringPool.EMPTY, smsCode.getId(), smsCode.getValue(), decryptedPhone);
// 与发送时使用同一短信资源编号(默认 ali_reg)
HttpServletRequest httpRequest = WebUtil.getRequest();
String smsResourceCode = Func.toStr(httpRequest.getParameter("code"), BladeAuthConstant.LOGIN_SMS_CODE);
R result = smsClient.validateMessage(tenantId, smsResourceCode, smsCode.getId(), smsCode.getValue(), decryptedPhone);
if (!result.isSuccess()) {
throw new UserInvalidException(OAuth2TokenConstant.CAPTCHA_NOT_CORRECT);
}
@@ -0,0 +1,102 @@
package org.springblade.auth.granter;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
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.OAuth2ClientService;
import org.springblade.core.oauth2.service.OAuth2User;
import org.springblade.core.oauth2.service.OAuth2UserService;
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.thirdparty.wechat.constant.WechatMiniConstant;
import org.springblade.thirdparty.wechat.exception.WechatMiniException;
import org.springblade.thirdparty.wechat.pojo.vo.WechatPhoneVO;
import org.springblade.thirdparty.wechat.pojo.vo.WechatSessionVO;
import org.springblade.thirdparty.wechat.service.IWechatMiniService;
import org.springframework.stereotype.Component;
/**
* 微信小程序手机号一键登录。
* <p>
* grant_type=wechat_applet,参数:loginCodewx.login)、phoneCodegetPhoneNumber)。
* 流程:换 openid + 手机号 → 按手机号查用户(不存在则拒绝)→ 记录 openid → 发令牌(与密码登录一致)。
*/
@Slf4j
@Component
public class WechatMiniTokenGranter extends AbstractTokenGranter {
private final OAuth2UserService userService;
private final IWechatMiniService wechatMiniService;
private final IUserClient userClient;
public WechatMiniTokenGranter(OAuth2ClientService clientService,
OAuth2UserService userService,
PasswordHandler passwordHandler,
IWechatMiniService wechatMiniService,
IUserClient userClient) {
super(clientService, userService, passwordHandler);
this.userService = userService;
this.wechatMiniService = wechatMiniService;
this.userClient = userClient;
}
@Override
public String type() {
return WechatMiniConstant.GRANT_TYPE;
}
@Override
public OAuth2User user(OAuth2Request request) {
// 先校验客户端是否允许该授权类型,避免先调微信再因客户端配置失败
var oauthClient = client(request);
HttpServletRequest httpRequest = WebUtil.getRequest();
String loginCode = httpRequest.getParameter("loginCode");
String phoneCode = httpRequest.getParameter("phoneCode");
if (StringUtil.isBlank(loginCode) || StringUtil.isBlank(phoneCode)) {
throw new UserInvalidException("微信登录参数不完整");
}
WechatSessionVO session;
WechatPhoneVO phoneInfo;
try {
session = wechatMiniService.code2Session(loginCode);
phoneInfo = wechatMiniService.getPhoneNumber(phoneCode);
} catch (WechatMiniException e) {
log.warn("微信小程序登录失败: {}", e.getMessage());
throw new UserInvalidException(e.getMessage());
}
String phone = Func.toStr(phoneInfo.getPurePhoneNumber(), phoneInfo.getPhoneNumber());
if (StringUtil.isBlank(phone)) {
throw new UserInvalidException("未获取到微信手机号");
}
OAuth2User user = userService.loadByPhone(phone, request);
if (!userService.validateUser(user)) {
throw new UserInvalidException("用户不存在,无法登录");
}
R<Boolean> bindResult = userClient.bindWxMiniOpenId(
request.getTenantId(),
Func.toLong(user.getUserId()),
session.getOpenid(),
phone
);
if (bindResult == null || !bindResult.isSuccess()) {
String msg = bindResult != null ? bindResult.getMsg() : "绑定 openid 失败";
log.warn("绑定微信 openid 失败 userId={} openid={} msg={}", user.getUserId(), session.getOpenid(), msg);
throw new UserInvalidException(StringUtil.isBlank(msg) ? "绑定 openid 失败" : msg);
}
user.setClient(oauthClient);
return user;
}
}
@@ -25,11 +25,17 @@
*/
package org.springblade.auth.service;
import org.springblade.core.oauth2.constant.OAuth2GranterConstant;
import org.springblade.core.oauth2.provider.OAuth2Request;
import org.springblade.core.oauth2.service.OAuth2Client;
import org.springblade.core.oauth2.service.impl.OAuth2ClientDetailService;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.StringPool;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.Arrays;
import java.util.Optional;
/**
* BladeClientDetailService
*
@@ -57,6 +63,13 @@ public class BladeClientDetailService extends OAuth2ClientDetailService {
@Override
public boolean validateGranter(OAuth2Client client, String grantType) {
return super.validateGranter(client, grantType);
// 微信小程序一键登录:兼容库表未配置 wechat_applet 的存量客户端
if (OAuth2GranterConstant.WECHAT_APPLET.equals(grantType) || "wechat_mini".equals(grantType)) {
return true;
}
return Optional.ofNullable(client)
.map(c -> Arrays.stream(Func.split(c.getAuthorizedGrantTypes(), StringPool.COMMA))
.anyMatch(s -> s.trim().equals(grantType)))
.orElse(false);
}
}