diff --git a/README.md b/README.md index c2879605..84db34d8 100644 --- a/README.md +++ b/README.md @@ -40,13 +40,32 @@ - **🛠️ 功能模块**:用户、角色、菜单、部门、字典管理等基础模块 -## 🌈 项目源码 +## 🌈 项目生态 -| 项目 | Gitee | GitHub | GitCode | -|------|-------|--------|---------| -| Java 后端 | [youlai-boot](https://gitee.com/youlaiorg/youlai-boot) | [youlai-boot](https://github.com/haoxianrui/youlai-boot) | [youlai-boot](https://gitcode.com/youlai/youlai-boot) | -| Vue 3 前端 | [vue3-element-admin](https://gitee.com/youlaiorg/vue3-element-admin) | [vue3-element-admin](https://github.com/youlaitech/vue3-element-admin) | [vue3-element-admin](https://gitcode.com/youlai/vue3-element-admin) | -| uni-app 移动端 | [vue-uniapp-template](https://gitee.com/youlaiorg/vue-uniapp-template) | [vue-uniapp-template](https://github.com/youlaitech/vue-uniapp-template) | [vue-uniapp-template](https://gitcode.com/youlai/vue-uniapp-template) | +youlai-boot 配套前端和移动端项目,形成完整的前后端分离开发体系: + +### 后端项目 + +| 项目 | 技术栈 | 说明 | Gitee | GitHub | AtomGit | +|------|--------|------|-------|--------|---------| +| youlai-boot | Spring Boot 4 + MyBatis-Plus | 标准版后端 | [Gitee](https://gitee.com/youlaiorg/youlai-boot) | [GitHub](https://github.com/haoxianrui/youlai-boot) | [AtomGit](https://atomgit.com/youlai/youlai-boot) | +| youlai-boot-tenant | Spring Boot 3 + MyBatis-Plus | 多租户 SaaS 版 | [Gitee](https://gitee.com/youlaiorg/youlai-boot-tenant) | [GitHub](https://github.com/youlaitech/youlai-boot-tenant) | [AtomGit](https://atomgit.com/youlai/youlai-boot-tenant) | +| youlai-boot-flex | Spring Boot 3 + MyBatis-Flex | MyBatis-Flex 版 | [Gitee](https://gitee.com/youlaiorg/youlai-boot-flex) | - | - | + +### 前端项目 + +| 项目 | 技术栈 | 说明 | Gitee | GitHub | AtomGit | +|------|--------|------|-------|--------|---------| +| vue3-element-admin | Vue 3 + TS + Element Plus | 标准版前端 | [Gitee](https://gitee.com/youlaiorg/vue3-element-admin) | [GitHub](https://github.com/youlaitech/vue3-element-admin) | [AtomGit](https://atomgit.com/youlai/vue3-element-admin) | +| vue3-element-admin-js | Vue 3 + JS + Element Plus | JavaScript 版 | [Gitee](https://gitee.com/youlaiorg/vue3-element-admin-js) | [GitHub](https://github.com/youlaitech/vue3-element-admin-js) | [AtomGit](https://atomgit.com/youlai/vue3-element-admin-js) | +| vue3-element-template | Vue 3 + TS + Element Plus | 精简开发模板 | [Gitee](https://gitee.com/youlaiorg/vue3-element-template) | [GitHub](https://github.com/youlaitech/vue3-element-template) | [AtomGit](https://atomgit.com/youlai/vue3-element-template) | +| vue3-naiveui-admin | Vue 3 + TS + Naive UI | Naive UI 版 | [Gitee](https://gitee.com/youlaiorg/vue3-naiveui-admin) | [GitHub](https://github.com/youlaitech/vue3-naiveui-admin) | [AtomGit](https://atomgit.com/youlai/vue3-naiveui-admin) | + +### 移动端项目 + +| 项目 | 技术栈 | 说明 | Gitee | GitHub | AtomGit | +|------|--------|------|-------|--------|---------| +| youlai-uniapp | Vue 3 + uni-app | 跨平台应用 | [Gitee](https://gitee.com/youlaiorg/youlai-uniapp) | [GitHub](https://github.com/youlaitech/youlai-uniapp) | [AtomGit](https://atomgit.com/youlai/youlai-uniapp) | ## 📚 项目文档 @@ -54,23 +73,14 @@ |------|------| | 在线接口文档 | [Apifox](https://www.apifox.cn/apidoc/shared-195e783f-4d85-4235-a038-eec696de4ea5) | | 官方文档 | [youlai.tech](https://www.youlai.tech/youlai-boot/) | -| 功能详解 | [CSDN](https://youlai.blog.csdn.net/article/details/145178880) | -| 入门指南 | [CSDN](https://youlai.blog.csdn.net/article/details/145177011) | +| 功能详解 | [https://youlai.blog.csdn.net/article/details/145178880](https://youlai.blog.csdn.net/article/details/145178880) | +| 入门指南 | [https://youlai.blog.csdn.net/article/details/145177011](https://youlai.blog.csdn.net/article/details/145177011) | ## 📁 项目目录 -
-展开目录结构 - ``` youlai-boot ├── docker/ # Docker 部署 -│ ├── minio/ # MinIO 对象存储 -│ ├── mysql/ # MySQL 数据库 -│ ├── postgres/ # PostgreSQL 数据库 -│ ├── redis/ # Redis 缓存 -│ ├── xxljob/ # XXL-JOB 调度中心 -│ └── docker-compose.yml # 容器编排脚本 ├── sql/ # 数据库脚本 │ └── mysql/ # MySQL 初始化脚本 ├── src/main/java/com/youlai/boot/ @@ -91,9 +101,6 @@ youlai-boot │ │ ├── validator/ # 参数校验 │ │ └── web/ # 响应封装 │ ├── file/ # 文件服务 -│ │ ├── controller/ # 文件接口 -│ │ ├── model/ # 文件模型 -│ │ └── service/ # 文件逻辑 │ ├── plugin/ # 插件扩展 │ │ ├── knife4j/ # 接口文档增强 │ │ └── mybatis/ # MyBatis 扩展 @@ -121,7 +128,6 @@ youlai-boot │ │ │ ├── bo/ # 业务对象 │ │ │ ├── dto/ # 传输对象 │ │ │ ├── entity/ # 实体对象 -│ │ │ ├── event/ # 事件对象 │ │ │ ├── form/ # 表单对象 │ │ │ ├── query/ # 查询对象 │ │ │ └── vo/ # 视图对象 @@ -132,8 +138,6 @@ youlai-boot └── pom.xml # Maven 配置 ``` -
- ## 🚀 快速开始 **详细文档**:[项目启动指南](https://youlai.blog.csdn.net/article/details/145177011) @@ -177,14 +181,25 @@ docker-compose up -d ## 💖 技术交流 -**公众号**:关注「有来技术」,点击菜单 **交流群** 获取加群二维码。 +欢迎通过以下方式交流学习,获取最新动态和技术支持: -![](https://foruda.gitee.com/images/1737108820762592766/3390ed0d_716974.png) +
-**微信**:添加 **`haoxianrui`**,备注「前端/后端/全栈」 +| 交流方式 | 说明 | +|:---:|:---| +| | 关注「有来技术」公众号
点击菜单 **交流群** 获取加群二维码 | +| 微信 **`haoxianrui`** | 添加好友时请备注「前端/后端/全栈」| -**博客**:[CSDN](https://youlai.blog.csdn.net/) | [掘金](https://juejin.cn/user/4187394044331261) | [博客园](https://www.cnblogs.com/haoxianrui) | [51CTO](https://blog.51cto.com/youlai) | [知乎](https://www.zhihu.com/people/haoxr) +
-**官网**:[https://www.youlai.tech](https://www.youlai.tech/) +--- -**代码仓库**:[Gitee](https://gitee.com/youlaiorg) | [GitHub](https://github.com/youlaitech) | [AtomGit](https://atomgit.com/youlai) +如果这个项目对你有帮助,欢迎 ⭐️ Star 支持一下! + +

+ ⭐ Gitee • + ⭐ GitHub • + ⭐ AtomGit • + 🌐 官网 • + 📝 博客 +

diff --git a/pom.xml b/pom.xml index f4ce3378..7134bdb0 100644 --- a/pom.xml +++ b/pom.xml @@ -276,7 +276,7 @@ com.github.binarywang weixin-java-miniapp - ${weixin.java.miniapp.version} + ${weixin-java-miniapp.version} diff --git a/src/main/java/com/youlai/boot/auth/controller/AuthController.java b/src/main/java/com/youlai/boot/auth/controller/AuthController.java index 215395a0..8a458437 100644 --- a/src/main/java/com/youlai/boot/auth/controller/AuthController.java +++ b/src/main/java/com/youlai/boot/auth/controller/AuthController.java @@ -1,7 +1,6 @@ package com.youlai.boot.auth.controller; import com.youlai.boot.auth.model.vo.CaptchaVO; -import com.youlai.boot.auth.model.vo.WechatLoginResult; import com.youlai.boot.auth.model.dto.LoginRequest; import com.youlai.boot.common.enums.LogModuleEnum; import com.youlai.boot.core.web.Result; @@ -41,10 +40,8 @@ public class AuthController { @Operation(summary = "账号密码登录") @PostMapping("/login") @Log(value = "登录", module = LogModuleEnum.LOGIN) - public Result login(@RequestBody @Valid LoginRequest request) { - String username = request.getUsername(); - String password = request.getPassword(); - AuthenticationToken authenticationToken = authService.login(username, password); + public Result login(@RequestBody @Valid LoginRequest request) { + AuthenticationToken authenticationToken = authService.login(request.getUsername(), request.getPassword()); return Result.success(authenticationToken); } @@ -53,7 +50,7 @@ public class AuthController { @Log(value = "短信验证码登录", module = LogModuleEnum.LOGIN) public Result loginBySms( @Parameter(description = "手机号", example = "18812345678") @RequestParam String mobile, - @Parameter(description = "验证码", example = "1234") @RequestParam String code + @Parameter(description = "验证码", example = "123456") @RequestParam String code ) { AuthenticationToken loginResult = authService.loginBySms(mobile, code); return Result.success(loginResult); @@ -61,57 +58,24 @@ public class AuthController { @Operation(summary = "发送登录短信验证码") @PostMapping("/sms/code") - public Result sendLoginVerifyCode( + public Result sendSmsCode( @Parameter(description = "手机号", example = "18812345678") @RequestParam String mobile ) { - authService.sendSmsLoginCode(mobile); + authService.sendSmsCode(mobile); return Result.success(); } - @Operation(summary = "微信小程序登录(个人小程序)") - @PostMapping("/wechat-miniapp/login") - @Log(value = "微信小程序登录", module = LogModuleEnum.LOGIN) - public Result loginByWechatMini( - @Parameter(description = "微信登录code", example = "xxx") @RequestParam String code - ) { - WechatLoginResult result = authService.loginByWechatMini(code); - return Result.success(result); - } - - @Operation(summary = "微信小程序一键登录(企业小程序)") - @PostMapping("/wechat-miniapp/phone-login") - @Log(value = "微信小程序一键登录", module = LogModuleEnum.LOGIN) - public Result loginByWechatMiniWithPhone( - @Parameter(description = "微信登录code", example = "xxx") @RequestParam String loginCode, - @Parameter(description = "手机号授权code", example = "xxx") @RequestParam String phoneCode - ) { - AuthenticationToken result = authService.wechatMiniLoginWithPhone(loginCode, phoneCode); - return Result.success(result); - } - - @Operation(summary = "微信小程序绑定手机号") - @PostMapping("/wechat-miniapp/bind-mobile") - @Log(value = "微信小程序绑定手机号", module = LogModuleEnum.LOGIN) - public Result bindMobileForWechatMini( - @Parameter(description = "微信openid") @RequestParam String openid, - @Parameter(description = "手机号", example = "18812345678") @RequestParam String mobile, - @Parameter(description = "短信验证码", example = "1234") @RequestParam String code - ) { - AuthenticationToken result = authService.bindMobileForWechatMini(openid, mobile, code); - return Result.success(result); - } - @Operation(summary = "退出登录") @DeleteMapping("/logout") @Log(value = "退出登录", module = LogModuleEnum.LOGIN) - public Result logout() { + public Result logout() { authService.logout(); return Result.success(); } @Operation(summary = "刷新令牌") @PostMapping("/refresh-token") - public Result refreshToken( + public Result refreshToken( @Parameter(description = "刷新令牌", example = "xxx.xxx.xxx") @RequestParam String refreshToken ) { AuthenticationToken authenticationToken = authService.refreshToken(refreshToken); diff --git a/src/main/java/com/youlai/boot/auth/controller/WechatMiniappAuthController.java b/src/main/java/com/youlai/boot/auth/controller/WechatMiniappAuthController.java new file mode 100644 index 00000000..cb677b8e --- /dev/null +++ b/src/main/java/com/youlai/boot/auth/controller/WechatMiniappAuthController.java @@ -0,0 +1,95 @@ +package com.youlai.boot.auth.controller; + +import com.youlai.boot.auth.model.vo.WechatMiniappLoginResult; +import com.youlai.boot.auth.service.WechatMiniappAuthService; +import com.youlai.boot.common.annotation.Log; +import com.youlai.boot.common.enums.LogModuleEnum; +import com.youlai.boot.core.web.Result; +import com.youlai.boot.security.model.AuthenticationToken; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 微信小程序认证控制层 + * + * @author Ray.Hao + * @since 2.4.0 + */ +@Tag(name = "13.微信小程序认证") +@RestController +@RequestMapping("/api/v1/wechat/miniapp/auth") +@RequiredArgsConstructor +@Slf4j +public class WechatMiniappAuthController { + + private final WechatMiniappAuthService wechatMiniappAuthService; + + /** + * 静默登录 + *

+ * 适用场景:个人小程序、无需手机号的登录场景 + *

    + *
  • 已绑定手机号的用户:直接返回 token,登录成功
  • + *
  • 未绑定手机号的用户:返回 openid,需调用绑定手机号接口
  • + *
+ */ + @Operation(summary = "静默登录", description = "通过微信 code 登录,已绑定用户直接返回 token,未绑定用户返回 openid 需绑定手机号") + @PostMapping("/silent-login") + @Log(value = "微信小程序静默登录", module = LogModuleEnum.LOGIN) + public Result silentLogin( + @Parameter(description = "微信登录凭证(wx.login 获取)", required = true, example = "0xxx") + @RequestParam String code + ) { + WechatMiniappLoginResult result = wechatMiniappAuthService.silentLogin(code); + return Result.success(result); + } + + /** + * 手机号快捷登录 + *

+ * 适用场景:企业认证小程序(已开通手机号快捷登录权限) + *

+ * 一步完成登录,无需绑定流程,自动创建新用户 + */ + @Operation(summary = "手机号快捷登录", description = "同时使用微信 code 和手机号授权 code 登录,适用于企业认证小程序") + @PostMapping("/phone-login") + @Log(value = "微信小程序手机号快捷登录", module = LogModuleEnum.LOGIN) + public Result phoneLogin( + @Parameter(description = "微信登录凭证(wx.login 获取)", required = true, example = "0xxx") + @RequestParam String loginCode, + @Parameter(description = "手机号授权凭证(getPhoneNumber 事件获取)", required = true, example = "0xxx") + @RequestParam String phoneCode + ) { + AuthenticationToken result = wechatMiniappAuthService.phoneLogin(loginCode, phoneCode); + return Result.success(result); + } + + /** + * 绑定手机号 + *

+ * 适用场景:静默登录后未绑定手机号的用户 + *

+ * 绑定成功后自动完成登录 + */ + @Operation(summary = "绑定手机号", description = "为静默登录用户绑定手机号,绑定成功后自动登录") + @PostMapping("/bind-mobile") + @Log(value = "微信小程序绑定手机号", module = LogModuleEnum.LOGIN) + public Result bindMobile( + @Parameter(description = "微信用户唯一标识(静默登录返回)", required = true) + @RequestParam String openid, + @Parameter(description = "手机号码", required = true, example = "18812345678") + @RequestParam String mobile, + @Parameter(description = "短信验证码", required = true, example = "123456") + @RequestParam String smsCode + ) { + AuthenticationToken result = wechatMiniappAuthService.bindMobile(openid, mobile, smsCode); + return Result.success(result); + } +} diff --git a/src/main/java/com/youlai/boot/auth/model/dto/LoginRequest.java b/src/main/java/com/youlai/boot/auth/model/dto/LoginRequest.java index 799ddc36..57b77de5 100644 --- a/src/main/java/com/youlai/boot/auth/model/dto/LoginRequest.java +++ b/src/main/java/com/youlai/boot/auth/model/dto/LoginRequest.java @@ -26,7 +26,7 @@ public class LoginRequest { @Schema(description = "验证码缓存ID", example = "captcha_id_123") private String captchaId; - @Schema(description = "验证码", example = "1234") + @Schema(description = "验证码", example = "123456") private String captchaCode; } diff --git a/src/main/java/com/youlai/boot/auth/model/vo/WechatLoginResult.java b/src/main/java/com/youlai/boot/auth/model/vo/WechatMiniappLoginResult.java similarity index 77% rename from src/main/java/com/youlai/boot/auth/model/vo/WechatLoginResult.java rename to src/main/java/com/youlai/boot/auth/model/vo/WechatMiniappLoginResult.java index 3e4d4012..deae4d36 100644 --- a/src/main/java/com/youlai/boot/auth/model/vo/WechatLoginResult.java +++ b/src/main/java/com/youlai/boot/auth/model/vo/WechatMiniappLoginResult.java @@ -6,10 +6,13 @@ import lombok.Data; /** * 微信小程序登录结果 + * + * @author Ray.Hao + * @since 2.4.0 */ @Data @Schema(description = "微信小程序登录结果") -public class WechatLoginResult { +public class WechatMiniappLoginResult { @Schema(description = "是否新用户") private Boolean isNewUser; @@ -30,13 +33,13 @@ public class WechatLoginResult { private String tokenType; @Schema(description = "过期时间(秒)") - private Long expiresIn; + private Integer expiresIn; /** * 创建需要绑定手机号的结果 */ - public static WechatLoginResult needBindMobile(String openid) { - WechatLoginResult result = new WechatLoginResult(); + public static WechatMiniappLoginResult needBindMobile(String openid) { + WechatMiniappLoginResult result = new WechatMiniappLoginResult(); result.setIsNewUser(true); result.setNeedBindMobile(true); result.setOpenid(openid); @@ -46,8 +49,8 @@ public class WechatLoginResult { /** * 创建登录成功的结果 */ - public static WechatLoginResult success(AuthenticationToken token) { - WechatLoginResult result = new WechatLoginResult(); + public static WechatMiniappLoginResult success(AuthenticationToken token) { + WechatMiniappLoginResult result = new WechatMiniappLoginResult(); result.setIsNewUser(false); result.setNeedBindMobile(false); result.setAccessToken(token.getAccessToken()); diff --git a/src/main/java/com/youlai/boot/auth/service/AuthService.java b/src/main/java/com/youlai/boot/auth/service/AuthService.java index 9a87fd38..6b478533 100644 --- a/src/main/java/com/youlai/boot/auth/service/AuthService.java +++ b/src/main/java/com/youlai/boot/auth/service/AuthService.java @@ -1,7 +1,6 @@ package com.youlai.boot.auth.service; import com.youlai.boot.auth.model.vo.CaptchaVO; -import com.youlai.boot.auth.model.vo.WechatLoginResult; import com.youlai.boot.security.model.AuthenticationToken; /** @@ -13,16 +12,32 @@ import com.youlai.boot.security.model.AuthenticationToken; public interface AuthService { /** - * 登录 + * 账号密码登录 * * @param username 用户名 * @param password 密码 - * @return 登录结果 + * @return 认证令牌 */ AuthenticationToken login(String username, String password); /** - * 登出 + * 短信验证码登录 + * + * @param mobile 手机号 + * @param code 验证码 + * @return 认证令牌 + */ + AuthenticationToken loginBySms(String mobile, String code); + + /** + * 发送短信验证码 + * + * @param mobile 手机号 + */ + void sendSmsCode(String mobile); + + /** + * 退出登录 */ void logout(); @@ -37,50 +52,7 @@ public interface AuthService { * 刷新令牌 * * @param refreshToken 刷新令牌 - * @return 登录结果 + * @return 认证令牌 */ AuthenticationToken refreshToken(String refreshToken); - - /** - * 发送短信验证码 - * - * @param mobile 手机号 - */ - void sendSmsLoginCode(String mobile); - - /** - * 短信验证码登录 - * - * @param mobile 手机号 - * @param code 验证码 - * @return 登录结果 - */ - AuthenticationToken loginBySms(String mobile, String code); - - /** - * 微信小程序登录(个人小程序) - * - * @param code 微信登录code - * @return 登录结果 - */ - WechatLoginResult loginByWechatMini(String code); - - /** - * 微信小程序一键登录(企业小程序) - * - * @param loginCode 微信登录code - * @param phoneCode 手机号授权code - * @return 登录结果 - */ - AuthenticationToken wechatMiniLoginWithPhone(String loginCode, String phoneCode); - - /** - * 微信小程序绑定手机号 - * - * @param openid 微信openid - * @param mobile 手机号 - * @param smsCode 短信验证码 - * @return 登录结果 - */ - AuthenticationToken bindMobileForWechatMini(String openid, String mobile, String smsCode); } diff --git a/src/main/java/com/youlai/boot/auth/service/WechatMiniappAuthService.java b/src/main/java/com/youlai/boot/auth/service/WechatMiniappAuthService.java new file mode 100644 index 00000000..3ffaadd2 --- /dev/null +++ b/src/main/java/com/youlai/boot/auth/service/WechatMiniappAuthService.java @@ -0,0 +1,53 @@ +package com.youlai.boot.auth.service; + +import com.youlai.boot.auth.model.vo.WechatMiniappLoginResult; +import com.youlai.boot.security.model.AuthenticationToken; + +/** + * 微信小程序认证服务接口 + * + * @author Ray.Hao + * @since 2.4.0 + */ +public interface WechatMiniappAuthService { + + /** + * 静默登录 + *

+ * 通过微信登录凭证(code)获取用户唯一标识(openid), + * 如果用户已绑定手机号则直接登录成功,否则返回需绑定手机号的提示。 + *

+ * + * @param code 微信登录凭证(wx.login 获取) + * @return 登录结果(成功返回 token,需绑定返回 openid) + */ + WechatMiniappLoginResult silentLogin(String code); + + /** + * 手机号快捷登录 + *

+ * 同时使用微信登录凭证和手机号授权凭证, + * 一步完成用户注册/登录,无需额外绑定流程。 + * 适用于企业认证的小程序(已开通手机号快捷登录权限)。 + *

+ * + * @param loginCode 微信登录凭证(wx.login 获取) + * @param phoneCode 手机号授权凭证(getPhoneNumber 事件获取) + * @return 认证令牌 + */ + AuthenticationToken phoneLogin(String loginCode, String phoneCode); + + /** + * 绑定手机号 + *

+ * 为已静默登录但未绑定手机号的用户绑定手机号, + * 绑定成功后自动完成登录。 + *

+ * + * @param openid 微信用户唯一标识 + * @param mobile 手机号码 + * @param smsCode 短信验证码 + * @return 认证令牌 + */ + AuthenticationToken bindMobile(String openid, String mobile, String smsCode); +} diff --git a/src/main/java/com/youlai/boot/auth/service/impl/AuthServiceImpl.java b/src/main/java/com/youlai/boot/auth/service/impl/AuthServiceImpl.java index c80a7aa9..a5334691 100644 --- a/src/main/java/com/youlai/boot/auth/service/impl/AuthServiceImpl.java +++ b/src/main/java/com/youlai/boot/auth/service/impl/AuthServiceImpl.java @@ -1,6 +1,8 @@ package com.youlai.boot.auth.service.impl; import cn.binarywang.wx.miniapp.api.WxMaService; +import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult; +import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo; import cn.hutool.captcha.AbstractCaptcha; import cn.hutool.captcha.CaptchaUtil; import cn.hutool.captcha.generator.CodeGenerator; @@ -236,7 +238,7 @@ public class AuthServiceImpl implements AuthService { @Transactional(rollbackFor = Exception.class) public AuthenticationToken wechatMiniLoginWithPhone(String loginCode, String phoneCode) { // 1. 用 loginCode 换取 openid - cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult session; + WxMaJscode2SessionResult session; try { session = wxMaService.jsCode2SessionInfo(loginCode); } catch (Exception e) { @@ -246,7 +248,7 @@ public class AuthServiceImpl implements AuthService { String openid = session.getOpenid(); // 2. 用 phoneCode 换取手机号 - cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo phoneInfo; + WxMaPhoneNumberInfo phoneInfo; try { phoneInfo = wxMaService.getUserService().getPhoneNoInfo(phoneCode); } catch (Exception e) { @@ -266,8 +268,8 @@ public class AuthServiceImpl implements AuthService { user = new User(); user.setMobile(mobile); user.setUsername("wx_" + IdUtil.fastSimpleUUID().substring(0, 8)); - user.setNickname(phoneInfo.getNickName() != null ? phoneInfo.getNickName() : "微信用户"); - user.setAvatar(phoneInfo.getAvatarUrl()); + user.setNickname("微信用户"); + user.setAvatar(null); user.setStatus(1); user.setIsDeleted(0); user.setCreateTime(LocalDateTime.now()); diff --git a/src/main/java/com/youlai/boot/auth/service/impl/WechatMiniappAuthServiceImpl.java b/src/main/java/com/youlai/boot/auth/service/impl/WechatMiniappAuthServiceImpl.java new file mode 100644 index 00000000..314b5848 --- /dev/null +++ b/src/main/java/com/youlai/boot/auth/service/impl/WechatMiniappAuthServiceImpl.java @@ -0,0 +1,234 @@ +package com.youlai.boot.auth.service.impl; + +import cn.binarywang.wx.miniapp.api.WxMaService; +import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult; +import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import com.youlai.boot.auth.model.vo.WechatMiniappLoginResult; +import com.youlai.boot.auth.service.WechatMiniappAuthService; +import com.youlai.boot.common.constant.RedisConstants; +import com.youlai.boot.security.exception.NeedBindMobileException; +import com.youlai.boot.security.model.AuthenticationToken; +import com.youlai.boot.security.model.SysUserDetails; +import com.youlai.boot.security.model.WechatMiniAuthenticationToken; +import com.youlai.boot.security.token.TokenManager; +import com.youlai.boot.system.enums.SocialPlatformEnum; +import com.youlai.boot.system.model.entity.User; +import com.youlai.boot.system.service.UserSocialService; +import com.youlai.boot.system.service.UserService; +import com.youlai.boot.system.service.UserRoleService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.Collections; + +/** + * 微信小程序认证服务实现 + * + * @author Ray.Hao + * @since 2.4.0 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class WechatMiniappAuthServiceImpl implements WechatMiniappAuthService { + + private final WxMaService wxMaService; + private final AuthenticationManager authenticationManager; + private final TokenManager tokenManager; + private final UserService userService; + private final UserSocialService userSocialService; + private final UserRoleService userRoleService; + private final RedisTemplate redisTemplate; + + /** + * 静默登录 + */ + @Override + public WechatMiniappLoginResult silentLogin(String code) { + WechatMiniAuthenticationToken token = new WechatMiniAuthenticationToken(code); + + try { + Authentication authentication = authenticationManager.authenticate(token); + AuthenticationToken authToken = tokenManager.generateToken(authentication); + SecurityContextHolder.getContext().setAuthentication(authentication); + return WechatMiniappLoginResult.success(authToken); + } catch (NeedBindMobileException e) { + return WechatMiniappLoginResult.needBindMobile(e.getOpenid()); + } + } + + /** + * 手机号快捷登录 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public AuthenticationToken phoneLogin(String loginCode, String phoneCode) { + // 1. 解析微信登录凭证,获取会话信息 + WxMaJscode2SessionResult session = resolveSession(loginCode); + String openid = session.getOpenid(); + + // 2. 解析手机号授权凭证,获取手机号 + String mobile = resolvePhoneNumber(phoneCode); + + log.info("微信小程序手机号快捷登录:openid={}, mobile={}", openid, mobile); + + // 3. 查询或创建用户 + User user = findOrCreateUser(mobile); + + // 4. 绑定微信 openid + bindWechatOpenid(user, session); + + // 5. 生成认证令牌 + return generateAuthToken(mobile); + } + + /** + * 绑定手机号 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public AuthenticationToken bindMobile(String openid, String mobile, String smsCode) { + // 1. 验证短信验证码 + validateSmsCode(mobile, smsCode); + + // 2. 查询或创建用户 + User user = findOrCreateUser(mobile); + + // 3. 绑定微信 openid + userSocialService.bindOrUpdate( + user.getId(), + SocialPlatformEnum.WECHAT_MINI, + openid, + null, null, null, null + ); + + log.info("微信小程序绑定手机号成功:mobile={}, openid={}", mobile, openid); + + // 4. 生成认证令牌 + return generateAuthToken(mobile); + } + + // ==================== 私有方法 ==================== + + /** + * 解析微信登录凭证,获取会话信息 + */ + private WxMaJscode2SessionResult resolveSession(String loginCode) { + try { + return wxMaService.jsCode2SessionInfo(loginCode); + } catch (Exception e) { + log.error("获取微信会话信息失败,loginCode={}", loginCode, e); + throw new IllegalArgumentException("微信登录失败:" + e.getMessage()); + } + } + + /** + * 解析手机号授权凭证,获取手机号 + */ + private String resolvePhoneNumber(String phoneCode) { + try { + WxMaPhoneNumberInfo phoneInfo = wxMaService.getUserService().getPhoneNoInfo(phoneCode); + return phoneInfo.getPhoneNumber(); + } catch (Exception e) { + log.error("获取微信手机号失败,phoneCode={}", phoneCode, e); + throw new IllegalArgumentException("获取手机号失败:" + e.getMessage()); + } + } + + /** + * 查询或创建用户 + */ + private User findOrCreateUser(String mobile) { + User user = userService.lambdaQuery() + .eq(User::getMobile, mobile) + .one(); + + if (user == null) { + user = createNewUser(mobile); + log.info("微信小程序登录:创建新用户,mobile={}, userId={}", mobile, user.getId()); + } + + return user; + } + + /** + * 创建新用户 + *

+ * 新用户默认分配 GUEST(访问游客)角色 + *

+ */ + private User createNewUser(String mobile) { + User user = new User(); + user.setMobile(mobile); + user.setUsername("wx_" + IdUtil.fastSimpleUUID().substring(0, 8)); + user.setNickname("微信用户"); + user.setStatus(1); + user.setIsDeleted(0); + user.setCreateTime(LocalDateTime.now()); + user.setUpdateTime(LocalDateTime.now()); + userService.save(user); + + // 分配 GUEST 角色(角色ID=3) + userRoleService.saveUserRoles(user.getId(), Collections.singletonList(3L)); + + return user; + } + + /** + * 绑定微信 openid + */ + private void bindWechatOpenid(User user, WxMaJscode2SessionResult session) { + try { + userSocialService.bindOrUpdate( + user.getId(), + SocialPlatformEnum.WECHAT_MINI, + session.getOpenid(), + session.getUnionid(), + user.getNickname(), + user.getAvatar(), + session.getSessionKey() + ); + } catch (Exception e) { + // 绑定失败不影响登录 + log.warn("绑定微信 openid 失败,userId={}, openid={}", user.getId(), session.getOpenid(), e); + } + } + + /** + * 验证短信验证码 + */ + private void validateSmsCode(String mobile, String smsCode) { + String cacheKey = StrUtil.format(RedisConstants.Captcha.SMS_LOGIN_CODE, mobile); + String cachedCode = (String) redisTemplate.opsForValue().get(cacheKey); + + if (!StrUtil.equals(smsCode, cachedCode)) { + throw new IllegalArgumentException("验证码错误"); + } + + // 验证成功后删除验证码 + redisTemplate.delete(cacheKey); + } + + /** + * 生成认证令牌 + */ + private AuthenticationToken generateAuthToken(String mobile) { + SysUserDetails userDetails = new SysUserDetails(userService.getAuthInfoByMobile(mobile)); + Authentication authentication = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities() + ); + AuthenticationToken authToken = tokenManager.generateToken(authentication); + SecurityContextHolder.getContext().setAuthentication(authentication); + return authToken; + } +} diff --git a/src/main/java/com/youlai/boot/config/WxMaConfiguration.java b/src/main/java/com/youlai/boot/config/WxMaConfig.java similarity index 76% rename from src/main/java/com/youlai/boot/config/WxMaConfiguration.java rename to src/main/java/com/youlai/boot/config/WxMaConfig.java index 76c495dc..8d6f952c 100644 --- a/src/main/java/com/youlai/boot/config/WxMaConfiguration.java +++ b/src/main/java/com/youlai/boot/config/WxMaConfig.java @@ -3,25 +3,32 @@ package com.youlai.boot.config; import cn.binarywang.wx.miniapp.api.WxMaService; import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl; import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl; +import com.youlai.boot.config.property.WxMaProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * 微信小程序配置 + * + * @author Ray.Hao + * @since 2024/01/01 */ @Configuration @EnableConfigurationProperties(WxMaProperties.class) -public class WxMaConfiguration { +public class WxMaConfig { + /** + * 微信小程序服务 + * + * @param properties 微信小程序配置属性 + * @return {@link WxMaService} + */ @Bean public WxMaService wxMaService(WxMaProperties properties) { WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl(); config.setAppid(properties.getAppid()); config.setSecret(properties.getSecret()); - config.setToken(properties.getToken()); - config.setAesKey(properties.getAesKey()); - config.setMsgDataFormat(properties.getMsgDataFormat()); WxMaService service = new WxMaServiceImpl(); service.setWxMaConfig(config); diff --git a/src/main/java/com/youlai/boot/config/WxMaProperties.java b/src/main/java/com/youlai/boot/config/property/WxMaProperties.java similarity index 51% rename from src/main/java/com/youlai/boot/config/WxMaProperties.java rename to src/main/java/com/youlai/boot/config/property/WxMaProperties.java index 532e0df9..841bf613 100644 --- a/src/main/java/com/youlai/boot/config/WxMaProperties.java +++ b/src/main/java/com/youlai/boot/config/property/WxMaProperties.java @@ -1,4 +1,4 @@ -package com.youlai.boot.config; +package com.youlai.boot.config.property; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -11,28 +11,13 @@ import org.springframework.boot.context.properties.ConfigurationProperties; public class WxMaProperties { /** - * 小程序appid + * 小程序 AppID */ private String appid; /** - * 小程序Secret + * 小程序 AppSecret */ private String secret; - /** - * 小程序token - */ - private String token; - - /** - * 小程序EncodingAESKey - */ - private String aesKey; - - /** - * 消息格式 - */ - private String msgDataFormat; - } diff --git a/src/main/java/com/youlai/boot/system/controller/UserController.java b/src/main/java/com/youlai/boot/system/controller/UserController.java index a2909079..7f381b43 100644 --- a/src/main/java/com/youlai/boot/system/controller/UserController.java +++ b/src/main/java/com/youlai/boot/system/controller/UserController.java @@ -276,5 +276,5 @@ public class UserController { return Result.judge(result); } - + } diff --git a/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java index c1cfea3b..286d9fb2 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java @@ -432,8 +432,8 @@ public class UserServiceImpl extends ServiceImpl implements Us } // String code = String.valueOf((int) ((Math.random() * 9 + 1) * 1000)); - // TODO 为了方便测试,验证码固定为 1234,实际开发中在配置了厂商短信服务后,可以使用上面的随机验证码 - String code = "1234"; + // TODO 为了方便测试,验证码固定为 123456,实际开发中在配置了厂商短信服务后,可以使用上面的随机验证码 + String code = "123456"; Map templateParams = new HashMap<>(); templateParams.put("code", code); @@ -517,8 +517,8 @@ public class UserServiceImpl extends ServiceImpl implements Us } // String code = String.valueOf((int) ((Math.random() * 9 + 1) * 1000)); - // TODO 为了方便测试,验证码固定为 1234,实际开发中在配置了邮箱服务后,可以使用上面的随机验证码 - String code = "1234"; + // TODO 为了方便测试,验证码固定为 123456,实际开发中在配置了邮箱服务后,可以使用上面的随机验证码 + String code = "123456"; mailService.sendMail(email, "邮箱验证码", "您的验证码为:" + code + ",请在5分钟内使用"); // 缓存验证码,5分钟有效,用于更换邮箱校验 diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index b6e919b1..8afaa7d1 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -81,6 +81,7 @@ security: ignore-urls: - /api/v1/auth/login/** # 登录接口(账号密码登录、手机验证码登录和微信登录) - /api/v1/auth/captcha # 验证码获取接口 + - /api/v1/auth/sms/code # 发送登录短信验证码 - /api/v1/auth/refresh-token # 刷新令牌接口 - /ws/** # WebSocket接口 # 非安全端点路径,完全绕过 Spring Security 的过滤器 @@ -213,8 +214,5 @@ captcha: # 微信小程序配置 wx: miniapp: - appid: your-app-id - secret: your-app-secret - token: your-token - aes-key: your-aes-key - msg-data-format: JSON + appid: xxxxxxx + secret: xxxxxxx