1、新增小程序相关接口
2、调整OA
This commit is contained in:
@@ -32,4 +32,10 @@ package org.springblade.auth.constant;
|
||||
*/
|
||||
public interface BladeAuthConstant {
|
||||
|
||||
/**
|
||||
* 小程序/登录短信验证码资源编号(对应后台 /resource/sms 的 smsCode)
|
||||
*/
|
||||
String LOGIN_SMS_CODE = "ali_reg";
|
||||
|
||||
}
|
||||
|
||||
|
||||
+94
@@ -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,参数:loginCode(wx.login)、phoneCode(getPhoneNumber)。
|
||||
* 流程:换 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;
|
||||
}
|
||||
|
||||
}
|
||||
+14
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user