35 changed files with 1971 additions and 10 deletions
-
6chenhai-admin/pom.xml
-
302chenhai-admin/src/main/java/com/chenhai/web/controller/auth/MultiAuthController.java
-
15chenhai-admin/src/main/resources/application.yml
-
12chenhai-common/src/main/java/com/chenhai/common/core/domain/entity/SysUser.java
-
99chenhai-common/src/main/java/com/chenhai/common/core/domain/entity/SysUserAuth.java
-
23chenhai-common/src/main/java/com/chenhai/common/core/domain/model/LoginUser.java
-
90chenhai-common/src/main/java/com/chenhai/common/core/domain/model/PhoneDecryptRequest.java
-
33chenhai-common/src/main/java/com/chenhai/common/core/domain/model/PhoneLoginBody.java
-
100chenhai-common/src/main/java/com/chenhai/common/core/domain/model/WechatBindRequest.java
-
64chenhai-common/src/main/java/com/chenhai/common/core/domain/model/WechatBindResult.java
-
25chenhai-common/src/main/java/com/chenhai/common/core/domain/model/WechatLoginBody.java
-
66chenhai-common/src/main/java/com/chenhai/common/utils/WechatDecryptUtil.java
-
6chenhai-framework/pom.xml
-
80chenhai-framework/src/main/java/com/chenhai/framework/config/AuthenticationProviderConfig.java
-
55chenhai-framework/src/main/java/com/chenhai/framework/config/SecurityConfig.java
-
29chenhai-framework/src/main/java/com/chenhai/framework/security/exception/WechatNeedBindException.java
-
90chenhai-framework/src/main/java/com/chenhai/framework/security/provider/PhoneAuthenticationProvider.java
-
142chenhai-framework/src/main/java/com/chenhai/framework/security/provider/WechatAuthenticationProvider.java
-
69chenhai-framework/src/main/java/com/chenhai/framework/security/token/PhoneAuthenticationToken.java
-
62chenhai-framework/src/main/java/com/chenhai/framework/security/token/WechatAuthenticationToken.java
-
17chenhai-framework/src/main/java/com/chenhai/framework/web/service/SysSmsService.java
-
27chenhai-framework/src/main/java/com/chenhai/framework/web/service/SysSmsServiceImpl.java
-
63chenhai-framework/src/main/java/com/chenhai/framework/web/service/TokenService.java
-
38chenhai-system/pom.xml
-
57chenhai-system/src/main/java/com/chenhai/muhu/domain/model/MuhuWxLoginBody.java
-
36chenhai-system/src/main/java/com/chenhai/muhu/service/IUserAuthService.java
-
39chenhai-system/src/main/java/com/chenhai/muhu/service/WechatService.java
-
79chenhai-system/src/main/java/com/chenhai/muhu/service/impl/UserAuthServiceImpl.java
-
76chenhai-system/src/main/java/com/chenhai/muhu/service/impl/WechatServiceImpl.java
-
56chenhai-system/src/main/java/com/chenhai/system/mapper/SysUserAuthMapper.java
-
8chenhai-system/src/main/java/com/chenhai/system/mapper/SysUserMapper.java
-
8chenhai-system/src/main/java/com/chenhai/system/service/ISysUserService.java
-
5chenhai-system/src/main/java/com/chenhai/system/service/impl/SysUserServiceImpl.java
-
97chenhai-system/src/main/resources/mapper/system/SysUserAuthMapper.xml
-
7chenhai-system/src/main/resources/mapper/system/SysUserMapper.xml
@ -0,0 +1,302 @@ |
|||
package com.chenhai.web.controller.auth; |
|||
|
|||
import com.alibaba.fastjson2.JSONObject; |
|||
import com.chenhai.common.constant.Constants; |
|||
import com.chenhai.common.core.domain.AjaxResult; |
|||
import com.chenhai.common.core.domain.entity.SysUser; |
|||
import com.chenhai.common.core.domain.model.*; |
|||
import com.chenhai.common.core.redis.RedisCache; |
|||
import com.chenhai.common.utils.WechatDecryptUtil; |
|||
import com.chenhai.framework.security.exception.WechatNeedBindException; |
|||
import com.chenhai.framework.security.token.PhoneAuthenticationToken; |
|||
import com.chenhai.framework.security.token.WechatAuthenticationToken; |
|||
import com.chenhai.framework.web.service.SysLoginService; |
|||
import com.chenhai.framework.web.service.SysPermissionService; |
|||
import com.chenhai.framework.web.service.SysSmsService; |
|||
import com.chenhai.framework.web.service.TokenService; |
|||
import com.chenhai.muhu.service.IUserAuthService; |
|||
import com.chenhai.muhu.service.WechatService; |
|||
import com.chenhai.system.service.ISysConfigService; |
|||
import com.chenhai.system.service.ISysUserService; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.security.authentication.AuthenticationManager; |
|||
import org.springframework.security.core.Authentication; |
|||
import org.springframework.transaction.annotation.Transactional; |
|||
import org.springframework.web.bind.annotation.PostMapping; |
|||
import org.springframework.web.bind.annotation.RequestBody; |
|||
import org.springframework.web.bind.annotation.RequestMapping; |
|||
import org.springframework.web.bind.annotation.RestController; |
|||
|
|||
import jakarta.validation.Valid; |
|||
import java.util.Date; |
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
|
|||
/** |
|||
* 多端认证控制器 - 统一处理牧户和兽医 |
|||
*/ |
|||
@RestController |
|||
@RequestMapping("/auth") |
|||
public class MultiAuthController { |
|||
|
|||
@Autowired |
|||
private AuthenticationManager authenticationManager; |
|||
|
|||
@Autowired |
|||
private TokenService tokenService; |
|||
|
|||
@Autowired |
|||
private SysSmsService smsService; |
|||
|
|||
@Autowired |
|||
private IUserAuthService userAuthService; |
|||
|
|||
@Autowired |
|||
private WechatService wechatService; |
|||
|
|||
@Autowired |
|||
private SysPermissionService permissionService; |
|||
|
|||
@Autowired |
|||
private RedisCache redisCache; |
|||
|
|||
@Autowired |
|||
private ISysUserService userService; |
|||
|
|||
@Autowired |
|||
private ISysConfigService sysConfigService; |
|||
|
|||
@Autowired |
|||
private SysLoginService loginService; |
|||
|
|||
/** |
|||
* 微信小程序登录(统一入口) |
|||
*/ |
|||
@PostMapping("/wechat/login") |
|||
public AjaxResult wechatLogin(@Valid @RequestBody WechatLoginBody loginBody) { |
|||
// 验证参数 |
|||
if (loginBody.getCode() == null || loginBody.getClientType() == null) { |
|||
return AjaxResult.error("参数错误"); |
|||
} |
|||
|
|||
// 验证clientType |
|||
if (!"herdsman-app".equals(loginBody.getClientType()) && !"vet-app".equals(loginBody.getClientType())) { |
|||
return AjaxResult.error("不支持的客户端类型"); |
|||
} |
|||
|
|||
// 创建认证token |
|||
WechatAuthenticationToken authToken = new WechatAuthenticationToken( |
|||
loginBody.getCode(), |
|||
loginBody.getClientType() |
|||
); |
|||
|
|||
try { |
|||
// 认证 |
|||
Authentication authentication = authenticationManager.authenticate(authToken); |
|||
LoginUser loginUser = (LoginUser) authentication.getPrincipal(); |
|||
|
|||
loginService.recordLoginInfo(loginUser.getUserId()); |
|||
|
|||
// 生成token |
|||
String token = tokenService.createToken(loginUser); |
|||
|
|||
// 返回结果 |
|||
AjaxResult ajax = AjaxResult.success(); |
|||
ajax.put(Constants.TOKEN, token); |
|||
return ajax; |
|||
|
|||
} catch (WechatNeedBindException e) { |
|||
// 需要绑定手机号 |
|||
Map<String, Object> data = new HashMap<>(); |
|||
data.put("needBind", true); |
|||
data.put("userType", e.getUserType()); // "herdsman" 或 "vet" |
|||
data.put("clientType", e.getClientType()); // "herdsman-app" 或 "vet-app" |
|||
data.put("openid", e.getOpenid()); |
|||
data.put("tempCode", e.getTempCode()); |
|||
data.put("authType", e.getAuthType()); |
|||
data.put("message", "请绑定手机号"); |
|||
return AjaxResult.success(data); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 微信绑定手机号(统一绑定接口) |
|||
* 牧户和兽医使用同一套逻辑 |
|||
*/ |
|||
@PostMapping("/wechat/bind") |
|||
@Transactional |
|||
public AjaxResult wechatBind(@Valid @RequestBody WechatBindRequest request) { |
|||
try { |
|||
// 1. 基本参数验证 |
|||
if (request.getEncryptedData() == null || request.getIv() == null) { |
|||
return AjaxResult.error("请通过微信授权获取手机号"); |
|||
} |
|||
|
|||
// 2. 从Redis获取sessionKey |
|||
String sessionKey = redisCache.getCacheObject("wechat:session:" + request.getTempCode()); |
|||
if (sessionKey == null) { |
|||
return AjaxResult.error("绑定会话已过期,请重新登录"); |
|||
} |
|||
|
|||
// 3. 解密手机号 |
|||
String phone = WechatDecryptUtil.decryptPhone( |
|||
request.getEncryptedData(), |
|||
request.getIv(), |
|||
sessionKey |
|||
); |
|||
if (phone == null || phone.trim().isEmpty()) { |
|||
return AjaxResult.error("获取手机号失败"); |
|||
} |
|||
|
|||
// 4. 查询手机号是否已注册 |
|||
SysUser existingUser = userService.selectUserByPhone(phone); |
|||
|
|||
// 5. 用户不存在,创建新用户 |
|||
if (existingUser == null) { |
|||
// 根据userType创建对应类型的用户 |
|||
String userTypeCode = "herdsman".equals(request.getUserType()) ? "02" : "01"; |
|||
|
|||
existingUser = createUser( |
|||
phone, |
|||
userTypeCode, |
|||
request.getNickName(), |
|||
request.getAvatarUrl(), |
|||
request.getClientType() |
|||
); |
|||
|
|||
} else { |
|||
// 6. 用户已存在,检查用户类型是否匹配 |
|||
String existingUserType = existingUser.getUserType(); |
|||
String expectedUserType = "herdsman".equals(request.getUserType()) ? "02" : "01"; |
|||
|
|||
if (!existingUserType.equals(expectedUserType)) { |
|||
String existingTypeName = "01".equals(existingUserType) ? "兽医" : "牧户"; |
|||
String expectedTypeName = "01".equals(expectedUserType) ? "兽医" : "牧户"; |
|||
return AjaxResult.error("该手机号已注册为" + existingTypeName + ",请使用" + expectedTypeName + "小程序"); |
|||
} |
|||
|
|||
// 7. 用户类型匹配,更新用户信息(昵称、头像等) |
|||
if (request.getNickName() != null) { |
|||
existingUser.setNickName(request.getNickName()); |
|||
} |
|||
if (request.getAvatarUrl() != null) { |
|||
existingUser.setAvatar(request.getAvatarUrl()); |
|||
} |
|||
userService.updateUser(existingUser); |
|||
} |
|||
|
|||
// 8. 绑定微信openid |
|||
userAuthService.bindAuth( |
|||
existingUser.getUserId(), |
|||
request.getAuthType(), |
|||
request.getOpenid(), |
|||
sessionKey |
|||
); |
|||
|
|||
// 9. 创建登录用户 |
|||
LoginUser loginUser = new LoginUser( |
|||
existingUser.getUserId(), |
|||
existingUser.getDeptId(), |
|||
existingUser, |
|||
permissionService.getMenuPermission(existingUser), |
|||
request.getClientType() |
|||
); |
|||
|
|||
loginService.recordLoginInfo(loginUser.getUserId()); |
|||
|
|||
// 10. 生成token |
|||
String token = tokenService.createToken(loginUser); |
|||
|
|||
// 11. 清理Redis临时数据 |
|||
redisCache.deleteObject("wechat:session:" + request.getTempCode()); |
|||
|
|||
AjaxResult ajax = AjaxResult.success("绑定成功"); |
|||
ajax.put(Constants.TOKEN, token); |
|||
return ajax; |
|||
|
|||
} catch (Exception e) { |
|||
return AjaxResult.error("绑定失败: " + e.getMessage()); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 手机号+密码登录(PC端使用) |
|||
* 牧户和兽医都可用 |
|||
*/ |
|||
@PostMapping("/phone/login") |
|||
public AjaxResult phoneLogin(@Valid @RequestBody PhoneLoginBody loginBody) { |
|||
// 验证参数 |
|||
if (loginBody.getPhone() == null || loginBody.getPassword() == null) { |
|||
return AjaxResult.error("参数错误"); |
|||
} |
|||
|
|||
// 根据请求来源确定clientType |
|||
String clientType = loginBody.getClientType(); // 前端可传 "vet-pc" 或 "herdsman-pc" |
|||
if (clientType == null) { |
|||
clientType = "vet-pc"; // 默认兽医PC端 |
|||
} |
|||
|
|||
// 创建认证token |
|||
PhoneAuthenticationToken authToken = new PhoneAuthenticationToken( |
|||
loginBody.getPhone(), |
|||
loginBody.getPassword(), |
|||
clientType |
|||
); |
|||
|
|||
// 认证 |
|||
Authentication authentication = authenticationManager.authenticate(authToken); |
|||
LoginUser loginUser = (LoginUser) authentication.getPrincipal(); |
|||
loginService.recordLoginInfo(loginUser.getUserId()); |
|||
|
|||
// 生成token |
|||
String token = tokenService.createToken(loginUser); |
|||
|
|||
// 返回结果 |
|||
AjaxResult ajax = AjaxResult.success(); |
|||
ajax.put(Constants.TOKEN, token); |
|||
return ajax; |
|||
} |
|||
|
|||
/** |
|||
* 创建用户(牧户或兽医) |
|||
*/ |
|||
private SysUser createUser(String phone, String userType, |
|||
String nickName, String avatarUrl, String clientType) { |
|||
SysUser user = new SysUser(); |
|||
|
|||
// 生成用户名:前缀 + 手机后4位 + 4位随机数 |
|||
String usernamePrefix = "01".equals(userType) ? "vet_" : "muhu_"; |
|||
String phoneSuffix = phone.length() > 4 ? phone.substring(phone.length() - 4) : phone; |
|||
String randomSuffix = String.format("%04d", (int)(Math.random() * 10000)); // 4位随机数 |
|||
String username = usernamePrefix + phoneSuffix + "_" + randomSuffix; |
|||
user.setUserName(username); |
|||
|
|||
// 设置昵称:统一为"用户" + 去掉前缀的username部分 |
|||
String displayName = phoneSuffix + "_" + randomSuffix; // 去掉前缀的部分 |
|||
user.setNickName(nickName != null ? nickName : ("用户" + displayName)); |
|||
|
|||
user.setUserType(userType); // "01":兽医, "02":牧户 |
|||
user.setEmail(""); |
|||
user.setPhonenumber(phone); |
|||
user.setSex("0"); |
|||
user.setAvatar(avatarUrl != null ? avatarUrl : ""); |
|||
|
|||
// 设置默认密码(微信登录用不到,但PC端登录需要) |
|||
String defaultPassword = sysConfigService.selectConfigByKey("sys.user.initPassword"); |
|||
user.setPassword(com.chenhai.common.utils.SecurityUtils.encryptPassword(defaultPassword)); |
|||
|
|||
user.setStatus("0"); // 正常状态 |
|||
user.setDelFlag("0"); |
|||
user.setCreateTime(new Date()); |
|||
|
|||
// 如果是兽医,设置初始审核状态 |
|||
// if ("01".equals(userType)) { |
|||
// user.setVetStatus("0"); // 0:未提交资质, 1:审核中, 2:已认证, 3:审核不通过 |
|||
// } |
|||
|
|||
userService.insertUser(user); |
|||
|
|||
// 重新查询获取完整用户信息 |
|||
return userService.selectUserByUserName(username); |
|||
} |
|||
} |
|||
@ -0,0 +1,99 @@ |
|||
package com.chenhai.common.core.domain.entity; |
|||
|
|||
import com.chenhai.common.annotation.Excel; |
|||
import com.chenhai.common.core.domain.BaseEntity; |
|||
import org.apache.commons.lang3.builder.ToStringBuilder; |
|||
import org.apache.commons.lang3.builder.ToStringStyle; |
|||
|
|||
/** |
|||
* 用户认证方式对象 sys_user_auth |
|||
*/ |
|||
public class SysUserAuth extends BaseEntity { |
|||
private static final long serialVersionUID = 1L; |
|||
|
|||
/** 认证ID */ |
|||
private Long authId; |
|||
|
|||
/** 用户ID */ |
|||
private Long userId; |
|||
|
|||
/** 认证类型: admin/phone/wechat_muhu/wechat_vet */ |
|||
@Excel(name = "认证类型") |
|||
private String authType; |
|||
|
|||
/** 认证标识: 用户名/手机号/微信openid */ |
|||
@Excel(name = "认证标识") |
|||
private String authKey; |
|||
|
|||
/** 认证密钥: 密码/微信session_key */ |
|||
private String authSecret; |
|||
|
|||
/** 状态(0正常 1停用) */ |
|||
@Excel(name = "状态", readConverterExp = "0=正常,1=停用") |
|||
private String status; |
|||
|
|||
public Long getAuthId() { |
|||
return authId; |
|||
} |
|||
|
|||
public void setAuthId(Long authId) { |
|||
this.authId = authId; |
|||
} |
|||
|
|||
public Long getUserId() { |
|||
return userId; |
|||
} |
|||
|
|||
public void setUserId(Long userId) { |
|||
this.userId = userId; |
|||
} |
|||
|
|||
public String getAuthType() { |
|||
return authType; |
|||
} |
|||
|
|||
public void setAuthType(String authType) { |
|||
this.authType = authType; |
|||
} |
|||
|
|||
public String getAuthKey() { |
|||
return authKey; |
|||
} |
|||
|
|||
public void setAuthKey(String authKey) { |
|||
this.authKey = authKey; |
|||
} |
|||
|
|||
public String getAuthSecret() { |
|||
return authSecret; |
|||
} |
|||
|
|||
public void setAuthSecret(String authSecret) { |
|||
this.authSecret = authSecret; |
|||
} |
|||
|
|||
public String getStatus() { |
|||
return status; |
|||
} |
|||
|
|||
public void setStatus(String status) { |
|||
this.status = status; |
|||
} |
|||
|
|||
@Override |
|||
public String toString() { |
|||
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE) |
|||
.append("authId", getAuthId()) |
|||
.append("userId", getUserId()) |
|||
.append("authType", getAuthType()) |
|||
.append("authKey", getAuthKey()) |
|||
.append("authSecret", getAuthSecret()) |
|||
.append("status", getStatus()) |
|||
.append("createBy", getCreateBy()) |
|||
.append("createTime", getCreateTime()) |
|||
.append("updateBy", getUpdateBy()) |
|||
.append("updateTime", getUpdateTime()) |
|||
.append("remark", getRemark()) |
|||
.toString(); |
|||
} |
|||
} |
|||
@ -0,0 +1,90 @@ |
|||
// PhoneDecryptRequest.java |
|||
package com.chenhai.common.core.domain.model; |
|||
|
|||
import jakarta.validation.constraints.NotBlank; |
|||
|
|||
public class PhoneDecryptRequest { |
|||
@NotBlank private String openid; |
|||
@NotBlank private String tempCode; |
|||
@NotBlank private String encryptedData; |
|||
@NotBlank private String iv; |
|||
@NotBlank private String authType; |
|||
@NotBlank private String userType; // "herdsman" |
|||
|
|||
// 用户基本信息(可选) |
|||
private String nickName; |
|||
private String avatarUrl; |
|||
private Integer gender; |
|||
|
|||
public String getOpenid() { |
|||
return openid; |
|||
} |
|||
|
|||
public void setOpenid(String openid) { |
|||
this.openid = openid; |
|||
} |
|||
|
|||
public String getTempCode() { |
|||
return tempCode; |
|||
} |
|||
|
|||
public void setTempCode(String tempCode) { |
|||
this.tempCode = tempCode; |
|||
} |
|||
|
|||
public String getEncryptedData() { |
|||
return encryptedData; |
|||
} |
|||
|
|||
public void setEncryptedData(String encryptedData) { |
|||
this.encryptedData = encryptedData; |
|||
} |
|||
|
|||
public String getIv() { |
|||
return iv; |
|||
} |
|||
|
|||
public void setIv(String iv) { |
|||
this.iv = iv; |
|||
} |
|||
|
|||
public String getAuthType() { |
|||
return authType; |
|||
} |
|||
|
|||
public void setAuthType(String authType) { |
|||
this.authType = authType; |
|||
} |
|||
|
|||
public String getUserType() { |
|||
return userType; |
|||
} |
|||
|
|||
public void setUserType(String userType) { |
|||
this.userType = userType; |
|||
} |
|||
|
|||
public String getNickName() { |
|||
return nickName; |
|||
} |
|||
|
|||
public void setNickName(String nickName) { |
|||
this.nickName = nickName; |
|||
} |
|||
|
|||
public String getAvatarUrl() { |
|||
return avatarUrl; |
|||
} |
|||
|
|||
public void setAvatarUrl(String avatarUrl) { |
|||
this.avatarUrl = avatarUrl; |
|||
} |
|||
|
|||
public Integer getGender() { |
|||
return gender; |
|||
} |
|||
|
|||
public void setGender(Integer gender) { |
|||
this.gender = gender; |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
package com.chenhai.common.core.domain.model; |
|||
|
|||
import jakarta.validation.constraints.NotBlank; |
|||
|
|||
public class PhoneLoginBody { |
|||
@NotBlank private String phone; |
|||
@NotBlank private String password; |
|||
private String clientType; // 可选:前端可以指定 "vet-pc" 或 "herdsman-pc" |
|||
|
|||
public String getPhone() { |
|||
return phone; |
|||
} |
|||
|
|||
public void setPhone(String phone) { |
|||
this.phone = phone; |
|||
} |
|||
|
|||
public String getPassword() { |
|||
return password; |
|||
} |
|||
|
|||
public void setPassword(String password) { |
|||
this.password = password; |
|||
} |
|||
|
|||
public String getClientType() { |
|||
return clientType; |
|||
} |
|||
|
|||
public void setClientType(String clientType) { |
|||
this.clientType = clientType; |
|||
} |
|||
} |
|||
@ -0,0 +1,100 @@ |
|||
package com.chenhai.common.core.domain.model; |
|||
|
|||
import jakarta.validation.constraints.NotBlank; |
|||
|
|||
public class WechatBindRequest { |
|||
@NotBlank private String openid; |
|||
@NotBlank private String tempCode; |
|||
@NotBlank private String authType; // "wechat_muhu" 或 "wechat_vet" |
|||
@NotBlank private String userType; // "herdsman" 或 "vet" |
|||
@NotBlank private String clientType; // "herdsman-app" 或 "vet-app" |
|||
|
|||
// 微信加密的手机号数据 |
|||
@NotBlank private String encryptedData; |
|||
@NotBlank private String iv; |
|||
|
|||
// 用户基本信息 |
|||
private String nickName; |
|||
private String avatarUrl; |
|||
private Integer gender; |
|||
|
|||
public String getOpenid() { |
|||
return openid; |
|||
} |
|||
|
|||
public void setOpenid(String openid) { |
|||
this.openid = openid; |
|||
} |
|||
|
|||
public String getTempCode() { |
|||
return tempCode; |
|||
} |
|||
|
|||
public void setTempCode(String tempCode) { |
|||
this.tempCode = tempCode; |
|||
} |
|||
|
|||
public String getAuthType() { |
|||
return authType; |
|||
} |
|||
|
|||
public void setAuthType(String authType) { |
|||
this.authType = authType; |
|||
} |
|||
|
|||
public String getUserType() { |
|||
return userType; |
|||
} |
|||
|
|||
public void setUserType(String userType) { |
|||
this.userType = userType; |
|||
} |
|||
|
|||
public String getClientType() { |
|||
return clientType; |
|||
} |
|||
|
|||
public void setClientType(String clientType) { |
|||
this.clientType = clientType; |
|||
} |
|||
|
|||
public String getEncryptedData() { |
|||
return encryptedData; |
|||
} |
|||
|
|||
public void setEncryptedData(String encryptedData) { |
|||
this.encryptedData = encryptedData; |
|||
} |
|||
|
|||
public String getIv() { |
|||
return iv; |
|||
} |
|||
|
|||
public void setIv(String iv) { |
|||
this.iv = iv; |
|||
} |
|||
|
|||
public String getNickName() { |
|||
return nickName; |
|||
} |
|||
|
|||
public void setNickName(String nickName) { |
|||
this.nickName = nickName; |
|||
} |
|||
|
|||
public String getAvatarUrl() { |
|||
return avatarUrl; |
|||
} |
|||
|
|||
public void setAvatarUrl(String avatarUrl) { |
|||
this.avatarUrl = avatarUrl; |
|||
} |
|||
|
|||
public Integer getGender() { |
|||
return gender; |
|||
} |
|||
|
|||
public void setGender(Integer gender) { |
|||
this.gender = gender; |
|||
} |
|||
} |
|||
@ -0,0 +1,64 @@ |
|||
package com.chenhai.common.core.domain.model; |
|||
|
|||
/** |
|||
* 微信绑定结果 |
|||
*/ |
|||
public class WechatBindResult { |
|||
|
|||
private boolean success; |
|||
private String message; |
|||
private String token; |
|||
private boolean needBind; // 是否需要绑定 |
|||
|
|||
public WechatBindResult() { |
|||
} |
|||
|
|||
public WechatBindResult(boolean success, String message) { |
|||
this.success = success; |
|||
this.message = message; |
|||
} |
|||
|
|||
public WechatBindResult(boolean success, String message, String token) { |
|||
this.success = success; |
|||
this.message = message; |
|||
this.token = token; |
|||
} |
|||
|
|||
// public WechatBindResult(boolean needBind, String message) { |
|||
// this.needBind = needBind; |
|||
// this.message = message; |
|||
// } |
|||
|
|||
// Getters and Setters |
|||
public boolean isSuccess() { |
|||
return success; |
|||
} |
|||
|
|||
public void setSuccess(boolean success) { |
|||
this.success = success; |
|||
} |
|||
|
|||
public String getMessage() { |
|||
return message; |
|||
} |
|||
|
|||
public void setMessage(String message) { |
|||
this.message = message; |
|||
} |
|||
|
|||
public String getToken() { |
|||
return token; |
|||
} |
|||
|
|||
public void setToken(String token) { |
|||
this.token = token; |
|||
} |
|||
|
|||
public boolean isNeedBind() { |
|||
return needBind; |
|||
} |
|||
|
|||
public void setNeedBind(boolean needBind) { |
|||
this.needBind = needBind; |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
package com.chenhai.common.core.domain.model; |
|||
|
|||
/** |
|||
* 微信登录请求体 |
|||
*/ |
|||
public class WechatLoginBody { |
|||
private String code; |
|||
private String clientType; // herdsman-app / vet-app |
|||
|
|||
public String getCode() { |
|||
return code; |
|||
} |
|||
|
|||
public void setCode(String code) { |
|||
this.code = code; |
|||
} |
|||
|
|||
public String getClientType() { |
|||
return clientType; |
|||
} |
|||
|
|||
public void setClientType(String clientType) { |
|||
this.clientType = clientType; |
|||
} |
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
package com.chenhai.common.utils; |
|||
|
|||
import com.alibaba.fastjson2.JSONObject; |
|||
import javax.crypto.Cipher; |
|||
import javax.crypto.spec.IvParameterSpec; |
|||
import javax.crypto.spec.SecretKeySpec; |
|||
import java.nio.charset.StandardCharsets; |
|||
import java.util.Base64; |
|||
|
|||
/** |
|||
* 微信解密工具类 |
|||
*/ |
|||
public class WechatDecryptUtil { |
|||
|
|||
/** |
|||
* 解密微信加密数据(手机号) |
|||
*/ |
|||
public static String decryptPhone(String encryptedData, String iv, String sessionKey) { |
|||
try { |
|||
byte[] dataByte = Base64.getDecoder().decode(encryptedData); |
|||
byte[] keyByte = Base64.getDecoder().decode(sessionKey); |
|||
byte[] ivByte = Base64.getDecoder().decode(iv); |
|||
|
|||
// AES-128-CBC 解密 |
|||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); |
|||
SecretKeySpec spec = new SecretKeySpec(keyByte, "AES"); |
|||
IvParameterSpec ivSpec = new IvParameterSpec(ivByte); |
|||
|
|||
cipher.init(Cipher.DECRYPT_MODE, spec, ivSpec); |
|||
byte[] resultByte = cipher.doFinal(dataByte); |
|||
|
|||
String result = new String(resultByte, StandardCharsets.UTF_8); |
|||
|
|||
// 解析JSON获取手机号 |
|||
JSONObject jsonObject = JSONObject.parseObject(result); |
|||
return jsonObject.getString("phoneNumber"); |
|||
|
|||
} catch (Exception e) { |
|||
throw new RuntimeException("微信手机号解密失败", e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 解密用户信息 |
|||
*/ |
|||
public static JSONObject decryptUserInfo(String encryptedData, String iv, String sessionKey) { |
|||
try { |
|||
byte[] dataByte = Base64.getDecoder().decode(encryptedData); |
|||
byte[] keyByte = Base64.getDecoder().decode(sessionKey); |
|||
byte[] ivByte = Base64.getDecoder().decode(iv); |
|||
|
|||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); |
|||
SecretKeySpec spec = new SecretKeySpec(keyByte, "AES"); |
|||
IvParameterSpec ivSpec = new IvParameterSpec(ivByte); |
|||
|
|||
cipher.init(Cipher.DECRYPT_MODE, spec, ivSpec); |
|||
byte[] resultByte = cipher.doFinal(dataByte); |
|||
|
|||
String result = new String(resultByte, StandardCharsets.UTF_8); |
|||
return JSONObject.parseObject(result); |
|||
|
|||
} catch (Exception e) { |
|||
throw new RuntimeException("微信用户信息解密失败", e); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,80 @@ |
|||
package com.chenhai.framework.config; |
|||
|
|||
import com.chenhai.common.core.redis.RedisCache; |
|||
import com.chenhai.framework.security.provider.PhoneAuthenticationProvider; |
|||
import com.chenhai.framework.security.provider.WechatAuthenticationProvider; |
|||
import com.chenhai.framework.web.service.UserDetailsServiceImpl; |
|||
import com.chenhai.framework.web.service.SysPermissionService; |
|||
import com.chenhai.framework.web.service.SysPasswordService; |
|||
import com.chenhai.muhu.service.IUserAuthService; |
|||
import com.chenhai.muhu.service.WechatService; |
|||
import com.chenhai.system.service.ISysConfigService; |
|||
import com.chenhai.system.service.ISysUserService; |
|||
import org.springframework.context.annotation.Bean; |
|||
import org.springframework.context.annotation.Configuration; |
|||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider; |
|||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; |
|||
|
|||
/** |
|||
* 认证提供者配置类 |
|||
*/ |
|||
@Configuration |
|||
public class AuthenticationProviderConfig { |
|||
|
|||
/** |
|||
* 默认的用户名密码认证提供者(用于管理后台) |
|||
*/ |
|||
@Bean |
|||
public DaoAuthenticationProvider daoAuthenticationProvider( |
|||
UserDetailsServiceImpl userDetailsService, |
|||
BCryptPasswordEncoder passwordEncoder) { |
|||
|
|||
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); |
|||
provider.setUserDetailsService(userDetailsService); |
|||
provider.setPasswordEncoder(passwordEncoder); |
|||
provider.setHideUserNotFoundExceptions(false); |
|||
return provider; |
|||
} |
|||
|
|||
/** |
|||
* 手机号认证提供者(用于兽医PC端) |
|||
*/ |
|||
@Bean |
|||
public PhoneAuthenticationProvider phoneAuthenticationProvider( |
|||
IUserAuthService userAuthService, |
|||
ISysUserService userService, |
|||
SysPermissionService permissionService, |
|||
SysPasswordService passwordService, |
|||
BCryptPasswordEncoder passwordEncoder) { |
|||
|
|||
return new PhoneAuthenticationProvider( |
|||
userAuthService, |
|||
userService, |
|||
permissionService, |
|||
passwordService, |
|||
passwordEncoder |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* 微信认证提供者(用于小程序) |
|||
*/ |
|||
@Bean |
|||
public WechatAuthenticationProvider wechatAuthenticationProvider( |
|||
WechatService wechatService, |
|||
IUserAuthService userAuthService, |
|||
ISysUserService userService, |
|||
SysPermissionService permissionService, |
|||
ISysConfigService sysConfigService, |
|||
RedisCache redisCache) { |
|||
|
|||
return new WechatAuthenticationProvider( |
|||
wechatService, |
|||
userAuthService, |
|||
userService, |
|||
permissionService, |
|||
sysConfigService, |
|||
redisCache |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
package com.chenhai.framework.security.exception; |
|||
|
|||
import org.springframework.security.core.AuthenticationException; |
|||
|
|||
public class WechatNeedBindException extends AuthenticationException { |
|||
|
|||
private final String openid; |
|||
private final String tempCode; |
|||
private final String authType; |
|||
private final String userType; // "herdsman" 或 "vet" |
|||
private final String clientType; // "herdsman-app" 或 "vet-app" |
|||
|
|||
public WechatNeedBindException(String msg, String openid, String tempCode, |
|||
String authType, String userType, String clientType) { |
|||
super(msg); |
|||
this.openid = openid; |
|||
this.tempCode = tempCode; |
|||
this.authType = authType; |
|||
this.userType = userType; |
|||
this.clientType = clientType; |
|||
} |
|||
|
|||
// Getters |
|||
public String getOpenid() { return openid; } |
|||
public String getTempCode() { return tempCode; } |
|||
public String getAuthType() { return authType; } |
|||
public String getUserType() { return userType; } |
|||
public String getClientType() { return clientType; } |
|||
} |
|||
@ -0,0 +1,90 @@ |
|||
package com.chenhai.framework.security.provider; |
|||
|
|||
import com.chenhai.common.core.domain.entity.SysUser; |
|||
import com.chenhai.common.core.domain.model.LoginUser; |
|||
import com.chenhai.common.exception.ServiceException; |
|||
import com.chenhai.common.utils.MessageUtils; |
|||
import com.chenhai.framework.security.token.PhoneAuthenticationToken; |
|||
import com.chenhai.framework.web.service.SysPasswordService; |
|||
import com.chenhai.framework.web.service.SysPermissionService; |
|||
import com.chenhai.muhu.service.IUserAuthService; |
|||
import com.chenhai.system.service.ISysUserService; |
|||
import org.springframework.security.authentication.AuthenticationProvider; |
|||
import org.springframework.security.authentication.BadCredentialsException; |
|||
import org.springframework.security.core.Authentication; |
|||
import org.springframework.security.core.AuthenticationException; |
|||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; |
|||
/** |
|||
* 手机号认证Provider |
|||
*/ |
|||
public class PhoneAuthenticationProvider implements AuthenticationProvider { |
|||
|
|||
private final IUserAuthService userAuthService; |
|||
private final ISysUserService userService; |
|||
private final SysPermissionService permissionService; |
|||
private final SysPasswordService passwordService; |
|||
private final BCryptPasswordEncoder passwordEncoder; |
|||
|
|||
public PhoneAuthenticationProvider(IUserAuthService userAuthService, |
|||
ISysUserService userService, |
|||
SysPermissionService permissionService, |
|||
SysPasswordService passwordService, |
|||
BCryptPasswordEncoder passwordEncoder) { |
|||
this.userAuthService = userAuthService; |
|||
this.userService = userService; |
|||
this.permissionService = permissionService; |
|||
this.passwordService = passwordService; |
|||
this.passwordEncoder = passwordEncoder; |
|||
} |
|||
|
|||
@Override |
|||
public Authentication authenticate(Authentication authentication) throws AuthenticationException { |
|||
PhoneAuthenticationToken authToken = (PhoneAuthenticationToken) authentication; |
|||
|
|||
String phone = authToken.getPhone(); |
|||
String password = authToken.getPassword(); |
|||
String clientType = authToken.getClientType(); |
|||
|
|||
// 1. 验证参数 |
|||
if (phone == null || password == null) { |
|||
throw new BadCredentialsException("手机号或密码不能为空"); |
|||
} |
|||
|
|||
// 2. 根据手机号查找用户 |
|||
SysUser user = userAuthService.findUserByAuth("phone", phone); |
|||
|
|||
if (user == null) { |
|||
throw new BadCredentialsException("手机号未注册"); |
|||
} |
|||
|
|||
// 3. 验证用户状态 |
|||
if ("1".equals(user.getStatus())) { |
|||
throw new ServiceException(MessageUtils.message("user.blocked")); |
|||
} |
|||
if ("2".equals(user.getDelFlag())) { |
|||
throw new ServiceException(MessageUtils.message("user.password.delete")); |
|||
} |
|||
|
|||
// 4. 验证密码 |
|||
if (!passwordEncoder.matches(password, user.getPassword())) { |
|||
// 密码错误计数逻辑(复用原有的) |
|||
throw new BadCredentialsException("密码错误"); |
|||
} |
|||
|
|||
// 5. 创建LoginUser |
|||
LoginUser loginUser = new LoginUser( |
|||
user.getUserId(), |
|||
user.getDeptId(), |
|||
user, |
|||
permissionService.getMenuPermission(user), |
|||
clientType |
|||
); |
|||
|
|||
return new PhoneAuthenticationToken(loginUser, loginUser.getAuthorities()); |
|||
} |
|||
|
|||
@Override |
|||
public boolean supports(Class<?> authentication) { |
|||
return PhoneAuthenticationToken.class.isAssignableFrom(authentication); |
|||
} |
|||
} |
|||
@ -0,0 +1,142 @@ |
|||
package com.chenhai.framework.security.provider; |
|||
|
|||
import com.alibaba.fastjson2.JSONObject; |
|||
import com.chenhai.common.core.domain.entity.SysUser; |
|||
import com.chenhai.common.core.domain.model.LoginUser; |
|||
import com.chenhai.common.core.redis.RedisCache; |
|||
import com.chenhai.framework.security.exception.WechatNeedBindException; |
|||
import com.chenhai.framework.security.token.WechatAuthenticationToken; |
|||
import com.chenhai.framework.web.service.SysPermissionService; |
|||
import com.chenhai.system.service.ISysConfigService; |
|||
import com.chenhai.system.service.ISysUserService; |
|||
import com.chenhai.muhu.service.IUserAuthService; |
|||
import com.chenhai.muhu.service.WechatService; |
|||
import org.springframework.security.authentication.AuthenticationProvider; |
|||
import org.springframework.security.authentication.BadCredentialsException; |
|||
import org.springframework.security.core.Authentication; |
|||
import org.springframework.security.core.AuthenticationException; |
|||
import org.springframework.util.DigestUtils; |
|||
|
|||
import java.util.Date; |
|||
import java.util.concurrent.TimeUnit; |
|||
|
|||
/** |
|||
* 微信认证Provider - 统一处理牧户和兽医 |
|||
*/ |
|||
public class WechatAuthenticationProvider implements AuthenticationProvider { |
|||
|
|||
private final WechatService wechatService; |
|||
private final IUserAuthService userAuthService; |
|||
private final ISysUserService userService; |
|||
private final SysPermissionService permissionService; |
|||
private final ISysConfigService sysConfigService; |
|||
private final RedisCache redisCache; |
|||
|
|||
public WechatAuthenticationProvider(WechatService wechatService, |
|||
IUserAuthService userAuthService, |
|||
ISysUserService userService, |
|||
SysPermissionService permissionService, |
|||
ISysConfigService sysConfigService, |
|||
RedisCache redisCache) { |
|||
this.wechatService = wechatService; |
|||
this.userAuthService = userAuthService; |
|||
this.userService = userService; |
|||
this.permissionService = permissionService; |
|||
this.sysConfigService = sysConfigService; |
|||
this.redisCache = redisCache; |
|||
} |
|||
|
|||
@Override |
|||
public Authentication authenticate(Authentication authentication) throws AuthenticationException { |
|||
WechatAuthenticationToken authToken = (WechatAuthenticationToken) authentication; |
|||
|
|||
String code = authToken.getCode(); |
|||
String clientType = authToken.getClientType(); |
|||
|
|||
// 1. 调用微信API获取openid |
|||
JSONObject wechatResult = wechatService.code2session(code, clientType); |
|||
String openid = wechatResult.getString("openid"); |
|||
String sessionKey = wechatResult.getString("session_key"); |
|||
|
|||
if (openid == null) { |
|||
throw new BadCredentialsException("微信登录失败:未获取到openid"); |
|||
} |
|||
|
|||
// 2. 确定认证类型(根据clientType) |
|||
String authType = getAuthTypeByClientType(clientType); |
|||
|
|||
// 3. 查询用户是否已绑定微信 |
|||
SysUser user = userAuthService.findUserByAuth(authType, openid); |
|||
|
|||
// 4. 已绑定用户,直接登录 |
|||
if (user != null) { |
|||
return createSuccessAuthentication(user, clientType); |
|||
} |
|||
|
|||
// 5. 未绑定,需要绑定手机号 |
|||
String tempCode = generateTempCode(openid, authType); |
|||
|
|||
// 存储sessionKey到Redis(5分钟过期) |
|||
redisCache.setCacheObject( |
|||
"wechat:session:" + tempCode, |
|||
sessionKey, |
|||
5, TimeUnit.MINUTES |
|||
); |
|||
|
|||
// 6. 根据客户端类型确定用户类型 |
|||
String userType = getUserTypeByClientType(clientType); |
|||
|
|||
// 7. 抛出需要绑定的异常 |
|||
throw new WechatNeedBindException( |
|||
"需要绑定手机号", |
|||
openid, |
|||
tempCode, |
|||
authType, |
|||
userType, // "herdsman" 或 "vet" |
|||
clientType // "herdsman-app" 或 "vet-app" |
|||
); |
|||
} |
|||
|
|||
@Override |
|||
public boolean supports(Class<?> authentication) { |
|||
return WechatAuthenticationToken.class.isAssignableFrom(authentication); |
|||
} |
|||
|
|||
private String getAuthTypeByClientType(String clientType) { |
|||
switch (clientType) { |
|||
case "herdsman-app": |
|||
return "wechat_muhu"; |
|||
case "vet-app": |
|||
return "wechat_vet"; |
|||
default: |
|||
throw new IllegalArgumentException("不支持的客户端类型: " + clientType); |
|||
} |
|||
} |
|||
|
|||
private String getUserTypeByClientType(String clientType) { |
|||
switch (clientType) { |
|||
case "herdsman-app": |
|||
return "herdsman"; |
|||
case "vet-app": |
|||
return "vet"; |
|||
default: |
|||
throw new IllegalArgumentException("不支持的客户端类型: " + clientType); |
|||
} |
|||
} |
|||
|
|||
private Authentication createSuccessAuthentication(SysUser user, String clientType) { |
|||
LoginUser loginUser = new LoginUser( |
|||
user.getUserId(), |
|||
user.getDeptId(), |
|||
user, |
|||
permissionService.getMenuPermission(user), |
|||
clientType |
|||
); |
|||
return new WechatAuthenticationToken(loginUser, loginUser.getAuthorities()); |
|||
} |
|||
|
|||
private String generateTempCode(String openid, String authType) { |
|||
String raw = openid + authType + System.currentTimeMillis() + Math.random(); |
|||
return DigestUtils.md5DigestAsHex(raw.getBytes()).substring(0, 16); |
|||
} |
|||
} |
|||
@ -0,0 +1,69 @@ |
|||
package com.chenhai.framework.security.token; |
|||
|
|||
import org.springframework.security.authentication.AbstractAuthenticationToken; |
|||
import org.springframework.security.core.GrantedAuthority; |
|||
|
|||
import java.util.Collection; |
|||
|
|||
/** |
|||
* 手机号认证Token |
|||
*/ |
|||
public class PhoneAuthenticationToken extends AbstractAuthenticationToken { |
|||
|
|||
private final String phone; |
|||
private final String password; |
|||
private final String clientType; |
|||
private Object principal; |
|||
|
|||
public PhoneAuthenticationToken(String phone, String password, String clientType) { |
|||
super(null); |
|||
this.phone = phone; |
|||
this.password = password; |
|||
this.clientType = clientType; |
|||
this.setAuthenticated(false); |
|||
} |
|||
|
|||
public PhoneAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) { |
|||
super(authorities); |
|||
this.principal = principal; |
|||
this.phone = null; |
|||
this.password = null; |
|||
this.clientType = null; |
|||
super.setAuthenticated(true); |
|||
} |
|||
|
|||
public String getPhone() { |
|||
return phone; |
|||
} |
|||
|
|||
public String getPassword() { |
|||
return password; |
|||
} |
|||
|
|||
public String getClientType() { |
|||
return clientType; |
|||
} |
|||
|
|||
@Override |
|||
public Object getCredentials() { |
|||
return password; |
|||
} |
|||
|
|||
@Override |
|||
public Object getPrincipal() { |
|||
return principal; |
|||
} |
|||
|
|||
@Override |
|||
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { |
|||
if (isAuthenticated) { |
|||
throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); |
|||
} |
|||
super.setAuthenticated(false); |
|||
} |
|||
|
|||
@Override |
|||
public void eraseCredentials() { |
|||
super.eraseCredentials(); |
|||
} |
|||
} |
|||
@ -0,0 +1,62 @@ |
|||
package com.chenhai.framework.security.token; |
|||
|
|||
import org.springframework.security.authentication.AbstractAuthenticationToken; |
|||
import org.springframework.security.core.GrantedAuthority; |
|||
|
|||
import java.util.Collection; |
|||
|
|||
/** |
|||
* 微信认证Token |
|||
*/ |
|||
public class WechatAuthenticationToken extends AbstractAuthenticationToken { |
|||
|
|||
private final String code; |
|||
private final String clientType; |
|||
private Object principal; |
|||
|
|||
public WechatAuthenticationToken(String code, String clientType) { |
|||
super(null); |
|||
this.code = code; |
|||
this.clientType = clientType; |
|||
this.setAuthenticated(false); |
|||
} |
|||
|
|||
public WechatAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) { |
|||
super(authorities); |
|||
this.principal = principal; |
|||
this.code = null; |
|||
this.clientType = null; |
|||
super.setAuthenticated(true); |
|||
} |
|||
|
|||
public String getCode() { |
|||
return code; |
|||
} |
|||
|
|||
public String getClientType() { |
|||
return clientType; |
|||
} |
|||
|
|||
@Override |
|||
public Object getCredentials() { |
|||
return null; |
|||
} |
|||
|
|||
@Override |
|||
public Object getPrincipal() { |
|||
return principal; |
|||
} |
|||
|
|||
@Override |
|||
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { |
|||
if (isAuthenticated) { |
|||
throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); |
|||
} |
|||
super.setAuthenticated(false); |
|||
} |
|||
|
|||
@Override |
|||
public void eraseCredentials() { |
|||
super.eraseCredentials(); |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
package com.chenhai.framework.web.service; |
|||
|
|||
/** |
|||
* 短信服务接口 |
|||
*/ |
|||
public interface SysSmsService { |
|||
|
|||
/** |
|||
* 发送短信验证码 |
|||
*/ |
|||
boolean sendSmsCode(String phone); |
|||
|
|||
/** |
|||
* 验证短信验证码 |
|||
*/ |
|||
boolean verifySmsCode(String phone, String code); |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
package com.chenhai.framework.web.service; |
|||
|
|||
import com.chenhai.framework.web.service.SysSmsService; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
/** |
|||
* 短信服务实现(临时实现,用于绕过依赖检查) |
|||
*/ |
|||
@Service |
|||
public class SysSmsServiceImpl implements SysSmsService { |
|||
|
|||
@Override |
|||
public boolean sendSmsCode(String phone) { |
|||
// 临时实现,直接返回true |
|||
// TODO: 后续实现真正的短信发送逻辑 |
|||
System.out.println("发送短信验证码到: " + phone + " (模拟)"); |
|||
return true; |
|||
} |
|||
|
|||
@Override |
|||
public boolean verifySmsCode(String phone, String code) { |
|||
// 临时实现,验证码固定为"123456" |
|||
// TODO: 后续实现真正的短信验证逻辑 |
|||
System.out.println("验证短信验证码: phone=" + phone + ", code=" + code); |
|||
return "123456".equals(code); |
|||
} |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
package com.chenhai.muhu.domain.model; |
|||
|
|||
|
|||
import jakarta.validation.constraints.NotBlank; |
|||
|
|||
/** |
|||
* 牧户微信登录请求体 |
|||
*/ |
|||
public class MuhuWxLoginBody { |
|||
|
|||
@NotBlank(message = "微信code不能为空") |
|||
private String code; // 微信登录code |
|||
private String encryptedData; // 加密数据 |
|||
private String iv; // 加密向量 |
|||
private String nickName; // 昵称(可选) |
|||
private String avatarUrl; // 头像(可选) |
|||
|
|||
public String getCode() { |
|||
return code; |
|||
} |
|||
|
|||
public void setCode(String code) { |
|||
this.code = code; |
|||
} |
|||
|
|||
public String getEncryptedData() { |
|||
return encryptedData; |
|||
} |
|||
|
|||
public void setEncryptedData(String encryptedData) { |
|||
this.encryptedData = encryptedData; |
|||
} |
|||
|
|||
public String getIv() { |
|||
return iv; |
|||
} |
|||
|
|||
public void setIv(String iv) { |
|||
this.iv = iv; |
|||
} |
|||
|
|||
public String getNickName() { |
|||
return nickName; |
|||
} |
|||
|
|||
public void setNickName(String nickName) { |
|||
this.nickName = nickName; |
|||
} |
|||
|
|||
public String getAvatarUrl() { |
|||
return avatarUrl; |
|||
} |
|||
|
|||
public void setAvatarUrl(String avatarUrl) { |
|||
this.avatarUrl = avatarUrl; |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
package com.chenhai.muhu.service; |
|||
|
|||
import com.chenhai.common.core.domain.entity.SysUser; |
|||
import com.chenhai.common.core.domain.entity.SysUserAuth; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 用户认证方式服务接口 |
|||
*/ |
|||
public interface IUserAuthService { |
|||
/** |
|||
* 根据认证类型和标识查找用户 |
|||
*/ |
|||
SysUser findUserByAuth(String authType, String authKey); |
|||
|
|||
/** |
|||
* 绑定新的认证方式 |
|||
*/ |
|||
int bindAuth(Long userId, String authType, String authKey, String authSecret); |
|||
|
|||
/** |
|||
* 解绑认证方式 |
|||
*/ |
|||
int unbindAuth(Long userId, String authType); |
|||
|
|||
/** |
|||
* 获取用户的所有认证方式 |
|||
*/ |
|||
List<SysUserAuth> getUserAuths(Long userId); |
|||
|
|||
/** |
|||
* 检查认证方式是否存在 |
|||
*/ |
|||
boolean checkAuthExists(String authType, String authKey); |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
package com.chenhai.muhu.service; |
|||
|
|||
import com.alibaba.fastjson2.JSONObject; |
|||
|
|||
/** |
|||
* 微信服务接口 |
|||
*/ |
|||
public interface WechatService { |
|||
/** |
|||
* 小程序登录,获取openid和session_key |
|||
*/ |
|||
JSONObject code2session(String code, String clientType); |
|||
|
|||
/** |
|||
* 获取微信小程序配置 |
|||
*/ |
|||
MiniProgramConfig getMiniProgramConfig(String clientType); |
|||
|
|||
/** |
|||
* 微信小程序配置类 |
|||
*/ |
|||
class MiniProgramConfig { |
|||
private String appId; |
|||
private String appSecret; |
|||
|
|||
public MiniProgramConfig(String appId, String appSecret) { |
|||
this.appId = appId; |
|||
this.appSecret = appSecret; |
|||
} |
|||
|
|||
public String getAppId() { |
|||
return appId; |
|||
} |
|||
|
|||
public String getAppSecret() { |
|||
return appSecret; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,79 @@ |
|||
package com.chenhai.muhu.service.impl; |
|||
|
|||
import com.chenhai.common.core.domain.entity.SysUser; |
|||
import com.chenhai.common.core.domain.entity.SysUserAuth; |
|||
import com.chenhai.muhu.service.IUserAuthService; |
|||
import com.chenhai.system.mapper.SysUserAuthMapper; |
|||
import com.chenhai.system.mapper.SysUserMapper; |
|||
import com.chenhai.system.service.ISysUserService; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.stereotype.Service; |
|||
import org.springframework.transaction.annotation.Transactional; |
|||
|
|||
import java.util.Date; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 用户认证方式服务实现 |
|||
*/ |
|||
@Service |
|||
public class UserAuthServiceImpl implements IUserAuthService { |
|||
|
|||
@Autowired |
|||
private SysUserAuthMapper sysUserAuthMapper; |
|||
|
|||
@Autowired |
|||
private SysUserMapper sysUserMapper; |
|||
|
|||
@Override |
|||
public SysUser findUserByAuth(String authType, String authKey) { |
|||
SysUserAuth auth = sysUserAuthMapper.selectByAuthTypeAndKey(authType, authKey); |
|||
if (auth == null) { |
|||
return null; |
|||
} |
|||
return sysUserMapper.selectUserById(auth.getUserId()); |
|||
} |
|||
|
|||
@Override |
|||
@Transactional |
|||
public int bindAuth(Long userId, String authType, String authKey, String authSecret) { |
|||
// 检查是否已存在 |
|||
SysUserAuth existAuth = sysUserAuthMapper.selectByAuthTypeAndKey(authType, authKey); |
|||
if (existAuth != null) { |
|||
throw new RuntimeException("该认证方式已被其他用户绑定"); |
|||
} |
|||
|
|||
SysUserAuth auth = new SysUserAuth(); |
|||
auth.setUserId(userId); |
|||
auth.setAuthType(authType); |
|||
auth.setAuthKey(authKey); |
|||
auth.setAuthSecret(authSecret); |
|||
auth.setStatus("0"); |
|||
auth.setCreateTime(new Date()); |
|||
|
|||
return sysUserAuthMapper.insertSysUserAuth(auth); |
|||
} |
|||
|
|||
@Override |
|||
@Transactional |
|||
public int unbindAuth(Long userId, String authType) { |
|||
List<SysUserAuth> auths = sysUserAuthMapper.selectByUserIdAndType(userId, authType); |
|||
if (auths.isEmpty()) { |
|||
return 0; |
|||
} |
|||
|
|||
Long[] authIds = auths.stream().map(SysUserAuth::getAuthId).toArray(Long[]::new); |
|||
return sysUserAuthMapper.deleteSysUserAuthByAuthIds(authIds); |
|||
} |
|||
|
|||
@Override |
|||
public List<SysUserAuth> getUserAuths(Long userId) { |
|||
return sysUserAuthMapper.selectByUserId(userId); |
|||
} |
|||
|
|||
@Override |
|||
public boolean checkAuthExists(String authType, String authKey) { |
|||
SysUserAuth auth = sysUserAuthMapper.selectByAuthTypeAndKey(authType, authKey); |
|||
return auth != null; |
|||
} |
|||
} |
|||
@ -0,0 +1,76 @@ |
|||
package com.chenhai.muhu.service.impl; |
|||
|
|||
import com.alibaba.fastjson2.JSONObject; |
|||
import com.chenhai.common.utils.StringUtils; |
|||
import com.chenhai.common.utils.http.HttpUtils; |
|||
import com.chenhai.muhu.service.WechatService; |
|||
import org.slf4j.Logger; |
|||
import org.slf4j.LoggerFactory; |
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
/** |
|||
* 微信服务实现 |
|||
*/ |
|||
@Service |
|||
public class WechatServiceImpl implements WechatService { |
|||
|
|||
private static final Logger log = LoggerFactory.getLogger(WechatServiceImpl.class); |
|||
|
|||
// 微信API地址 |
|||
private static final String WECHAT_API_URL = "https://api.weixin.qq.com/sns/jscode2session"; |
|||
|
|||
@Value("${wx.muhu.app-id:}") |
|||
private String muhuAppId; |
|||
|
|||
@Value("${wx.muhu.app-secret:}") |
|||
private String muhuAppSecret; |
|||
|
|||
@Value("${wx.vet.app-id:}") |
|||
private String vetAppId; |
|||
|
|||
@Value("${wx.vet.app-secret:}") |
|||
private String vetAppSecret; |
|||
|
|||
@Override |
|||
public JSONObject code2session(String code, String clientType) { |
|||
MiniProgramConfig config = getMiniProgramConfig(clientType); |
|||
if (config == null) { |
|||
throw new RuntimeException("未找到微信小程序配置"); |
|||
} |
|||
|
|||
String url = String.format("%s?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code", |
|||
WECHAT_API_URL, config.getAppId(), config.getAppSecret(), code); |
|||
|
|||
try { |
|||
String response = HttpUtils.sendGet(url); |
|||
log.info("微信API响应: {}", response); |
|||
|
|||
JSONObject result = JSONObject.parseObject(response); |
|||
if (result.containsKey("errcode")) { |
|||
throw new RuntimeException("微信API调用失败: " + result.getString("errmsg")); |
|||
} |
|||
|
|||
return result; |
|||
} catch (Exception e) { |
|||
log.error("调用微信API失败", e); |
|||
throw new RuntimeException("微信登录失败: " + e.getMessage()); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public MiniProgramConfig getMiniProgramConfig(String clientType) { |
|||
if ("herdsman-app".equals(clientType)) { |
|||
if (StringUtils.isEmpty(muhuAppId) || StringUtils.isEmpty(muhuAppSecret)) { |
|||
throw new RuntimeException("牧户小程序配置未设置"); |
|||
} |
|||
return new MiniProgramConfig(muhuAppId, muhuAppSecret); |
|||
} else if ("vet-app".equals(clientType)) { |
|||
if (StringUtils.isEmpty(vetAppId) || StringUtils.isEmpty(vetAppSecret)) { |
|||
throw new RuntimeException("兽医小程序配置未设置"); |
|||
} |
|||
return new MiniProgramConfig(vetAppId, vetAppSecret); |
|||
} |
|||
throw new RuntimeException("不支持的客户端类型: " + clientType); |
|||
} |
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
package com.chenhai.system.mapper; |
|||
|
|||
import com.chenhai.common.core.domain.entity.SysUserAuth; |
|||
import org.apache.ibatis.annotations.Param; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 用户认证方式Mapper接口 |
|||
*/ |
|||
public interface SysUserAuthMapper { |
|||
/** |
|||
* 查询用户认证方式 |
|||
*/ |
|||
SysUserAuth selectSysUserAuthByAuthId(Long authId); |
|||
|
|||
/** |
|||
* 根据认证类型和标识查询 |
|||
*/ |
|||
SysUserAuth selectByAuthTypeAndKey(@Param("authType") String authType, @Param("authKey") String authKey); |
|||
|
|||
/** |
|||
* 查询用户的所有认证方式 |
|||
*/ |
|||
List<SysUserAuth> selectByUserId(Long userId); |
|||
|
|||
/** |
|||
* 查询用户特定类型的认证方式 |
|||
*/ |
|||
List<SysUserAuth> selectByUserIdAndType(@Param("userId") Long userId, @Param("authType") String authType); |
|||
|
|||
/** |
|||
* 新增用户认证方式 |
|||
*/ |
|||
int insertSysUserAuth(SysUserAuth sysUserAuth); |
|||
|
|||
/** |
|||
* 修改用户认证方式 |
|||
*/ |
|||
int updateSysUserAuth(SysUserAuth sysUserAuth); |
|||
|
|||
/** |
|||
* 删除用户认证方式 |
|||
*/ |
|||
int deleteSysUserAuthByAuthId(Long authId); |
|||
|
|||
/** |
|||
* 批量删除用户认证方式 |
|||
*/ |
|||
int deleteSysUserAuthByAuthIds(Long[] authIds); |
|||
|
|||
/** |
|||
* 删除用户的所有认证方式 |
|||
*/ |
|||
int deleteByUserId(Long userId); |
|||
} |
|||
@ -0,0 +1,97 @@ |
|||
<?xml version="1.0" encoding="UTF-8" ?> |
|||
<!DOCTYPE mapper |
|||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" |
|||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
|||
<mapper namespace="com.chenhai.system.mapper.SysUserAuthMapper"> |
|||
|
|||
<resultMap type="SysUserAuth" id="SysUserAuthResult"> |
|||
<id property="authId" column="auth_id"/> |
|||
<result property="userId" column="user_id"/> |
|||
<result property="authType" column="auth_type"/> |
|||
<result property="authKey" column="auth_key"/> |
|||
<result property="authSecret" column="auth_secret"/> |
|||
<result property="status" column="status"/> |
|||
<result property="createTime" column="create_time"/> |
|||
<result property="updateTime" column="update_time"/> |
|||
</resultMap> |
|||
|
|||
<sql id="selectSysUserAuthVo"> |
|||
select auth_id, user_id, auth_type, auth_key, auth_secret, status, create_time, update_time |
|||
from sys_user_auth |
|||
</sql> |
|||
|
|||
<select id="selectSysUserAuthByAuthId" parameterType="Long" resultMap="SysUserAuthResult"> |
|||
<include refid="selectSysUserAuthVo"/> |
|||
where auth_id = #{authId} |
|||
</select> |
|||
|
|||
<select id="selectByAuthTypeAndKey" resultMap="SysUserAuthResult"> |
|||
<include refid="selectSysUserAuthVo"/> |
|||
where auth_type = #{authType} and auth_key = #{authKey} |
|||
limit 1 |
|||
</select> |
|||
|
|||
<select id="selectByUserId" parameterType="Long" resultMap="SysUserAuthResult"> |
|||
<include refid="selectSysUserAuthVo"/> |
|||
where user_id = #{userId} |
|||
order by create_time desc |
|||
</select> |
|||
|
|||
<select id="selectByUserIdAndType" resultMap="SysUserAuthResult"> |
|||
<include refid="selectSysUserAuthVo"/> |
|||
where user_id = #{userId} and auth_type = #{authType} |
|||
order by create_time desc |
|||
</select> |
|||
|
|||
<insert id="insertSysUserAuth" parameterType="SysUserAuth" useGeneratedKeys="true" keyProperty="authId"> |
|||
insert into sys_user_auth |
|||
<trim prefix="(" suffix=")" suffixOverrides=","> |
|||
<if test="userId != null">user_id,</if> |
|||
<if test="authType != null and authType != ''">auth_type,</if> |
|||
<if test="authKey != null and authKey != ''">auth_key,</if> |
|||
<if test="authSecret != null and authSecret != ''">auth_secret,</if> |
|||
<if test="status != null">status,</if> |
|||
<if test="createTime != null">create_time,</if> |
|||
<if test="updateTime != null">update_time,</if> |
|||
</trim> |
|||
<trim prefix="values (" suffix=")" suffixOverrides=","> |
|||
<if test="userId != null">#{userId},</if> |
|||
<if test="authType != null and authType != ''">#{authType},</if> |
|||
<if test="authKey != null and authKey != ''">#{authKey},</if> |
|||
<if test="authSecret != null and authSecret != ''">#{authSecret},</if> |
|||
<if test="status != null">#{status},</if> |
|||
<if test="createTime != null">#{createTime},</if> |
|||
<if test="updateTime != null">#{updateTime},</if> |
|||
</trim> |
|||
</insert> |
|||
|
|||
<update id="updateSysUserAuth" parameterType="SysUserAuth"> |
|||
update sys_user_auth |
|||
<set> |
|||
<if test="userId != null">user_id = #{userId},</if> |
|||
<if test="authType != null and authType != ''">auth_type = #{authType},</if> |
|||
<if test="authKey != null and authKey != ''">auth_key = #{authKey},</if> |
|||
<if test="authSecret != null and authSecret != ''">auth_secret = #{authSecret},</if> |
|||
<if test="status != null">status = #{status},</if> |
|||
<if test="createTime != null">create_time = #{createTime},</if> |
|||
<if test="updateTime != null">update_time = #{updateTime},</if> |
|||
</set> |
|||
where auth_id = #{authId} |
|||
</update> |
|||
|
|||
<delete id="deleteSysUserAuthByAuthId" parameterType="Long"> |
|||
delete from sys_user_auth where auth_id = #{authId} |
|||
</delete> |
|||
|
|||
<delete id="deleteSysUserAuthByAuthIds" parameterType="Long"> |
|||
delete from sys_user_auth where auth_id in |
|||
<foreach collection="array" item="authId" open="(" separator="," close=")"> |
|||
#{authId} |
|||
</foreach> |
|||
</delete> |
|||
|
|||
<delete id="deleteByUserId" parameterType="Long"> |
|||
delete from sys_user_auth where user_id = #{userId} |
|||
</delete> |
|||
|
|||
</mapper> |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue