diff --git a/chenhai-admin/pom.xml b/chenhai-admin/pom.xml index dd3420c..c48e98b 100644 --- a/chenhai-admin/pom.xml +++ b/chenhai-admin/pom.xml @@ -54,6 +54,12 @@ chenhai-generator + + org.projectlombok + lombok + true + + diff --git a/chenhai-admin/src/main/java/com/chenhai/web/controller/auth/MultiAuthController.java b/chenhai-admin/src/main/java/com/chenhai/web/controller/auth/MultiAuthController.java new file mode 100644 index 0000000..9804c20 --- /dev/null +++ b/chenhai-admin/src/main/java/com/chenhai/web/controller/auth/MultiAuthController.java @@ -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 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); + } +} \ No newline at end of file diff --git a/chenhai-admin/src/main/resources/application.yml b/chenhai-admin/src/main/resources/application.yml index 1563c3b..7a17ed1 100644 --- a/chenhai-admin/src/main/resources/application.yml +++ b/chenhai-admin/src/main/resources/application.yml @@ -94,9 +94,22 @@ token: # 令牌自定义标识 header: Authorization # 令牌密钥 - secret: abcdefghijklmnopqrstuvwxyz +# secret: abcdefghijklmnopqrstuvwxyz + secret: e8f5b8c9d2a1f3e5c7b9d1a3f5c7e9b1d3a5f7c9e1b3d5a7f9c1e3b5d7a9f1c3e5b7d9a1f3c5e7b9d1a3f5c7e9b1d3a5f7c9e1b3d5a7f9c1e3b5d7a9f1c3e5b7d9a1f3c5 # 令牌有效期(默认30分钟) expireTime: 30 + # 牧户30天(30*24*3600秒) + + +# application.yml +# 微信小程序配置 +wx: + muhu: + app-id: wxb5becc8d6d8123a6 + app-secret: 74f4211d3985aa782ff9148aa00f824e + vet: + app-id: ${WX_MINI_APPID:your_app_id} + app-secret: ${WX_MINI_SECRET:your_app_secret} # MyBatis配置 mybatis: diff --git a/chenhai-common/src/main/java/com/chenhai/common/core/domain/entity/SysUser.java b/chenhai-common/src/main/java/com/chenhai/common/core/domain/entity/SysUser.java index 72c543a..f5c55e9 100644 --- a/chenhai-common/src/main/java/com/chenhai/common/core/domain/entity/SysUser.java +++ b/chenhai-common/src/main/java/com/chenhai/common/core/domain/entity/SysUser.java @@ -38,6 +38,10 @@ public class SysUser extends BaseEntity @Excel(name = "用户名称") private String nickName; + /** 用户类型 */ + @Excel(name = "用户类型", readConverterExp = "00=系统用户,01=注册用户") + private String userType; + /** 用户邮箱 */ @Excel(name = "用户邮箱") private String email; @@ -145,6 +149,14 @@ public class SysUser extends BaseEntity this.nickName = nickName; } + public String getUserType() { + return userType; + } + + public void setUserType(String userType) { + this.userType = userType; + } + @Xss(message = "用户账号不能包含脚本字符") @NotBlank(message = "用户账号不能为空") @Size(min = 0, max = 30, message = "用户账号长度不能超过30个字符") diff --git a/chenhai-common/src/main/java/com/chenhai/common/core/domain/entity/SysUserAuth.java b/chenhai-common/src/main/java/com/chenhai/common/core/domain/entity/SysUserAuth.java new file mode 100644 index 0000000..ffde8dd --- /dev/null +++ b/chenhai-common/src/main/java/com/chenhai/common/core/domain/entity/SysUserAuth.java @@ -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(); + } +} \ No newline at end of file diff --git a/chenhai-common/src/main/java/com/chenhai/common/core/domain/model/LoginUser.java b/chenhai-common/src/main/java/com/chenhai/common/core/domain/model/LoginUser.java index e3d83bb..16f3c89 100644 --- a/chenhai-common/src/main/java/com/chenhai/common/core/domain/model/LoginUser.java +++ b/chenhai-common/src/main/java/com/chenhai/common/core/domain/model/LoginUser.java @@ -71,6 +71,12 @@ public class LoginUser implements UserDetails */ private SysUser user; + /** + * 客户端类型: admin/vet-pc/vet-app/herdsman-app + */ + private String clientType; + + public LoginUser() { } @@ -89,6 +95,15 @@ public class LoginUser implements UserDetails this.permissions = permissions; } + // 修改构造函数,支持clientType + public LoginUser(Long userId, Long deptId, SysUser user, Set permissions, String clientType) { + this.userId = userId; + this.deptId = deptId; + this.user = user; + this.permissions = permissions; + this.clientType = clientType; + } + public Long getUserId() { return userId; @@ -258,6 +273,14 @@ public class LoginUser implements UserDetails this.user = user; } + public String getClientType() { + return clientType; + } + + public void setClientType(String clientType) { + this.clientType = clientType; + } + @Override public Collection getAuthorities() { diff --git a/chenhai-common/src/main/java/com/chenhai/common/core/domain/model/PhoneDecryptRequest.java b/chenhai-common/src/main/java/com/chenhai/common/core/domain/model/PhoneDecryptRequest.java new file mode 100644 index 0000000..79ef2ad --- /dev/null +++ b/chenhai-common/src/main/java/com/chenhai/common/core/domain/model/PhoneDecryptRequest.java @@ -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; + } +} \ No newline at end of file diff --git a/chenhai-common/src/main/java/com/chenhai/common/core/domain/model/PhoneLoginBody.java b/chenhai-common/src/main/java/com/chenhai/common/core/domain/model/PhoneLoginBody.java new file mode 100644 index 0000000..b05ccb8 --- /dev/null +++ b/chenhai-common/src/main/java/com/chenhai/common/core/domain/model/PhoneLoginBody.java @@ -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; + } +} \ No newline at end of file diff --git a/chenhai-common/src/main/java/com/chenhai/common/core/domain/model/WechatBindRequest.java b/chenhai-common/src/main/java/com/chenhai/common/core/domain/model/WechatBindRequest.java new file mode 100644 index 0000000..71c04ca --- /dev/null +++ b/chenhai-common/src/main/java/com/chenhai/common/core/domain/model/WechatBindRequest.java @@ -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; + } +} \ No newline at end of file diff --git a/chenhai-common/src/main/java/com/chenhai/common/core/domain/model/WechatBindResult.java b/chenhai-common/src/main/java/com/chenhai/common/core/domain/model/WechatBindResult.java new file mode 100644 index 0000000..5b8cfed --- /dev/null +++ b/chenhai-common/src/main/java/com/chenhai/common/core/domain/model/WechatBindResult.java @@ -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; + } +} \ No newline at end of file diff --git a/chenhai-common/src/main/java/com/chenhai/common/core/domain/model/WechatLoginBody.java b/chenhai-common/src/main/java/com/chenhai/common/core/domain/model/WechatLoginBody.java new file mode 100644 index 0000000..b3c5bfb --- /dev/null +++ b/chenhai-common/src/main/java/com/chenhai/common/core/domain/model/WechatLoginBody.java @@ -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; + } +} \ No newline at end of file diff --git a/chenhai-common/src/main/java/com/chenhai/common/utils/WechatDecryptUtil.java b/chenhai-common/src/main/java/com/chenhai/common/utils/WechatDecryptUtil.java new file mode 100644 index 0000000..d6bc13b --- /dev/null +++ b/chenhai-common/src/main/java/com/chenhai/common/utils/WechatDecryptUtil.java @@ -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); + } + } +} \ No newline at end of file diff --git a/chenhai-framework/pom.xml b/chenhai-framework/pom.xml index dfbc74b..22eaf12 100644 --- a/chenhai-framework/pom.xml +++ b/chenhai-framework/pom.xml @@ -59,6 +59,12 @@ chenhai-system + + org.projectlombok + lombok + true + + \ No newline at end of file diff --git a/chenhai-framework/src/main/java/com/chenhai/framework/config/AuthenticationProviderConfig.java b/chenhai-framework/src/main/java/com/chenhai/framework/config/AuthenticationProviderConfig.java new file mode 100644 index 0000000..61746a1 --- /dev/null +++ b/chenhai-framework/src/main/java/com/chenhai/framework/config/AuthenticationProviderConfig.java @@ -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 + ); + } +} \ No newline at end of file diff --git a/chenhai-framework/src/main/java/com/chenhai/framework/config/SecurityConfig.java b/chenhai-framework/src/main/java/com/chenhai/framework/config/SecurityConfig.java index ca37547..2e9cdff 100644 --- a/chenhai-framework/src/main/java/com/chenhai/framework/config/SecurityConfig.java +++ b/chenhai-framework/src/main/java/com/chenhai/framework/config/SecurityConfig.java @@ -1,10 +1,17 @@ package com.chenhai.framework.config; +import com.chenhai.framework.security.provider.PhoneAuthenticationProvider; +import com.chenhai.framework.security.provider.WechatAuthenticationProvider; +import com.chenhai.framework.web.service.UserDetailsServiceImpl; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -19,11 +26,16 @@ import com.chenhai.framework.security.filter.JwtAuthenticationTokenFilter; import com.chenhai.framework.security.handle.AuthenticationEntryPointImpl; import com.chenhai.framework.security.handle.LogoutSuccessHandlerImpl; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + /** * spring security配置 * * @author ruoyi */ +@Slf4j @EnableMethodSecurity(prePostEnabled = true, securedEnabled = true) @Configuration public class SecurityConfig @@ -58,14 +70,46 @@ public class SecurityConfig @Autowired private PermitAllUrlProperties permitAllUrl; + @Autowired + private UserDetailsServiceImpl userDetailsService; + + /** * 身份验证实现 */ - @Bean - public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception - { - return authenticationConfiguration.getAuthenticationManager(); - } +// @Bean +// public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception +// { +// return authenticationConfiguration.getAuthenticationManager(); +// } + @Bean + public AuthenticationManager authenticationManager( + DaoAuthenticationProvider daoAuthenticationProvider, + PhoneAuthenticationProvider phoneAuthenticationProvider, + WechatAuthenticationProvider wechatAuthenticationProvider) { + + // 组合所有Provider + List providers = new ArrayList<>(); + providers.add(daoAuthenticationProvider); // 管理端用户名密码 + + // 添加手机号认证 + if (phoneAuthenticationProvider != null) { + providers.add(phoneAuthenticationProvider); + log.info("PhoneAuthenticationProvider loaded successfully"); + } else { + log.warn("PhoneAuthenticationProvider is not available"); + } + + // 添加微信认证 + if (wechatAuthenticationProvider != null) { + providers.add(wechatAuthenticationProvider); + log.info("WechatAuthenticationProvider loaded successfully"); + } else { + log.warn("WechatAuthenticationProvider is not available"); + } + + return new ProviderManager(providers); + } /** * anyRequest | 匹配所有请求路径 @@ -101,6 +145,7 @@ public class SecurityConfig permitAllUrl.getUrls().forEach(url -> requests.requestMatchers(url).permitAll()); // 对于登录login 注册register 验证码captchaImage 允许匿名访问 requests.requestMatchers("/login", "/register", "/captchaImage").permitAll() + .requestMatchers("/auth/**").permitAll() // 静态资源,可匿名访问 .requestMatchers(HttpMethod.GET, "/", "/*.html", "/**.html", "/**.css", "/**.js", "/profile/**").permitAll() .requestMatchers("/swagger-ui.html", "/v3/api-docs/**", "/swagger-ui/**", "/druid/**").permitAll() diff --git a/chenhai-framework/src/main/java/com/chenhai/framework/security/exception/WechatNeedBindException.java b/chenhai-framework/src/main/java/com/chenhai/framework/security/exception/WechatNeedBindException.java new file mode 100644 index 0000000..cc43880 --- /dev/null +++ b/chenhai-framework/src/main/java/com/chenhai/framework/security/exception/WechatNeedBindException.java @@ -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; } +} \ No newline at end of file diff --git a/chenhai-framework/src/main/java/com/chenhai/framework/security/provider/PhoneAuthenticationProvider.java b/chenhai-framework/src/main/java/com/chenhai/framework/security/provider/PhoneAuthenticationProvider.java new file mode 100644 index 0000000..df54b92 --- /dev/null +++ b/chenhai-framework/src/main/java/com/chenhai/framework/security/provider/PhoneAuthenticationProvider.java @@ -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); + } +} \ No newline at end of file diff --git a/chenhai-framework/src/main/java/com/chenhai/framework/security/provider/WechatAuthenticationProvider.java b/chenhai-framework/src/main/java/com/chenhai/framework/security/provider/WechatAuthenticationProvider.java new file mode 100644 index 0000000..7652dbc --- /dev/null +++ b/chenhai-framework/src/main/java/com/chenhai/framework/security/provider/WechatAuthenticationProvider.java @@ -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); + } +} \ No newline at end of file diff --git a/chenhai-framework/src/main/java/com/chenhai/framework/security/token/PhoneAuthenticationToken.java b/chenhai-framework/src/main/java/com/chenhai/framework/security/token/PhoneAuthenticationToken.java new file mode 100644 index 0000000..1e112f7 --- /dev/null +++ b/chenhai-framework/src/main/java/com/chenhai/framework/security/token/PhoneAuthenticationToken.java @@ -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 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(); + } +} \ No newline at end of file diff --git a/chenhai-framework/src/main/java/com/chenhai/framework/security/token/WechatAuthenticationToken.java b/chenhai-framework/src/main/java/com/chenhai/framework/security/token/WechatAuthenticationToken.java new file mode 100644 index 0000000..1c3c868 --- /dev/null +++ b/chenhai-framework/src/main/java/com/chenhai/framework/security/token/WechatAuthenticationToken.java @@ -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 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(); + } +} \ No newline at end of file diff --git a/chenhai-framework/src/main/java/com/chenhai/framework/web/service/SysSmsService.java b/chenhai-framework/src/main/java/com/chenhai/framework/web/service/SysSmsService.java new file mode 100644 index 0000000..4f4936d --- /dev/null +++ b/chenhai-framework/src/main/java/com/chenhai/framework/web/service/SysSmsService.java @@ -0,0 +1,17 @@ +package com.chenhai.framework.web.service; + +/** + * 短信服务接口 + */ +public interface SysSmsService { + + /** + * 发送短信验证码 + */ + boolean sendSmsCode(String phone); + + /** + * 验证短信验证码 + */ + boolean verifySmsCode(String phone, String code); +} \ No newline at end of file diff --git a/chenhai-framework/src/main/java/com/chenhai/framework/web/service/SysSmsServiceImpl.java b/chenhai-framework/src/main/java/com/chenhai/framework/web/service/SysSmsServiceImpl.java new file mode 100644 index 0000000..facb6d3 --- /dev/null +++ b/chenhai-framework/src/main/java/com/chenhai/framework/web/service/SysSmsServiceImpl.java @@ -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); + } +} \ No newline at end of file diff --git a/chenhai-framework/src/main/java/com/chenhai/framework/web/service/TokenService.java b/chenhai-framework/src/main/java/com/chenhai/framework/web/service/TokenService.java index ce9840b..80b227c 100644 --- a/chenhai-framework/src/main/java/com/chenhai/framework/web/service/TokenService.java +++ b/chenhai-framework/src/main/java/com/chenhai/framework/web/service/TokenService.java @@ -145,13 +145,25 @@ public class TokenService * * @param loginUser 登录信息 */ - public void refreshToken(LoginUser loginUser) - { +// public void refreshToken(LoginUser loginUser) +// { +// loginUser.setLoginTime(System.currentTimeMillis()); +// loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE); +// // 根据uuid将loginUser缓存 +// String userKey = getTokenKey(loginUser.getToken()); +// redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES); +// } + // 2. 修改refreshToken方法,支持多端 + public void refreshToken(LoginUser loginUser) { loginUser.setLoginTime(System.currentTimeMillis()); - loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE); + + // 根据客户端类型设置不同的过期时间 + int customExpireTime = getExpireTimeByClientType(loginUser.getClientType()); + loginUser.setExpireTime(loginUser.getLoginTime() + customExpireTime * MILLIS_MINUTE); + // 根据uuid将loginUser缓存 String userKey = getTokenKey(loginUser.getToken()); - redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES); + redisCache.setCacheObject(userKey, loginUser, customExpireTime, TimeUnit.MINUTES); } /** @@ -229,4 +241,47 @@ public class TokenService { return CacheConstants.LOGIN_TOKEN_KEY + uuid; } + + /** + * 根据客户端类型获取token过期时间(分钟) + */ + private int getExpireTimeByClientType(String clientType) { + if (clientType == null) { + return expireTime; // 默认30分钟 + } + + switch(clientType) { + case "herdsman-app": + // 从配置读取牧户token时长,默认30天 + return 43200; // 30天(分钟) + case "vet-app": + return 10080; // 7天 + case "vet-pc": + return 480; // 8小时 + case "admin": + default: + return expireTime; // 30分钟 + } + } + + // 修改createToken方法,使用客户端类型决定过期时间 +// public String createToken(LoginUser loginUser) { +// String token = IdUtils.fastUUID(); +// loginUser.setToken(token); +// setUserAgent(loginUser); +// +// // 根据客户端类型设置不同的过期时间 +// int customExpireTime = getExpireTimeByClientType(loginUser.getClientType()); +// loginUser.setLoginTime(System.currentTimeMillis()); +// loginUser.setExpireTime(loginUser.getLoginTime() + customExpireTime * MILLIS_MINUTE); +// +// // 根据uuid将loginUser缓存 +// String userKey = getTokenKey(loginUser.getToken()); +// redisCache.setCacheObject(userKey, loginUser, customExpireTime, TimeUnit.MINUTES); +// +// Map claims = new HashMap<>(); +// claims.put(Constants.LOGIN_USER_KEY, token); +// claims.put(Constants.JWT_USERNAME, loginUser.getUsername()); +// return createToken(claims); +// } } diff --git a/chenhai-system/pom.xml b/chenhai-system/pom.xml index 5af37ab..629ee65 100644 --- a/chenhai-system/pom.xml +++ b/chenhai-system/pom.xml @@ -23,6 +23,44 @@ chenhai-common + + org.projectlombok + lombok + true + + + + + cn.hutool + hutool-all + 5.8.16 + + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + com.fasterxml.jackson.core + jackson-databind + + \ No newline at end of file diff --git a/chenhai-system/src/main/java/com/chenhai/muhu/domain/model/MuhuWxLoginBody.java b/chenhai-system/src/main/java/com/chenhai/muhu/domain/model/MuhuWxLoginBody.java new file mode 100644 index 0000000..6bf9b5f --- /dev/null +++ b/chenhai-system/src/main/java/com/chenhai/muhu/domain/model/MuhuWxLoginBody.java @@ -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; + } +} \ No newline at end of file diff --git a/chenhai-system/src/main/java/com/chenhai/muhu/service/IUserAuthService.java b/chenhai-system/src/main/java/com/chenhai/muhu/service/IUserAuthService.java new file mode 100644 index 0000000..6b70f22 --- /dev/null +++ b/chenhai-system/src/main/java/com/chenhai/muhu/service/IUserAuthService.java @@ -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 getUserAuths(Long userId); + + /** + * 检查认证方式是否存在 + */ + boolean checkAuthExists(String authType, String authKey); +} \ No newline at end of file diff --git a/chenhai-system/src/main/java/com/chenhai/muhu/service/WechatService.java b/chenhai-system/src/main/java/com/chenhai/muhu/service/WechatService.java new file mode 100644 index 0000000..46f6b77 --- /dev/null +++ b/chenhai-system/src/main/java/com/chenhai/muhu/service/WechatService.java @@ -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; + } + } +} \ No newline at end of file diff --git a/chenhai-system/src/main/java/com/chenhai/muhu/service/impl/UserAuthServiceImpl.java b/chenhai-system/src/main/java/com/chenhai/muhu/service/impl/UserAuthServiceImpl.java new file mode 100644 index 0000000..16d693c --- /dev/null +++ b/chenhai-system/src/main/java/com/chenhai/muhu/service/impl/UserAuthServiceImpl.java @@ -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 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 getUserAuths(Long userId) { + return sysUserAuthMapper.selectByUserId(userId); + } + + @Override + public boolean checkAuthExists(String authType, String authKey) { + SysUserAuth auth = sysUserAuthMapper.selectByAuthTypeAndKey(authType, authKey); + return auth != null; + } +} \ No newline at end of file diff --git a/chenhai-system/src/main/java/com/chenhai/muhu/service/impl/WechatServiceImpl.java b/chenhai-system/src/main/java/com/chenhai/muhu/service/impl/WechatServiceImpl.java new file mode 100644 index 0000000..c42a5b0 --- /dev/null +++ b/chenhai-system/src/main/java/com/chenhai/muhu/service/impl/WechatServiceImpl.java @@ -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); + } +} \ No newline at end of file diff --git a/chenhai-system/src/main/java/com/chenhai/system/mapper/SysUserAuthMapper.java b/chenhai-system/src/main/java/com/chenhai/system/mapper/SysUserAuthMapper.java new file mode 100644 index 0000000..0e8499b --- /dev/null +++ b/chenhai-system/src/main/java/com/chenhai/system/mapper/SysUserAuthMapper.java @@ -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 selectByUserId(Long userId); + + /** + * 查询用户特定类型的认证方式 + */ + List 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); +} \ No newline at end of file diff --git a/chenhai-system/src/main/java/com/chenhai/system/mapper/SysUserMapper.java b/chenhai-system/src/main/java/com/chenhai/system/mapper/SysUserMapper.java index 229549b..d585b92 100644 --- a/chenhai-system/src/main/java/com/chenhai/system/mapper/SysUserMapper.java +++ b/chenhai-system/src/main/java/com/chenhai/system/mapper/SysUserMapper.java @@ -52,6 +52,14 @@ public interface SysUserMapper */ public SysUser selectUserById(Long userId); + /** + * 通过手机号查询用户 + * + * @param phone 手机号 + * @return 用户对象信息 + */ + public SysUser selectUserByPhone(String phone); + /** * 新增用户信息 * diff --git a/chenhai-system/src/main/java/com/chenhai/system/service/ISysUserService.java b/chenhai-system/src/main/java/com/chenhai/system/service/ISysUserService.java index 838b0b6..a9190f4 100644 --- a/chenhai-system/src/main/java/com/chenhai/system/service/ISysUserService.java +++ b/chenhai-system/src/main/java/com/chenhai/system/service/ISysUserService.java @@ -67,6 +67,14 @@ public interface ISysUserService */ public String selectUserPostGroup(String userName); + /** + * 通过手机号码查询用户 + * + * @param phone 手机号码 + * @return 结果 + */ + public SysUser selectUserByPhone(String phone); + /** * 校验用户名称是否唯一 * diff --git a/chenhai-system/src/main/java/com/chenhai/system/service/impl/SysUserServiceImpl.java b/chenhai-system/src/main/java/com/chenhai/system/service/impl/SysUserServiceImpl.java index 6239ef4..61d9c11 100644 --- a/chenhai-system/src/main/java/com/chenhai/system/service/impl/SysUserServiceImpl.java +++ b/chenhai-system/src/main/java/com/chenhai/system/service/impl/SysUserServiceImpl.java @@ -163,6 +163,11 @@ public class SysUserServiceImpl implements ISysUserService return list.stream().map(SysPost::getPostName).collect(Collectors.joining(",")); } + @Override + public SysUser selectUserByPhone(String phone) { + return userMapper.selectUserByPhone(phone); + } + /** * 校验用户名称是否唯一 * diff --git a/chenhai-system/src/main/resources/mapper/system/SysUserAuthMapper.xml b/chenhai-system/src/main/resources/mapper/system/SysUserAuthMapper.xml new file mode 100644 index 0000000..ab94f75 --- /dev/null +++ b/chenhai-system/src/main/resources/mapper/system/SysUserAuthMapper.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + select auth_id, user_id, auth_type, auth_key, auth_secret, status, create_time, update_time + from sys_user_auth + + + + + + + + + + + + insert into sys_user_auth + + user_id, + auth_type, + auth_key, + auth_secret, + status, + create_time, + update_time, + + + #{userId}, + #{authType}, + #{authKey}, + #{authSecret}, + #{status}, + #{createTime}, + #{updateTime}, + + + + + update sys_user_auth + + user_id = #{userId}, + auth_type = #{authType}, + auth_key = #{authKey}, + auth_secret = #{authSecret}, + status = #{status}, + create_time = #{createTime}, + update_time = #{updateTime}, + + where auth_id = #{authId} + + + + delete from sys_user_auth where auth_id = #{authId} + + + + delete from sys_user_auth where auth_id in + + #{authId} + + + + + delete from sys_user_auth where user_id = #{userId} + + + \ No newline at end of file diff --git a/chenhai-system/src/main/resources/mapper/system/SysUserMapper.xml b/chenhai-system/src/main/resources/mapper/system/SysUserMapper.xml index 69e29c1..024f901 100644 --- a/chenhai-system/src/main/resources/mapper/system/SysUserMapper.xml +++ b/chenhai-system/src/main/resources/mapper/system/SysUserMapper.xml @@ -130,6 +130,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" where u.user_id = #{userId} + +