Browse Source

!45 增加管理后台的企业微信、钉钉的社交登陆方式
Merge pull request !45 from 芋道源码/admin-social-auth

芋道源码 3 years ago
parent
commit
53bda604b0
100 changed files with 2171 additions and 592 deletions
  1. 2 2
      README.md
  2. 10 352
      sql/ruoyi-vue-pro.sql
  3. 6 0
      yudao-admin-server/pom.xml
  4. 1 1
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/infra/controller/file/InfFileController.java
  5. 62 13
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/auth/SysAuthController.java
  6. 2 2
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/auth/vo/auth/SysAuthLoginReqVO.java
  7. 1 1
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/auth/vo/auth/SysAuthLoginRespVO.java
  8. 1 1
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/auth/vo/auth/SysAuthMenuRespVO.java
  9. 1 1
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/auth/vo/auth/SysAuthPermissionInfoRespVO.java
  10. 35 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/auth/vo/auth/SysAuthSocialBindReqVO.java
  11. 48 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/auth/vo/auth/SysAuthSocialLogin2ReqVO.java
  12. 35 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/auth/vo/auth/SysAuthSocialLoginReqVO.java
  13. 31 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/auth/vo/auth/SysAuthSocialUnbindReqVO.java
  14. 1 1
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/auth/vo/session/SysUserSessionPageItemRespVO.java
  15. 4 4
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/logger/SysLoginLogController.java
  16. 3 3
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/logger/vo/loginlog/SysLoginLogBaseVO.java
  17. 2 2
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/logger/vo/loginlog/SysLoginLogCreateReqVO.java
  18. 8 4
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/logger/vo/loginlog/SysLoginLogExcelVO.java
  19. 1 1
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/logger/vo/loginlog/SysLoginLogExportReqVO.java
  20. 1 1
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/logger/vo/loginlog/SysLoginLogPageReqVO.java
  21. 11 2
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/logger/vo/loginlog/SysLoginLogRespVO.java
  22. 3 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/user/SysUserProfileController.http
  23. 11 4
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/user/SysUserProfileController.java
  24. 17 1
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/user/vo/profile/SysUserProfileRespVO.java
  25. 1 1
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/user/vo/user/SysUserRespVO.java
  26. 5 6
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/convert/auth/SysAuthConvert.java
  27. 3 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/convert/user/SysUserConvert.java
  28. 14 3
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/dal/dataobject/logger/SysLoginLogDO.java
  29. 82 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/dal/dataobject/social/SysSocialUserDO.java
  30. 1 1
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/dal/dataobject/user/SysUserDO.java
  31. 28 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/dal/mysql/social/SysSocialUserMapper.java
  32. 12 1
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/dal/redis/SysRedisKeyConstants.java
  33. 1 1
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/dal/redis/auth/SysLoginUserRedisDAO.java
  34. 38 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/dal/redis/social/SysSocialAuthUserRedisDAO.java
  35. 2 2
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/enums/SysDictTypeConstants.java
  36. 8 1
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/enums/SysErrorCodeConstants.java
  37. 6 4
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/enums/common/SysSexEnum.java
  38. 3 1
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/enums/logger/SysLoginLogTypeEnum.java
  39. 1 1
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/enums/logger/SysLoginResultEnum.java
  40. 55 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/enums/social/SysSocialTypeEnum.java
  41. 35 5
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/auth/SysAuthService.java
  42. 9 2
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/auth/SysUserSessionService.java
  43. 94 21
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/auth/impl/SysAuthServiceImpl.java
  44. 14 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/auth/impl/SysUserSessionServiceImpl.java
  45. 6 6
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/logger/SysLoginLogService.java
  46. 13 1
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/logger/impl/SysLoginLogServiceImpl.java
  47. 2 2
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/permission/impl/SysPermissionServiceImpl.java
  48. 80 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/social/SysSocialService.java
  49. 178 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/social/impl/SysSocialServiceImpl.java
  50. 1 1
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/user/SysUserService.java
  51. 15 14
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/user/impl/SysUserServiceImpl.java
  52. 2 2
      yudao-admin-server/src/main/resources/application-dev.yaml
  53. 23 2
      yudao-admin-server/src/main/resources/application-local.yaml
  54. 1 1
      yudao-admin-server/src/test-integration/java/cn/iocoder/yudao/adminserver/modules/system/service/sms/SysSmsServiceIntegrationTest.java
  55. 0 13
      yudao-admin-server/src/test/java/cn/iocoder/yudao/adminserver/BaseMockitoUnitTest.java
  56. 1 1
      yudao-admin-server/src/test/java/cn/iocoder/yudao/adminserver/modules/system/service/auth/SysAuthServiceImplTest.java
  57. 1 1
      yudao-admin-server/src/test/java/cn/iocoder/yudao/adminserver/modules/system/service/auth/SysUserSessionServiceImplTest.java
  58. 1 1
      yudao-admin-server/src/test/java/cn/iocoder/yudao/adminserver/modules/system/service/sms/SysSmsServiceTest.java
  59. 171 0
      yudao-admin-server/src/test/java/cn/iocoder/yudao/adminserver/modules/system/service/social/SysSocialServiceTest.java
  60. 1 0
      yudao-admin-server/src/test/java/cn/iocoder/yudao/adminserver/modules/system/service/user/SysUserServiceImplTest.java
  61. 1 0
      yudao-admin-server/src/test/resources/sql/clean.sql
  62. 20 0
      yudao-admin-server/src/test/resources/sql/create_tables.sql
  63. 1 1
      yudao-admin-ui/.env.production
  64. 61 0
      yudao-admin-ui/src/api/login.js
  65. 1 1
      yudao-admin-ui/src/api/system/user.js
  66. 1 1
      yudao-admin-ui/src/permission.js
  67. 5 0
      yudao-admin-ui/src/router/index.js
  68. 37 1
      yudao-admin-ui/src/store/modules/user.js
  69. 24 0
      yudao-admin-ui/src/utils/constants.js
  70. 1 0
      yudao-admin-ui/src/utils/dict.js
  71. 1 1
      yudao-admin-ui/src/views/index_old.vue
  72. 58 32
      yudao-admin-ui/src/views/login.vue
  73. 187 0
      yudao-admin-ui/src/views/socialLogin.vue
  74. 2 2
      yudao-admin-ui/src/views/system/loginlog/index.vue
  75. 6 27
      yudao-admin-ui/src/views/system/session/index.vue
  76. 9 2
      yudao-admin-ui/src/views/system/user/profile/index.vue
  77. 5 5
      yudao-admin-ui/src/views/system/user/profile/userAvatar.vue
  78. 100 0
      yudao-admin-ui/src/views/system/user/profile/userSocial.vue
  79. 1 1
      yudao-dependencies/pom.xml
  80. 30 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java
  81. 5 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java
  82. 30 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/validation/ValidationUtils.java
  83. 28 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/Mobile.java
  84. 25 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/MobileValidator.java
  85. 4 0
      yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
  86. 4 4
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoSecurityAutoConfiguration.java
  87. 10 13
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java
  88. 3 2
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java
  89. 2 2
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/annotations/PreAuthenticated.java
  90. 3 3
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/JWTAuthenticationTokenFilter.java
  91. 53 0
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/handler/AbstractSignUpUrlAuthenticationSuccessHandler.java
  92. 1 1
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java
  93. 1 1
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/DemoFilter.java
  94. 1 1
      yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/infra/service/auth/SysAuthService.java
  95. 100 0
      yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/controller/auth/MbrAuthController.java
  96. 31 0
      yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/controller/auth/vo/MbrAuthLoginReqVO.java
  97. 20 0
      yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/controller/auth/vo/MbrAuthLoginRespVO.java
  98. 32 0
      yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/controller/auth/vo/MbrAuthResetPasswordReqVO.java
  99. 24 0
      yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/controller/auth/vo/MbrAuthSendSmsReqVO.java
  100. 38 0
      yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/controller/auth/vo/MbrAuthSmsLoginReqVO.java

+ 2 - 2
README.md

@@ -127,11 +127,11 @@
 
 | 模块 | biu |  biu | biu |
 | --- | --- | --- | --- |
-| 登 & 首页 | ![登录](https://static.iocoder.cn/images/ruoyi-vue-pro/登录.jpg) | ![首页](https://static.iocoder.cn/images/ruoyi-vue-pro/首页.jpg) | ![个人中心](https://static.iocoder.cn/images/ruoyi-vue-pro/个人中心.jpg) |
+| 登 & 首页 | ![登录](https://static.iocoder.cn/images/ruoyi-vue-pro/登录.jpg) | ![首页](https://static.iocoder.cn/images/ruoyi-vue-pro/首页.jpg) | ![个人中心](https://static.iocoder.cn/images/ruoyi-vue-pro/个人中心.jpg) |
 | 用户 | ![用户管理](https://static.iocoder.cn/images/ruoyi-vue-pro/用户管理.jpg) | ![在线用户](https://static.iocoder.cn/images/ruoyi-vue-pro/在线用户.jpg) | - |
 | 部门 & 岗位 | ![部门管理](https://static.iocoder.cn/images/ruoyi-vue-pro/部门管理.jpg) | ![岗位管理](https://static.iocoder.cn/images/ruoyi-vue-pro/岗位管理.jpg) | - |
 | 菜单 & 角色 | ![菜单管理](https://static.iocoder.cn/images/ruoyi-vue-pro/菜单管理.jpg) | ![角色管理](https://static.iocoder.cn/images/ruoyi-vue-pro/角色管理.jpg) | - |
-| 审计日志 | ![操作日志](https://static.iocoder.cn/images/ruoyi-vue-pro/操作日志.jpg) | ![登陆日志](https://static.iocoder.cn/images/ruoyi-vue-pro/登陆日志.jpg) | - |
+| 审计日志 | ![操作日志](https://static.iocoder.cn/images/ruoyi-vue-pro/操作日志.jpg) | ![登录日志](https://static.iocoder.cn/images/ruoyi-vue-pro/登录日志.jpg) | - |
 | 短信 | ![短信渠道](https://static.iocoder.cn/images/ruoyi-vue-pro/短信渠道.jpg) | ![短信模板](https://static.iocoder.cn/images/ruoyi-vue-pro/短信模板.jpg) | ![短信日志](https://static.iocoder.cn/images/ruoyi-vue-pro/短信日志.jpg) |
 | 字典 | ![字典类型](https://static.iocoder.cn/images/ruoyi-vue-pro/字典类型.jpg) | ![字典数据](https://static.iocoder.cn/images/ruoyi-vue-pro/字典数据.jpg) | - |
 | 错误码 & 通知 | ![错误码管理](https://static.iocoder.cn/images/ruoyi-vue-pro/错误码管理.jpg) | ![通知公告](https://static.iocoder.cn/images/ruoyi-vue-pro/通知公告.jpg) | - |

File diff suppressed because it is too large
+ 10 - 352
sql/ruoyi-vue-pro.sql


+ 6 - 0
yudao-admin-server/pom.xml

@@ -117,6 +117,12 @@
             <artifactId>screw-core</artifactId> <!-- 实现数据库文档 -->
         </dependency>
 
+        <!-- TODO 后续看情况,进行调整 -->
+        <dependency>
+            <groupId>com.xkcoding.justauth</groupId>
+            <artifactId>justauth-spring-boot-starter</artifactId>
+            <version>1.4.0</version>
+        </dependency>
     </dependencies>
 
     <build>

+ 1 - 1
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/infra/controller/file/InfFileController.java

@@ -41,7 +41,7 @@ public class InfFileController {
     @ApiOperation("上传文件")
     @ApiImplicitParams({
             @ApiImplicitParam(name = "file", value = "文件附件", required = true, dataTypeClass = MultipartFile.class),
-            @ApiImplicitParam(name = "path", value = "文件路径", required = false, example = "yudaoyuanma.png", dataTypeClass = String.class)
+            @ApiImplicitParam(name = "path", value = "文件路径", example = "yudaoyuanma.png", dataTypeClass = String.class)
     })
     public CommonResult<String> uploadFile(@RequestParam("file") MultipartFile file,
                                            @RequestParam("path") String path) throws IOException {

+ 62 - 13
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/auth/SysAuthController.java

@@ -1,12 +1,6 @@
 package cn.iocoder.yudao.adminserver.modules.system.controller.auth;
 
-import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
-import cn.iocoder.yudao.framework.common.pojo.CommonResult;
-import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
-import cn.iocoder.yudao.adminserver.modules.system.controller.auth.vo.auth.SysAuthLoginReqVO;
-import cn.iocoder.yudao.adminserver.modules.system.controller.auth.vo.auth.SysAuthLoginRespVO;
-import cn.iocoder.yudao.adminserver.modules.system.controller.auth.vo.auth.SysAuthMenuRespVO;
-import cn.iocoder.yudao.adminserver.modules.system.controller.auth.vo.auth.SysAuthPermissionInfoRespVO;
+import cn.iocoder.yudao.adminserver.modules.system.controller.auth.vo.auth.*;
 import cn.iocoder.yudao.adminserver.modules.system.convert.auth.SysAuthConvert;
 import cn.iocoder.yudao.adminserver.modules.system.dal.dataobject.permission.SysMenuDO;
 import cn.iocoder.yudao.adminserver.modules.system.dal.dataobject.permission.SysRoleDO;
@@ -15,10 +9,17 @@ import cn.iocoder.yudao.adminserver.modules.system.enums.permission.MenuTypeEnum
 import cn.iocoder.yudao.adminserver.modules.system.service.auth.SysAuthService;
 import cn.iocoder.yudao.adminserver.modules.system.service.permission.SysPermissionService;
 import cn.iocoder.yudao.adminserver.modules.system.service.permission.SysRoleService;
+import cn.iocoder.yudao.adminserver.modules.system.service.social.SysSocialService;
 import cn.iocoder.yudao.adminserver.modules.system.service.user.SysUserService;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
 import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
 import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
@@ -27,15 +28,16 @@ import javax.validation.Valid;
 import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
-import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
-import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserRoleIds;
 import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
 import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getUserAgent;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserRoleIds;
 
 @Api(tags = "认证")
 @RestController
 @RequestMapping("/")
 @Validated
+@Slf4j
 public class SysAuthController {
 
     @Resource
@@ -46,6 +48,8 @@ public class SysAuthController {
     private SysRoleService roleService;
     @Resource
     private SysPermissionService permissionService;
+    @Resource
+    private SysSocialService socialService;
 
     @PostMapping("/login")
     @ApiOperation("使用账号密码登录")
@@ -57,7 +61,7 @@ public class SysAuthController {
     }
 
     @GetMapping("/get-permission-info")
-    @ApiOperation("获取登用户的权限信息")
+    @ApiOperation("获取登用户的权限信息")
     public CommonResult<SysAuthPermissionInfoRespVO> getPermissionInfo() {
         // 获得用户信息
         SysUserDO user = userService.getUser(getLoginUserId());
@@ -68,7 +72,7 @@ public class SysAuthController {
         List<SysRoleDO> roleList = roleService.getRolesFromCache(getLoginUserRoleIds());
         // 获得菜单列表
         List<SysMenuDO> menuList = permissionService.getRoleMenusFromCache(
-                getLoginUserRoleIds(), // 注意,基于登的角色,因为后续的权限判断也是基于它
+                getLoginUserRoleIds(), // 注意,基于登的角色,因为后续的权限判断也是基于它
                 SetUtils.asSet(MenuTypeEnum.DIR.getType(), MenuTypeEnum.MENU.getType(), MenuTypeEnum.BUTTON.getType()),
                 SetUtils.asSet(CommonStatusEnum.ENABLE.getStatus()));
         // 拼接结果返回
@@ -76,15 +80,60 @@ public class SysAuthController {
     }
 
     @GetMapping("list-menus")
-    @ApiOperation("获得登用户的菜单列表")
+    @ApiOperation("获得登用户的菜单列表")
     public CommonResult<List<SysAuthMenuRespVO>> getMenus() {
         // 获得用户拥有的菜单列表
         List<SysMenuDO> menuList = permissionService.getRoleMenusFromCache(
-                getLoginUserRoleIds(), // 注意,基于登的角色,因为后续的权限判断也是基于它
+                getLoginUserRoleIds(), // 注意,基于登的角色,因为后续的权限判断也是基于它
                 SetUtils.asSet(MenuTypeEnum.DIR.getType(), MenuTypeEnum.MENU.getType()), // 只要目录和菜单类型
                 SetUtils.asSet(CommonStatusEnum.ENABLE.getStatus())); // 只要开启的
         // 转换成 Tree 结构返回
         return success(SysAuthConvert.INSTANCE.buildMenuTree(menuList));
     }
 
+    // ========== 社交登录相关 ==========
+
+    @GetMapping("/social-auth-redirect")
+    @ApiOperation("社交授权的跳转")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "type", value = "社交类型", required = true, dataTypeClass = Integer.class),
+            @ApiImplicitParam(name = "redirectUri", value = "回调路径", dataTypeClass = String.class)
+    })
+    public CommonResult<String> socialAuthRedirect(@RequestParam("type") Integer type,
+                                                    @RequestParam("redirectUri") String redirectUri) {
+        return CommonResult.success(socialService.getAuthorizeUrl(type, redirectUri));
+    }
+
+    @PostMapping("/social-login")
+    @ApiOperation("社交登录,使用 code 授权码")
+    @OperateLog(enable = false) // 避免 Post 请求被记录操作日志
+    public CommonResult<SysAuthLoginRespVO> socialLogin(@RequestBody @Valid SysAuthSocialLoginReqVO reqVO) {
+        String token = authService.socialLogin(reqVO, getClientIP(), getUserAgent());
+        // 返回结果
+        return success(SysAuthLoginRespVO.builder().token(token).build());
+    }
+
+    @PostMapping("/social-login2")
+    @ApiOperation("社交登录,使用 code 授权码 + 账号密码")
+    @OperateLog(enable = false) // 避免 Post 请求被记录操作日志
+    public CommonResult<SysAuthLoginRespVO> socialLogin2(@RequestBody @Valid SysAuthSocialLogin2ReqVO reqVO) {
+        String token = authService.socialLogin2(reqVO, getClientIP(), getUserAgent());
+        // 返回结果
+        return success(SysAuthLoginRespVO.builder().token(token).build());
+    }
+
+    @PostMapping("/social-bind")
+    @ApiOperation("社交绑定,使用 code 授权码")
+    public CommonResult<Boolean> socialBind(@RequestBody @Valid SysAuthSocialBindReqVO reqVO) {
+        authService.socialBind(getLoginUserId(), reqVO);
+        return CommonResult.success(true);
+    }
+
+    @DeleteMapping("/social-unbind")
+    @ApiOperation("取消社交绑定")
+    public CommonResult<Boolean> socialUnbind(@RequestBody SysAuthSocialUnbindReqVO reqVO) {
+        socialService.unbindSocialUser(getLoginUserId(), reqVO.getType(), reqVO.getUnionId());
+        return CommonResult.success(true);
+    }
+
 }

+ 2 - 2
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/auth/vo/auth/SysAuthLoginReqVO.java

@@ -11,7 +11,7 @@ import org.hibernate.validator.constraints.Length;
 import javax.validation.constraints.NotEmpty;
 import javax.validation.constraints.Pattern;
 
-@ApiModel("账号密码登 Request VO")
+@ApiModel("账号密码登 Request VO")
 @Data
 @NoArgsConstructor
 @AllArgsConstructor
@@ -19,7 +19,7 @@ import javax.validation.constraints.Pattern;
 public class SysAuthLoginReqVO {
 
     @ApiModelProperty(value = "账号", required = true, example = "yudaoyuanma")
-    @NotEmpty(message = "登账号不能为空")
+    @NotEmpty(message = "登账号不能为空")
     @Length(min = 4, max = 16, message = "账号长度为 4-16 位")
     @Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母")
     private String username;

+ 1 - 1
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/auth/vo/auth/SysAuthLoginRespVO.java

@@ -7,7 +7,7 @@ import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 
-@ApiModel("账号密码登 Response VO")
+@ApiModel("账号密码登 Response VO")
 @Data
 @NoArgsConstructor
 @AllArgsConstructor

+ 1 - 1
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/auth/vo/auth/SysAuthMenuRespVO.java

@@ -9,7 +9,7 @@ import lombok.NoArgsConstructor;
 
 import java.util.List;
 
-@ApiModel("登用户的菜单信息 Response VO")
+@ApiModel("登用户的菜单信息 Response VO")
 @Data
 @NoArgsConstructor
 @AllArgsConstructor

+ 1 - 1
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/auth/vo/auth/SysAuthPermissionInfoRespVO.java

@@ -9,7 +9,7 @@ import lombok.NoArgsConstructor;
 
 import java.util.Set;
 
-@ApiModel(value = "登用户的权限信息 Response VO", description = "额外包括用户信息和角色列表")
+@ApiModel(value = "登用户的权限信息 Response VO", description = "额外包括用户信息和角色列表")
 @Data
 @NoArgsConstructor
 @AllArgsConstructor

+ 35 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/auth/vo/auth/SysAuthSocialBindReqVO.java

@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.adminserver.modules.system.controller.auth.vo.auth;
+
+import cn.iocoder.yudao.adminserver.modules.system.enums.social.SysSocialTypeEnum;
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+
+@ApiModel("社交绑定 Request VO,使用 code 授权码")
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class SysAuthSocialBindReqVO {
+
+    @ApiModelProperty(value = "社交平台的类型", required = true, example = "10", notes = "参见 SysUserSocialTypeEnum 枚举值")
+    @InEnum(SysSocialTypeEnum.class)
+    @NotNull(message = "社交平台的类型不能为空")
+    private Integer type;
+
+    @ApiModelProperty(value = "授权码", required = true, example = "1024")
+    @NotEmpty(message = "授权码不能为空")
+    private String code;
+
+    @ApiModelProperty(value = "state", required = true, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62")
+    @NotEmpty(message = "state 不能为空")
+    private String state;
+
+}

+ 48 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/auth/vo/auth/SysAuthSocialLogin2ReqVO.java

@@ -0,0 +1,48 @@
+package cn.iocoder.yudao.adminserver.modules.system.controller.auth.vo.auth;
+
+import cn.iocoder.yudao.adminserver.modules.system.enums.social.SysSocialTypeEnum;
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.hibernate.validator.constraints.Length;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
+
+@ApiModel("社交登录 Request VO,使用 code 授权码 + 账号密码")
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class SysAuthSocialLogin2ReqVO {
+
+    @ApiModelProperty(value = "社交平台的类型", required = true, example = "10", notes = "参见 SysUserSocialTypeEnum 枚举值")
+    @InEnum(SysSocialTypeEnum.class)
+    @NotNull(message = "社交平台的类型不能为空")
+    private Integer type;
+
+    @ApiModelProperty(value = "授权码", required = true, example = "1024")
+    @NotEmpty(message = "授权码不能为空")
+    private String code;
+
+    @ApiModelProperty(value = "state", required = true, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62")
+    @NotEmpty(message = "state 不能为空")
+    private String state;
+
+    @ApiModelProperty(value = "账号", required = true, example = "yudaoyuanma")
+    @NotEmpty(message = "登录账号不能为空")
+    @Length(min = 4, max = 16, message = "账号长度为 4-16 位")
+    @Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母")
+    private String username;
+
+    @ApiModelProperty(value = "密码", required = true, example = "buzhidao")
+    @NotEmpty(message = "密码不能为空")
+    @Length(min = 4, max = 16, message = "密码长度为 4-16 位")
+    private String password;
+
+}

+ 35 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/auth/vo/auth/SysAuthSocialLoginReqVO.java

@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.adminserver.modules.system.controller.auth.vo.auth;
+
+import cn.iocoder.yudao.adminserver.modules.system.enums.social.SysSocialTypeEnum;
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+
+@ApiModel("社交登录 Request VO,使用 code 授权码")
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class SysAuthSocialLoginReqVO {
+
+    @ApiModelProperty(value = "社交平台的类型", required = true, example = "10", notes = "参见 SysUserSocialTypeEnum 枚举值")
+    @InEnum(SysSocialTypeEnum.class)
+    @NotNull(message = "社交平台的类型不能为空")
+    private Integer type;
+
+    @ApiModelProperty(value = "授权码", required = true, example = "1024")
+    @NotEmpty(message = "授权码不能为空")
+    private String code;
+
+    @ApiModelProperty(value = "state", required = true, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62")
+    @NotEmpty(message = "state 不能为空")
+    private String state;
+
+}

+ 31 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/auth/vo/auth/SysAuthSocialUnbindReqVO.java

@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.adminserver.modules.system.controller.auth.vo.auth;
+
+import cn.iocoder.yudao.adminserver.modules.system.enums.social.SysSocialTypeEnum;
+import cn.iocoder.yudao.framework.common.validation.InEnum;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+
+@ApiModel("取消社交绑定 Request VO,使用 code 授权码")
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class SysAuthSocialUnbindReqVO {
+
+    @ApiModelProperty(value = "社交平台的类型", required = true, example = "10", notes = "参见 SysUserSocialTypeEnum 枚举值")
+    @InEnum(SysSocialTypeEnum.class)
+    @NotNull(message = "社交平台的类型不能为空")
+    private Integer type;
+
+    @ApiModelProperty(value = "社交的全局编号", required = true, example = "IPRmJ0wvBptiPIlGEZiPewGwiEiE")
+    @NotEmpty(message = "社交的全局编号不能为空")
+    private String unionId;
+
+}

+ 1 - 1
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/auth/vo/session/SysUserSessionPageItemRespVO.java

@@ -26,7 +26,7 @@ public class SysUserSessionPageItemRespVO extends PageParam {
     @ApiModelProperty(value = "浏览器 UserAgent", required = true, example = "Mozilla/5.0")
     private String userAgent;
 
-    @ApiModelProperty(value = "登时间", required = true)
+    @ApiModelProperty(value = "登时间", required = true)
     private Date createTime;
 
     @ApiModelProperty(value = "用户账号", required = true, example = "yudao")

+ 4 - 4
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/logger/SysLoginLogController.java

@@ -27,7 +27,7 @@ import java.util.List;
 
 import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
 
-@Api(tags = "登日志")
+@Api(tags = "登日志")
 @RestController
 @RequestMapping("/system/login-log")
 @Validated
@@ -37,7 +37,7 @@ public class SysLoginLogController {
     private SysLoginLogService loginLogService;
 
     @GetMapping("/page")
-    @ApiOperation("获得登日志分页列表")
+    @ApiOperation("获得登日志分页列表")
     @PreAuthorize("@ss.hasPermission('system:login-log:query')")
     public CommonResult<PageResult<SysLoginLogRespVO>> getLoginLogPage(@Valid SysLoginLogPageReqVO reqVO) {
         PageResult<SysLoginLogDO> page = loginLogService.getLoginLogPage(reqVO);
@@ -45,7 +45,7 @@ public class SysLoginLogController {
     }
 
     @GetMapping("/export")
-    @ApiOperation("导出登日志 Excel")
+    @ApiOperation("导出登日志 Excel")
     @PreAuthorize("@ss.hasPermission('system:login-log:export')")
     @OperateLog(type = EXPORT)
     public void exportLoginLog(HttpServletResponse response, @Valid SysLoginLogExportReqVO reqVO) throws IOException {
@@ -53,7 +53,7 @@ public class SysLoginLogController {
         // 拼接数据
         List<SysLoginLogExcelVO> data = SysLoginLogConvert.INSTANCE.convertList(list);
         // 输出
-        ExcelUtils.write(response, "登日志.xls", "数据列表", SysLoginLogExcelVO.class, data);
+        ExcelUtils.write(response, "登日志.xls", "数据列表", SysLoginLogExcelVO.class, data);
     }
 
 }

+ 3 - 3
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/logger/vo/loginlog/SysLoginLogBaseVO.java

@@ -9,7 +9,7 @@ import javax.validation.constraints.NotNull;
 import javax.validation.constraints.Size;
 
 /**
- * 登日志 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 登日志 Base VO,提供给添加、修改、详细的子 VO 使用
  * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
  */
 @Data
@@ -28,8 +28,8 @@ public class SysLoginLogBaseVO {
     @Size(max = 30, message = "用户账号长度不能超过30个字符")
     private String username;
 
-    @ApiModelProperty(value = "登结果", required = true, example = "1", notes = "参见 SysLoginResultEnum 枚举类")
-    @NotNull(message = "登结果不能为空")
+    @ApiModelProperty(value = "登结果", required = true, example = "1", notes = "参见 SysLoginResultEnum 枚举类")
+    @NotNull(message = "登结果不能为空")
     private Integer result;
 
     @ApiModelProperty(value = "用户 IP", required = true, example = "127.0.0.1")

+ 2 - 2
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/logger/vo/loginlog/SysLoginLogCreateReqVO.java

@@ -5,8 +5,8 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.ToString;
 
-@ApiModel(value = "登日志创建 Request VO",
-        description = "暂时提供给前端,仅仅后端记录登日志时,进行使用")
+@ApiModel(value = "登日志创建 Request VO",
+        description = "暂时提供给前端,仅仅后端记录登日志时,进行使用")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)

+ 8 - 4
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/logger/vo/loginlog/SysLoginLogExcelVO.java

@@ -9,7 +9,7 @@ import lombok.Data;
 import java.util.Date;
 
 /**
- * 登日志 Excel 导出响应 VO
+ * 登日志 Excel 导出响应 VO
  */
 @Data
 public class SysLoginLogExcelVO {
@@ -20,17 +20,21 @@ public class SysLoginLogExcelVO {
     @ExcelProperty("用户账号")
     private String username;
 
-    @ExcelProperty(value = "登陆结果", converter = DictConvert.class)
+    @ExcelProperty(value = "日志类型", converter = DictConvert.class)
+    @DictFormat(SysDictTypeConstants.LOGIN_TYPE)
+    private Integer logType;
+
+    @ExcelProperty(value = "登录结果", converter = DictConvert.class)
     @DictFormat(SysDictTypeConstants.LOGIN_RESULT)
     private Integer result;
 
-    @ExcelProperty("登陆 IP")
+    @ExcelProperty("登 IP")
     private String userIp;
 
     @ExcelProperty("浏览器 UA")
     private String userAgent;
 
-    @ExcelProperty("登时间")
+    @ExcelProperty("登时间")
     private Date createTime;
 
 }

+ 1 - 1
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/logger/vo/loginlog/SysLoginLogExportReqVO.java

@@ -9,7 +9,7 @@ import java.util.Date;
 
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
 
-@ApiModel("登日志分页列表 Request VO")
+@ApiModel("登日志分页列表 Request VO")
 @Data
 public class SysLoginLogExportReqVO {
 

+ 1 - 1
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/logger/vo/loginlog/SysLoginLogPageReqVO.java

@@ -11,7 +11,7 @@ import java.util.Date;
 
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
 
-@ApiModel("登日志分页列表 Request VO")
+@ApiModel("登日志分页列表 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 public class SysLoginLogPageReqVO extends PageParam {

+ 11 - 2
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/logger/vo/loginlog/SysLoginLogRespVO.java

@@ -6,9 +6,10 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.ToString;
 
+import javax.validation.constraints.NotNull;
 import java.util.Date;
 
-@ApiModel("登日志 Response VO")
+@ApiModel("登日志 Response VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
@@ -17,7 +18,15 @@ public class SysLoginLogRespVO extends SysLoginLogBaseVO {
     @ApiModelProperty(value = "日志编号", required = true, example = "1024")
     private Long id;
 
-    @ApiModelProperty(value = "登陆时间", required = true)
+    @ApiModelProperty(value = "用户编号", required = true, example = "666")
+    @NotNull(message = "用户编号不能为空")
+    private Long userId;
+
+    @ApiModelProperty(value = "用户类型", required = true, example = "2", notes = "参见 UserTypeEnum 枚举")
+    @NotNull(message = "用户类型不能为空")
+    private Integer userType;
+
+    @ApiModelProperty(value = "登录时间", required = true)
     private Date createTime;
 
 }

+ 3 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/user/SysUserProfileController.http

@@ -0,0 +1,3 @@
+### 请求 /system/user/profile/get 接口 => 没有权限
+GET {{baseUrl}}/system/user/profile/get
+Authorization: Bearer test1

+ 11 - 4
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/user/SysUserProfileController.java

@@ -1,6 +1,8 @@
 package cn.iocoder.yudao.adminserver.modules.system.controller.user;
 
 import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.adminserver.modules.system.dal.dataobject.social.SysSocialUserDO;
+import cn.iocoder.yudao.adminserver.modules.system.service.social.SysSocialService;
 import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.adminserver.modules.system.controller.user.vo.profile.SysUserProfileRespVO;
@@ -52,6 +54,8 @@ public class SysUserProfileController {
     private SysPermissionService permissionService;
     @Resource
     private SysRoleService roleService;
+    @Resource
+    private SysSocialService socialService;
 
     @GetMapping("/get")
     @ApiOperation("获得登录用户信息")
@@ -72,6 +76,9 @@ public class SysUserProfileController {
             List<SysPostDO> posts = postService.getPosts(user.getPostIds());
             resp.setPosts(SysUserConvert.INSTANCE.convertList02(posts));
         }
+        // 获得社交用户信息
+        List<SysSocialUserDO> socialUsers = socialService.getSocialUserList(user.getId());
+        resp.setSocialUsers(SysUserConvert.INSTANCE.convertList03(socialUsers));
         return success(resp);
     }
 
@@ -89,14 +96,14 @@ public class SysUserProfileController {
         return success(true);
     }
 
-    @PutMapping("/upload-avatar")
+    @PutMapping("/update-avatar")
     @ApiOperation("上传用户个人头像")
-    public CommonResult<Boolean> updateUserAvatar(@RequestParam("avatarFile") MultipartFile file) throws IOException {
+    public CommonResult<String> updateUserAvatar(@RequestParam("avatarFile") MultipartFile file) throws IOException {
         if (file.isEmpty()) {
             throw ServiceExceptionUtil.exception(FILE_IS_EMPTY);
         }
-        userService.updateUserAvatar(getLoginUserId(), file.getInputStream());
-        return success(true);
+        String avatar = userService.updateUserAvatar(getLoginUserId(), file.getInputStream());
+        return success(avatar);
     }
 
 }

+ 17 - 1
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/user/vo/profile/SysUserProfileRespVO.java

@@ -25,7 +25,7 @@ public class SysUserProfileRespVO extends SysUserBaseVO {
     @ApiModelProperty(value = "状态", required = true, example = "1", notes = "参见 SysCommonStatusEnum 枚举类")
     private Integer status;
 
-    @ApiModelProperty(value = "最后登 IP", required = true, example = "192.168.1.1")
+    @ApiModelProperty(value = "最后登 IP", required = true, example = "192.168.1.1")
     private String loginIp;
 
     @ApiModelProperty(value = "最后登录时间", required = true, example = "时间戳格式")
@@ -48,6 +48,10 @@ public class SysUserProfileRespVO extends SysUserBaseVO {
      * 所属岗位数组
      */
     private List<Post> posts;
+    /**
+     * 社交用户数组
+     */
+    private List<SocialUser> socialUsers;
 
     @ApiModel("角色")
     @Data
@@ -85,4 +89,16 @@ public class SysUserProfileRespVO extends SysUserBaseVO {
 
     }
 
+    @ApiModel("社交用户")
+    @Data
+    public static class SocialUser {
+
+        @ApiModelProperty(value = "社交平台的类型", required = true, example = "10", notes = "参见 SysSocialTypeEnum 枚举类")
+        private Integer type;
+
+        @ApiModelProperty(value = "社交的全局编号", required = true, example = "IPRmJ0wvBptiPIlGEZiPewGwiEiE")
+        private String unionId;
+
+    }
+
 }

+ 1 - 1
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/user/vo/user/SysUserRespVO.java

@@ -19,7 +19,7 @@ public class SysUserRespVO extends SysUserBaseVO {
     @ApiModelProperty(value = "状态", required = true, example = "1", notes = "参见 SysCommonStatusEnum 枚举类")
     private Integer status;
 
-    @ApiModelProperty(value = "最后登 IP", required = true, example = "192.168.1.1")
+    @ApiModelProperty(value = "最后登 IP", required = true, example = "192.168.1.1")
     private String loginIp;
 
     @ApiModelProperty(value = "最后登录时间", required = true, example = "时间戳格式")

+ 5 - 6
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/convert/auth/SysAuthConvert.java

@@ -1,8 +1,9 @@
 package cn.iocoder.yudao.adminserver.modules.system.convert.auth;
 
-import cn.iocoder.yudao.framework.security.core.LoginUser;
 import cn.iocoder.yudao.adminserver.modules.system.controller.auth.vo.auth.SysAuthMenuRespVO;
 import cn.iocoder.yudao.adminserver.modules.system.controller.auth.vo.auth.SysAuthPermissionInfoRespVO;
+import cn.iocoder.yudao.adminserver.modules.system.controller.auth.vo.auth.SysAuthSocialLogin2ReqVO;
+import cn.iocoder.yudao.adminserver.modules.system.controller.auth.vo.auth.SysAuthSocialLoginReqVO;
 import cn.iocoder.yudao.adminserver.modules.system.controller.user.vo.profile.SysUserProfileUpdatePasswordReqVO;
 import cn.iocoder.yudao.adminserver.modules.system.controller.user.vo.profile.SysUserProfileUpdateReqVO;
 import cn.iocoder.yudao.adminserver.modules.system.dal.dataobject.permission.SysMenuDO;
@@ -10,16 +11,14 @@ import cn.iocoder.yudao.adminserver.modules.system.dal.dataobject.permission.Sys
 import cn.iocoder.yudao.adminserver.modules.system.dal.dataobject.user.SysUserDO;
 import cn.iocoder.yudao.adminserver.modules.system.enums.permission.MenuIdEnum;
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.framework.security.core.LoginUser;
+import me.zhyd.oauth.model.AuthCallback;
 import org.mapstruct.Mapper;
 import org.mapstruct.Mapping;
 import org.mapstruct.factory.Mappers;
 import org.slf4j.LoggerFactory;
 
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 
 @Mapper
 public interface SysAuthConvert {

+ 3 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/convert/user/SysUserConvert.java

@@ -7,6 +7,7 @@ import cn.iocoder.yudao.adminserver.modules.system.controller.user.vo.user.*;
 import cn.iocoder.yudao.adminserver.modules.system.dal.dataobject.dept.SysDeptDO;
 import cn.iocoder.yudao.adminserver.modules.system.dal.dataobject.dept.SysPostDO;
 import cn.iocoder.yudao.adminserver.modules.system.dal.dataobject.permission.SysRoleDO;
+import cn.iocoder.yudao.adminserver.modules.system.dal.dataobject.social.SysSocialUserDO;
 import cn.iocoder.yudao.adminserver.modules.system.dal.dataobject.user.SysUserDO;
 import org.mapstruct.Mapper;
 import org.mapstruct.factory.Mappers;
@@ -42,4 +43,6 @@ public interface SysUserConvert {
 
     List<SysUserProfileRespVO.Post> convertList02(List<SysPostDO> list);
 
+    List<SysUserProfileRespVO.SocialUser> convertList03(List<SysSocialUserDO> list);
+
 }

+ 14 - 3
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/dal/dataobject/logger/SysLoginLogDO.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.adminserver.modules.system.dal.dataobject.logger;
 
+import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 import cn.iocoder.yudao.adminserver.modules.system.enums.logger.SysLoginLogTypeEnum;
 import cn.iocoder.yudao.adminserver.modules.system.enums.logger.SysLoginResultEnum;
@@ -9,9 +10,9 @@ import lombok.EqualsAndHashCode;
 import lombok.ToString;
 
 /**
- * 登日志表
+ * 登日志表
  *
- * 注意,包括登和登出两种行为
+ * 注意,包括登和登出两种行为
  *
  * @author ruoyi
  */
@@ -35,6 +36,16 @@ public class SysLoginLogDO extends BaseDO {
      * 链路追踪编号
      */
     private String traceId;
+    /**
+     * 用户编号
+     */
+    private Long userId;
+    /**
+     * 用户类型
+     *
+     * 枚举 {@link UserTypeEnum}
+     */
+    private Integer userType;
     /**
      * 用户账号
      *
@@ -42,7 +53,7 @@ public class SysLoginLogDO extends BaseDO {
      */
     private String username;
     /**
-     * 登结果
+     * 登结果
      *
      * 枚举 {@link SysLoginResultEnum}
      */

+ 82 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/dal/dataobject/social/SysSocialUserDO.java

@@ -0,0 +1,82 @@
+package cn.iocoder.yudao.adminserver.modules.system.dal.dataobject.social;
+
+import cn.iocoder.yudao.adminserver.modules.system.dal.dataobject.user.SysUserDO;
+import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+/**
+ * 社交用户
+ * 通过 {@link SysSocialUserDO#getUserId()} 关联到对应的 {@link SysUserDO}
+ *
+ * @author weir
+ */
+@TableName(value = "sys_social_user", autoResultMap = true)
+@Data
+@EqualsAndHashCode(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class SysSocialUserDO extends BaseDO {
+
+    /**
+     * 自增主键
+     */
+    @TableId
+    private Long id;
+    /**
+     * 关联的用户编号
+     */
+    private Long userId;
+    /**
+     * 用户类型
+     *
+     * 枚举 {@link UserTypeEnum}
+     */
+    private Integer userType;
+
+    /**
+     * 社交平台的类型
+     *
+     * 枚举 {@link UserTypeEnum}
+     */
+    private Integer type;
+
+    /**
+     * 社交 openid
+     */
+    private String openid;
+    /**
+     * 社交 token
+     */
+    private String token;
+    /**
+     * 社交的全局编号
+     *
+     * 例如说,微信平台的 https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/union-id.html
+     * 如果没有 unionId 的平台,直接使用 openid 作为该字段的值
+     */
+    private String unionId;
+    /**
+     * 原始 Token 数据,一般是 JSON 格式
+     */
+    private String rawTokenInfo;
+
+    /**
+     * 用户昵称
+     */
+    private String nickname;
+    /**
+     * 用户头像
+     */
+    private String avatar;
+    /**
+     * 原始用户数据,一般是 JSON 格式
+     */
+    private String rawUserInfo;
+
+}
+
+

+ 1 - 1
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/dal/dataobject/user/SysUserDO.java

@@ -14,7 +14,7 @@ import java.util.Date;
 import java.util.Set;
 
 /**
- * 用户 DO
+ * 管理后台的用户 DO
  *
  * @author ruoyi
  */

+ 28 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/dal/mysql/social/SysSocialUserMapper.java

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.adminserver.modules.system.dal.mysql.social;
+
+import cn.iocoder.yudao.adminserver.modules.system.dal.dataobject.social.SysSocialUserDO;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.Collection;
+import java.util.List;
+
+@Mapper
+public interface SysSocialUserMapper extends BaseMapperX<SysSocialUserDO> {
+
+    default List<SysSocialUserDO> selectListByTypeAndUnionId(Integer userType, Collection<Integer> types, String unionId) {
+        return selectList(new QueryWrapper<SysSocialUserDO>().eq("user_type", userType)
+                .in("type", types).eq("union_id", unionId));
+    }
+
+    default List<SysSocialUserDO> selectListByTypeAndUserId(Integer userType, Collection<Integer> types, Long userId) {
+        return selectList(new QueryWrapper<SysSocialUserDO>().eq("user_type", userType)
+                .in("type", types).eq("user_id", userId));
+    }
+
+    default List<SysSocialUserDO> selectListByUserId(Integer userType, Long userId) {
+        return selectList(new QueryWrapper<SysSocialUserDO>().eq("user_type", userType).eq("user_id", userId));
+    }
+
+}

+ 12 - 1
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/dal/redis/SysRedisKeyConstants.java

@@ -2,6 +2,9 @@ package cn.iocoder.yudao.adminserver.modules.system.dal.redis;
 
 import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
 import cn.iocoder.yudao.framework.security.core.LoginUser;
+import me.zhyd.oauth.model.AuthUser;
+
+import java.time.Duration;
 
 import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.STRING;
 
@@ -12,7 +15,7 @@ import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.S
  */
 public interface SysRedisKeyConstants {
 
-    RedisKeyDefine LOGIN_USER = new RedisKeyDefine("登用户的缓存",
+    RedisKeyDefine LOGIN_USER = new RedisKeyDefine("登用户的缓存",
             "login_user:%s", // 参数为 sessionId
             STRING, LoginUser.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC);
 
@@ -20,4 +23,12 @@ public interface SysRedisKeyConstants {
             "captcha_code:%s", // 参数为 uuid
             STRING, String.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC);
 
+    RedisKeyDefine SOCIAL_AUTH_USER = new RedisKeyDefine("社交的授权用户",
+            "social_auth_user:%d:%s", // 参数为 type,code
+            STRING, AuthUser.class, Duration.ofDays(1));
+
+    RedisKeyDefine SOCIAL_AUTH_STATE = new RedisKeyDefine("社交的 state",
+            "social_auth_state:%s", // 参数为 state
+            STRING, String.class, Duration.ofHours(24)); // 值为 state
+
 }

+ 1 - 1
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/dal/redis/auth/SysLoginUserRedisDAO.java

@@ -22,7 +22,7 @@ public class SysLoginUserRedisDAO {
     @Resource
     private StringRedisTemplate stringRedisTemplate;
     @Resource
-    private SysUserSessionService sysUserSessionService;
+    private SysUserSessionService sysUserSessionService; // TODO 芋艿:得看看怎么拿出去
 
     public LoginUser get(String sessionId) {
         String redisKey = formatKey(sessionId);

+ 38 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/dal/redis/social/SysSocialAuthUserRedisDAO.java

@@ -0,0 +1,38 @@
+package cn.iocoder.yudao.adminserver.modules.system.dal.redis.social;
+
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import me.zhyd.oauth.model.AuthCallback;
+import me.zhyd.oauth.model.AuthUser;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Repository;
+
+import javax.annotation.Resource;
+
+import static cn.iocoder.yudao.adminserver.modules.system.dal.redis.SysRedisKeyConstants.SOCIAL_AUTH_USER;
+
+/**
+ * 社交 {@link me.zhyd.oauth.model.AuthUser} 的 RedisDAO
+ *
+ * @author 芋道源码
+ */
+@Repository
+public class SysSocialAuthUserRedisDAO {
+
+    @Resource
+    private StringRedisTemplate stringRedisTemplate;
+
+    public AuthUser get(Integer type, AuthCallback authCallback) {
+        String redisKey = formatKey(type, authCallback.getCode());
+        return JsonUtils.parseObject(stringRedisTemplate.opsForValue().get(redisKey), AuthUser.class);
+    }
+
+    public void set(Integer type, AuthCallback authCallback, AuthUser authUser) {
+        String redisKey = formatKey(type, authCallback.getCode());
+        stringRedisTemplate.opsForValue().set(redisKey, JsonUtils.toJsonString(authUser), SOCIAL_AUTH_USER.getTimeout());
+    }
+
+    private static String formatKey(Integer type, String code) {
+        return String.format(SOCIAL_AUTH_USER.getKeyTemplate(), type, code);
+    }
+
+}

+ 2 - 2
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/enums/SysDictTypeConstants.java

@@ -12,7 +12,8 @@ public interface SysDictTypeConstants {
 
     String USER_SEX = "sys_user_sex"; // 用户性别
     String OPERATE_TYPE = "sys_operate_type"; // 操作类型
-    String LOGIN_RESULT = "sys_login_result"; // 登陆结果
+    String LOGIN_TYPE = "sys_login_type"; // 登录日志的类型
+    String LOGIN_RESULT = "sys_login_result"; // 登录结果
     String CONFIG_TYPE = "sys_config_type"; // 参数配置类型
     String BOOLEAN_STRING = "sys_boolean_string"; // Boolean 是否类型
 
@@ -21,5 +22,4 @@ public interface SysDictTypeConstants {
     String SMS_SEND_STATUS = "sys_sms_send_status"; // 短信发送状态
     String SMS_RECEIVE_STATUS = "sys_sms_receive_status"; // 短信接收状态
 
-
 }

+ 8 - 1
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/enums/SysErrorCodeConstants.java

@@ -1,6 +1,8 @@
 package cn.iocoder.yudao.adminserver.modules.system.enums;
 
+import cn.iocoder.yudao.adminserver.modules.tool.framework.errorcode.config.ErrorCodeConfiguration;
 import cn.iocoder.yudao.framework.common.exception.ErrorCode;
+import org.springframework.validation.Errors;
 
 /**
  * System 错误码枚举类
@@ -12,9 +14,10 @@ public interface SysErrorCodeConstants {
     // ========== AUTH 模块 1002000000 ==========
     ErrorCode AUTH_LOGIN_BAD_CREDENTIALS = new ErrorCode(1002000000, "登录失败,账号密码不正确");
     ErrorCode AUTH_LOGIN_USER_DISABLED = new ErrorCode(1002000001, "登录失败,账号被禁用");
-    ErrorCode AUTH_LOGIN_FAIL_UNKNOWN = new ErrorCode(1002000002, "登录失败"); // 登失败的兜底,位置原因
+    ErrorCode AUTH_LOGIN_FAIL_UNKNOWN = new ErrorCode(1002000002, "登录失败"); // 登失败的兜底,位置原因
     ErrorCode AUTH_LOGIN_CAPTCHA_NOT_FOUND = new ErrorCode(1002000003, "验证码不存在");
     ErrorCode AUTH_LOGIN_CAPTCHA_CODE_ERROR = new ErrorCode(1002000004, "验证码不正确");
+    ErrorCode AUTH_THIRD_LOGIN_NOT_BIND = new ErrorCode(1002000005, "未绑定账号,需要进行绑定");
 
     // ========== TOKEN 模块 1002001000 ==========
     ErrorCode TOKEN_EXPIRED = new ErrorCode(1002001000, "Token 已经过期");
@@ -95,4 +98,8 @@ public interface SysErrorCodeConstants {
     ErrorCode ERROR_CODE_NOT_EXISTS = new ErrorCode(1002013000, "错误码不存在");
     ErrorCode ERROR_CODE_DUPLICATE = new ErrorCode(1002013001, "已经存在编码为【{}】的错误码");
 
+    // ========== 社交模块 1002014000 ==========
+    ErrorCode SOCIAL_AUTH_FAILURE = new ErrorCode(1002014000, "社交授权失败,原因是:{}");
+    ErrorCode SOCIAL_UNBIND_NOT_SELF = new ErrorCode(1002014001, "社交解绑失败,非当前用户绑定");
+
 }

+ 6 - 4
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/enums/common/SysSexEnum.java

@@ -11,10 +11,12 @@ import lombok.Getter;
 @Getter
 @AllArgsConstructor
 public enum SysSexEnum {
-
-    MALE(1), // 男
-    FEMALE(2); // 女
-
+    /** 男 */
+    MALE(1),
+    /** 女 */
+    FEMALE(2),
+    /* 未知 */
+    UNKNOWN(3);
     /**
      * 性别
      */

+ 3 - 1
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/enums/logger/SysLoginLogTypeEnum.java

@@ -4,13 +4,15 @@ import lombok.AllArgsConstructor;
 import lombok.Getter;
 
 /**
- * 登日志的类型枚举
+ * 登日志的类型枚举
  */
 @Getter
 @AllArgsConstructor
 public enum SysLoginLogTypeEnum {
 
     LOGIN_USERNAME(100), // 使用账号登录
+    LOGIN_SOCIAL(101), // 使用社交登录
+    LOGIN_MOCK(102), // 使用 Mock 登录
 
     LOGOUT_SELF(200),  // 自己主动登出
     LOGOUT_TIMEOUT(201), // 超时登出

+ 1 - 1
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/enums/logger/SysLoginResultEnum.java

@@ -4,7 +4,7 @@ import lombok.AllArgsConstructor;
 import lombok.Getter;
 
 /**
- * 登结果的枚举类
+ * 登结果的枚举类
  */
 @Getter
 @AllArgsConstructor

+ 55 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/enums/social/SysSocialTypeEnum.java

@@ -0,0 +1,55 @@
+package cn.iocoder.yudao.adminserver.modules.system.enums.social;
+
+import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.util.ArrayUtil;
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * 社交平台的类型枚举
+ *
+ * @author 芋道源码
+ */
+@Getter
+@AllArgsConstructor
+public enum SysSocialTypeEnum implements IntArrayValuable {
+
+    GITEE(10, "GITEE"), // https://gitee.com/api/v5/oauth_doc#/
+    DINGTALK(20, "DINGTALK"), // https://developers.dingtalk.com/document/app/obtain-identity-credentials
+    WECHAT_ENTERPRISE(30, "WECHAT_ENTERPRISE"), // https://xkcoding.com/2019/08/06/use-justauth-integration-wechat-enterprise.html
+    ;
+
+    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(SysSocialTypeEnum::getType).toArray();
+
+    public static final List<Integer> WECHAT_ALL = ListUtil.toList(WECHAT_ENTERPRISE.type);
+
+    /**
+     * 类型
+     */
+    private final Integer type;
+    /**
+     * 类型的标识
+     */
+    private final String source;
+
+    @Override
+    public int[] array() {
+        return ARRAYS;
+    }
+
+    public static SysSocialTypeEnum valueOfType(Integer type) {
+        return ArrayUtil.firstMatch(o -> o.getType().equals(type), values());
+    }
+
+    public static List<Integer> getRelationTypes(Integer type) {
+        if (WECHAT_ALL.contains(type)) {
+            return WECHAT_ALL;
+        }
+        return ListUtil.toList(type);
+    }
+
+}

+ 35 - 5
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/auth/SysAuthService.java

@@ -1,25 +1,55 @@
 package cn.iocoder.yudao.adminserver.modules.system.service.auth;
 
+import cn.iocoder.yudao.adminserver.modules.system.controller.auth.vo.auth.*;
 import cn.iocoder.yudao.framework.security.core.service.SecurityAuthFrameworkService;
-import cn.iocoder.yudao.adminserver.modules.system.controller.auth.vo.auth.SysAuthLoginReqVO;
+
+import javax.validation.Valid;
 
 /**
  * 认证 Service 接口
  *
- * 提供用户的账号密码登、token 的校验等认证相关的功能
+ * 提供用户的账号密码登、token 的校验等认证相关的功能
  *
  * @author 芋道源码
  */
 public interface SysAuthService extends SecurityAuthFrameworkService {
 
     /**
-     * 登陆用户
+     * 账号登录
+     *
+     * @param reqVO 登录信息
+     * @param userIp 用户 IP
+     * @param userAgent 用户 UA
+     * @return 身份令牌,使用 JWT 方式
+     */
+    String login(@Valid SysAuthLoginReqVO reqVO, String userIp, String userAgent);
+
+    /**
+     * 社交登录,使用 code 授权码
      *
-     * @param reqVO 登陆信息
+     * @param reqVO 登信息
      * @param userIp 用户 IP
      * @param userAgent 用户 UA
      * @return 身份令牌,使用 JWT 方式
      */
-    String login(SysAuthLoginReqVO reqVO, String userIp, String userAgent);
+    String socialLogin(@Valid SysAuthSocialLoginReqVO reqVO, String userIp, String userAgent);
+
+    /**
+     * 社交登录,使用 code 授权码 + 账号密码
+     *
+     * @param reqVO 登录信息
+     * @param userIp 用户 IP
+     * @param userAgent 用户 UA
+     * @return 身份令牌,使用 JWT 方式
+     */
+    String socialLogin2(@Valid SysAuthSocialLogin2ReqVO reqVO, String userIp, String userAgent);
+
+    /**
+     * 社交绑定,使用 code 授权码
+     *
+     * @param userId 用户编号
+     * @param reqVO 绑定信息
+     */
+    void socialBind(Long userId, @Valid SysAuthSocialBindReqVO reqVO);
 
 }

+ 9 - 2
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/auth/SysUserSessionService.java

@@ -15,7 +15,7 @@ public interface SysUserSessionService {
     /**
      * 创建在线用户 Session
      *
-     * @param loginUser 登用户
+     * @param loginUser 登用户
      * @param userIp 用户 IP
      * @param userAgent 用户 UA
      * @return Session 编号
@@ -26,7 +26,7 @@ public interface SysUserSessionService {
      * 刷新在线用户 Session 的更新时间
      *
      * @param sessionId Session 编号
-     * @param loginUser 登用户
+     * @param loginUser 登用户
      */
     void refreshUserSession(String sessionId, LoginUser loginUser);
 
@@ -45,6 +45,13 @@ public interface SysUserSessionService {
      */
     LoginUser getLoginUser(String sessionId);
 
+    /**
+     * 获取当前登录用户信息
+     * @param username 用户名称
+     * @return 在线用户
+     */
+    String getSessionId(String username);
+
     /**
      * 获得 Session 超时时间,单位:毫秒
      *

+ 94 - 21
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/auth/impl/SysAuthServiceImpl.java

@@ -1,12 +1,13 @@
 package cn.iocoder.yudao.adminserver.modules.system.service.auth.impl;
 
-import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
-import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
-import cn.iocoder.yudao.framework.security.core.LoginUser;
-import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
+import cn.hutool.core.collection.CollUtil;
 import cn.iocoder.yudao.adminserver.modules.system.controller.auth.vo.auth.SysAuthLoginReqVO;
+import cn.iocoder.yudao.adminserver.modules.system.controller.auth.vo.auth.SysAuthSocialBindReqVO;
+import cn.iocoder.yudao.adminserver.modules.system.controller.auth.vo.auth.SysAuthSocialLogin2ReqVO;
+import cn.iocoder.yudao.adminserver.modules.system.controller.auth.vo.auth.SysAuthSocialLoginReqVO;
 import cn.iocoder.yudao.adminserver.modules.system.controller.logger.vo.loginlog.SysLoginLogCreateReqVO;
 import cn.iocoder.yudao.adminserver.modules.system.convert.auth.SysAuthConvert;
+import cn.iocoder.yudao.adminserver.modules.system.dal.dataobject.social.SysSocialUserDO;
 import cn.iocoder.yudao.adminserver.modules.system.dal.dataobject.user.SysUserDO;
 import cn.iocoder.yudao.adminserver.modules.system.enums.logger.SysLoginLogTypeEnum;
 import cn.iocoder.yudao.adminserver.modules.system.enums.logger.SysLoginResultEnum;
@@ -15,9 +16,14 @@ import cn.iocoder.yudao.adminserver.modules.system.service.auth.SysUserSessionSe
 import cn.iocoder.yudao.adminserver.modules.system.service.common.SysCaptchaService;
 import cn.iocoder.yudao.adminserver.modules.system.service.logger.SysLoginLogService;
 import cn.iocoder.yudao.adminserver.modules.system.service.permission.SysPermissionService;
+import cn.iocoder.yudao.adminserver.modules.system.service.social.SysSocialService;
 import cn.iocoder.yudao.adminserver.modules.system.service.user.SysUserService;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
 import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
+import cn.iocoder.yudao.framework.security.core.LoginUser;
 import lombok.extern.slf4j.Slf4j;
+import me.zhyd.oauth.model.AuthUser;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.BadCredentialsException;
@@ -31,10 +37,11 @@ import org.springframework.stereotype.Service;
 import org.springframework.util.Assert;
 
 import javax.annotation.Resource;
+import java.util.List;
 import java.util.Set;
 
-import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.adminserver.modules.system.enums.SysErrorCodeConstants.*;
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static java.util.Collections.singleton;
 
 /**
@@ -49,6 +56,7 @@ public class SysAuthServiceImpl implements SysAuthService {
     @Resource
     @Lazy // 延迟加载,因为存在相互依赖的问题
     private AuthenticationManager authenticationManager;
+
     @Resource
     private SysUserService userService;
     @Resource
@@ -59,6 +67,8 @@ public class SysAuthServiceImpl implements SysAuthService {
     private SysLoginLogService loginLogService;
     @Resource
     private SysUserSessionService userSessionService;
+    @Resource
+    private SysSocialService socialService;
 
     @Override
     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
@@ -78,6 +88,8 @@ public class SysAuthServiceImpl implements SysAuthService {
         if (user == null) {
             throw new UsernameNotFoundException(String.valueOf(userId));
         }
+        this.createLoginLog(user.getUsername(), SysLoginLogTypeEnum.LOGIN_MOCK, SysLoginResultEnum.SUCCESS);
+
         // 创建 LoginUser 对象
         LoginUser loginUser = SysAuthConvert.INSTANCE.convert(user);
         loginUser.setRoleIds(this.getUserRoleIds(loginUser.getId())); // 获取用户角色列表
@@ -89,33 +101,35 @@ public class SysAuthServiceImpl implements SysAuthService {
         // 判断验证码是否正确
         this.verifyCaptcha(reqVO.getUsername(), reqVO.getUuid(), reqVO.getCode());
 
-        // 使用账号密码,进行登
+        // 使用账号密码,进行登
         LoginUser loginUser = this.login0(reqVO.getUsername(), reqVO.getPassword());
         loginUser.setRoleIds(this.getUserRoleIds(loginUser.getId())); // 获取用户角色列表
 
-        // 缓存登用户到 Redis 中,返回 sessionId 编号
+        // 缓存登用户到 Redis 中,返回 sessionId 编号
         return userSessionService.createUserSession(loginUser, userIp, userAgent);
     }
 
     private void verifyCaptcha(String username, String captchaUUID, String captchaCode) {
+        final SysLoginLogTypeEnum logTypeEnum = SysLoginLogTypeEnum.LOGIN_USERNAME;
         String code = captchaService.getCaptchaCode(captchaUUID);
         // 验证码不存在
         if (code == null) {
-            // 创建登失败日志(验证码不存在)
-            this.createLoginLog(username, SysLoginResultEnum.CAPTCHA_NOT_FOUND);
-            throw ServiceExceptionUtil.exception(AUTH_LOGIN_CAPTCHA_NOT_FOUND);
+            // 创建登失败日志(验证码不存在)
+            this.createLoginLog(username, logTypeEnum, SysLoginResultEnum.CAPTCHA_NOT_FOUND);
+            throw exception(AUTH_LOGIN_CAPTCHA_NOT_FOUND);
         }
         // 验证码不正确
         if (!code.equals(captchaCode)) {
-            // 创建登失败日志(验证码不正确)
-            this.createLoginLog(username, SysLoginResultEnum.CAPTCHA_CODE_ERROR);
-            throw ServiceExceptionUtil.exception(AUTH_LOGIN_CAPTCHA_CODE_ERROR);
+            // 创建登失败日志(验证码不正确)
+            this.createLoginLog(username, logTypeEnum, SysLoginResultEnum.CAPTCHA_CODE_ERROR);
+            throw exception(AUTH_LOGIN_CAPTCHA_CODE_ERROR);
         }
         // 正确,所以要删除下验证码
         captchaService.deleteCaptchaCode(captchaUUID);
     }
 
     private LoginUser login0(String username, String password) {
+        final SysLoginLogTypeEnum logTypeEnum = SysLoginLogTypeEnum.LOGIN_USERNAME;
         // 用户验证
         Authentication authentication;
         try {
@@ -123,25 +137,25 @@ public class SysAuthServiceImpl implements SysAuthService {
             // 在其内部,会调用到 loadUserByUsername 方法,获取 User 信息
             authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
         } catch (BadCredentialsException badCredentialsException) {
-            this.createLoginLog(username, SysLoginResultEnum.BAD_CREDENTIALS);
+            this.createLoginLog(username, logTypeEnum, SysLoginResultEnum.BAD_CREDENTIALS);
             throw exception(AUTH_LOGIN_BAD_CREDENTIALS);
         } catch (DisabledException disabledException) {
-            this.createLoginLog(username, SysLoginResultEnum.USER_DISABLED);
+            this.createLoginLog(username, logTypeEnum, SysLoginResultEnum.USER_DISABLED);
             throw exception(AUTH_LOGIN_USER_DISABLED);
         } catch (AuthenticationException authenticationException) {
             log.error("[login0][username({}) 发生未知异常]", username, authenticationException);
-            this.createLoginLog(username, SysLoginResultEnum.UNKNOWN_ERROR);
+            this.createLoginLog(username, logTypeEnum, SysLoginResultEnum.UNKNOWN_ERROR);
             throw exception(AUTH_LOGIN_FAIL_UNKNOWN);
         }
-        // 登陆成功
+        // 登录成功的日志
         Assert.notNull(authentication.getPrincipal(), "Principal 不会为空");
-        this.createLoginLog(username, SysLoginResultEnum.SUCCESS);
+        this.createLoginLog(username, logTypeEnum, SysLoginResultEnum.SUCCESS);
         return (LoginUser) authentication.getPrincipal();
     }
 
-    private void createLoginLog(String username, SysLoginResultEnum loginResult) {
+    private void createLoginLog(String username, SysLoginLogTypeEnum logTypeEnum, SysLoginResultEnum loginResult) {
         SysLoginLogCreateReqVO reqVO = new SysLoginLogCreateReqVO();
-        reqVO.setLogType(SysLoginLogTypeEnum.LOGIN_USERNAME.getType());
+        reqVO.setLogType(logTypeEnum.getType());
         reqVO.setTraceId(TracerUtils.getTraceId());
         reqVO.setUsername(username);
         reqVO.setUserAgent(ServletUtils.getUserAgent());
@@ -160,6 +174,65 @@ public class SysAuthServiceImpl implements SysAuthService {
         return permissionService.getUserRoleIds(userId, singleton(CommonStatusEnum.ENABLE.getStatus()));
     }
 
+    @Override
+    public String socialLogin(SysAuthSocialLoginReqVO reqVO, String userIp, String userAgent) {
+        // 使用 code 授权码,进行登录
+        AuthUser authUser = socialService.getAuthUser(reqVO.getType(), reqVO.getCode(), reqVO.getState());
+        Assert.notNull(authUser, "授权用户不为空");
+
+        // 如果未绑定 SysSocialUserDO 用户,则无法自动登录,进行报错
+        String unionId = socialService.getAuthUserUnionId(authUser);
+        List<SysSocialUserDO> socialUsers = socialService.getAllSocialUserList(reqVO.getType(), unionId);
+        if (CollUtil.isEmpty(socialUsers)) {
+            throw exception(AUTH_THIRD_LOGIN_NOT_BIND);
+        }
+
+        // 自动登录
+        SysUserDO user = userService.getUser(socialUsers.get(0).getUserId());
+        if (user == null) {
+            throw exception(USER_NOT_EXISTS);
+        }
+        this.createLoginLog(user.getUsername(), SysLoginLogTypeEnum.LOGIN_SOCIAL, SysLoginResultEnum.SUCCESS);
+
+        // 创建 LoginUser 对象
+        LoginUser loginUser = SysAuthConvert.INSTANCE.convert(user);
+        // TODO 芋艿:需要改造下,增加各种登录方式
+        loginUser.setRoleIds(this.getUserRoleIds(loginUser.getId())); // 获取用户角色列表
+
+        // 绑定社交用户(更新)
+        socialService.bindSocialUser(loginUser.getId(), reqVO.getType(), authUser);
+
+        // 缓存登录用户到 Redis 中,返回 sessionId 编号
+        return userSessionService.createUserSession(loginUser, userIp, userAgent);
+    }
+
+    @Override
+    public String socialLogin2(SysAuthSocialLogin2ReqVO reqVO, String userIp, String userAgent) {
+        // 使用 code 授权码,进行登录
+        AuthUser authUser = socialService.getAuthUser(reqVO.getType(), reqVO.getCode(), reqVO.getState());
+        Assert.notNull(authUser, "授权用户不为空");
+
+        // 使用账号密码,进行登录。
+        LoginUser loginUser = this.login0(reqVO.getUsername(), reqVO.getPassword());
+        loginUser.setRoleIds(this.getUserRoleIds(loginUser.getId())); // 获取用户角色列表
+
+        // 绑定社交用户(新增)
+        socialService.bindSocialUser(loginUser.getId(), reqVO.getType(), authUser);
+
+        // 缓存登录用户到 Redis 中,返回 sessionId 编号
+        return userSessionService.createUserSession(loginUser, userIp, userAgent);
+    }
+
+    @Override
+    public void socialBind(Long userId, SysAuthSocialBindReqVO reqVO) {
+        // 使用 code 授权码,进行登录
+        AuthUser authUser = socialService.getAuthUser(reqVO.getType(), reqVO.getCode(), reqVO.getState());
+        Assert.notNull(authUser, "授权用户不为空");
+
+        // 绑定社交用户(新增)
+        socialService.bindSocialUser(userId, reqVO.getType(), authUser);
+    }
+
     @Override
     public void logout(String token) {
         // 查询用户信息
@@ -206,7 +279,7 @@ public class SysAuthServiceImpl implements SysAuthService {
         // 重新加载 SysUserDO 信息
         SysUserDO user = userService.getUser(loginUser.getId());
         if (user == null || CommonStatusEnum.DISABLE.getStatus().equals(user.getStatus())) {
-            throw exception(TOKEN_EXPIRED); // 校验 token 时,用户被禁用的情况下,也认为 token 过期,方便前端跳转到登界面
+            throw exception(TOKEN_EXPIRED); // 校验 token 时,用户被禁用的情况下,也认为 token 过期,方便前端跳转到登界面
         }
 
         // 刷新 LoginUser 缓存

+ 14 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/auth/impl/SysUserSessionServiceImpl.java

@@ -18,7 +18,11 @@ import cn.iocoder.yudao.adminserver.modules.system.enums.logger.SysLoginResultEn
 import cn.iocoder.yudao.adminserver.modules.system.service.auth.SysUserSessionService;
 import cn.iocoder.yudao.adminserver.modules.system.service.logger.SysLoginLogService;
 import cn.iocoder.yudao.adminserver.modules.system.service.user.SysUserService;
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.google.common.collect.Lists;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
@@ -34,6 +38,7 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.addTime;
  *
  * @author 芋道源码
  */
+@Slf4j
 @Service
 public class SysUserSessionServiceImpl implements SysUserSessionService {
 
@@ -91,6 +96,15 @@ public class SysUserSessionServiceImpl implements SysUserSessionService {
         return loginUserRedisDAO.get(sessionId);
     }
 
+    @Override
+    public String getSessionId(String username) {
+        QueryWrapper<SysUserSessionDO> wrapper = new QueryWrapper<>();
+        wrapper.eq("username", username);
+        wrapper.orderByDesc("create_time");
+        SysUserSessionDO sysUserSessionDO = userSessionMapper.selectOne(wrapper);
+        return sysUserSessionDO.getId();
+    }
+
     @Override
     public Long getSessionTimeoutMillis() {
         return securityProperties.getSessionTimeout().toMillis();

+ 6 - 6
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/logger/SysLoginLogService.java

@@ -9,30 +9,30 @@ import cn.iocoder.yudao.adminserver.modules.system.dal.dataobject.logger.SysLogi
 import java.util.List;
 
 /**
- * 登日志 Service 接口
+ * 登日志 Service 接口
  */
 public interface SysLoginLogService {
 
     /**
-     * 创建登日志
+     * 创建登日志
      *
      * @param reqVO 日志信息
      */
     void createLoginLog(SysLoginLogCreateReqVO reqVO);
 
     /**
-     * 获得登日志分页
+     * 获得登日志分页
      *
      * @param reqVO 分页条件
-     * @return 登日志分页
+     * @return 登日志分页
      */
     PageResult<SysLoginLogDO> getLoginLogPage(SysLoginLogPageReqVO reqVO);
 
     /**
-     * 获得登日志列表
+     * 获得登日志列表
      *
      * @param reqVO 列表条件
-     * @return 登日志列表
+     * @return 登日志列表
      */
     List<SysLoginLogDO> getLoginLogList(SysLoginLogExportReqVO reqVO);
 

+ 13 - 1
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/logger/impl/SysLoginLogServiceImpl.java

@@ -1,5 +1,8 @@
 package cn.iocoder.yudao.adminserver.modules.system.service.logger.impl;
 
+import cn.iocoder.yudao.adminserver.modules.system.dal.dataobject.user.SysUserDO;
+import cn.iocoder.yudao.adminserver.modules.system.service.user.SysUserService;
+import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.adminserver.modules.system.controller.logger.vo.loginlog.SysLoginLogCreateReqVO;
 import cn.iocoder.yudao.adminserver.modules.system.controller.logger.vo.loginlog.SysLoginLogExportReqVO;
@@ -14,17 +17,26 @@ import javax.annotation.Resource;
 import java.util.List;
 
 /**
- * 登日志 Service 实现
+ * 登日志 Service 实现
  */
 @Service
 public class SysLoginLogServiceImpl implements SysLoginLogService {
 
     @Resource
     private SysLoginLogMapper loginLogMapper;
+    @Resource
+    private SysUserService userService;
 
     @Override
     public void createLoginLog(SysLoginLogCreateReqVO reqVO) {
         SysLoginLogDO loginLog = SysLoginLogConvert.INSTANCE.convert(reqVO);
+        // 获得用户
+        SysUserDO user = userService.getUserByUsername(reqVO.getUsername());
+        if (user != null) {
+            loginLog.setUserId(user.getId());
+        }
+        loginLog.setUserType(UserTypeEnum.ADMIN.getValue());
+        // 插入
         loginLogMapper.insert(loginLog);
     }
 

+ 2 - 2
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/permission/impl/SysPermissionServiceImpl.java

@@ -280,7 +280,7 @@ public class SysPermissionServiceImpl implements SysPermissionService {
             return true;
         }
 
-        // 获得当前登的角色。如果为空,说明没有权限
+        // 获得当前登的角色。如果为空,说明没有权限
         Set<Long> roleIds = SecurityFrameworkUtils.getLoginUserRoleIds();
         if (CollUtil.isEmpty(roleIds)) {
             return false;
@@ -315,7 +315,7 @@ public class SysPermissionServiceImpl implements SysPermissionService {
             return true;
         }
 
-        // 获得当前登的角色。如果为空,说明没有权限
+        // 获得当前登的角色。如果为空,说明没有权限
         Set<Long> roleIds = SecurityFrameworkUtils.getLoginUserRoleIds();
         if (CollUtil.isEmpty(roleIds)) {
             return false;

+ 80 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/social/SysSocialService.java

@@ -0,0 +1,80 @@
+package cn.iocoder.yudao.adminserver.modules.system.service.social;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.adminserver.modules.system.dal.dataobject.social.SysSocialUserDO;
+import cn.iocoder.yudao.adminserver.modules.system.enums.social.SysSocialTypeEnum;
+import cn.iocoder.yudao.framework.common.exception.ServiceException;
+import me.zhyd.oauth.model.AuthUser;
+
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+/**
+ * 社交 Service 接口,例如说社交平台的授权登录
+ *
+ * @author 芋道源码
+ */
+public interface SysSocialService {
+
+    /**
+     * 获得社交平台的授权 URL
+     *
+     * @param type 社交平台的类型 {@link SysSocialTypeEnum}
+     * @param redirectUri 重定向 URL
+     * @return 社交平台的授权 URL
+     */
+    String getAuthorizeUrl(Integer type, String redirectUri);
+
+    /**
+     * 获得授权的用户
+     * 如果授权失败,则会抛出 {@link ServiceException} 异常
+     *
+     * @param type 社交平台的类型 {@link SysSocialTypeEnum}
+     * @param code 授权码
+     * @param state state
+     * @return 授权用户
+     */
+    @NotNull
+    AuthUser getAuthUser(Integer type, String code, String state);
+
+    default String getAuthUserUnionId(AuthUser authUser) {
+        return StrUtil.blankToDefault(authUser.getToken().getUnionId(), authUser.getUuid());
+    }
+
+    /**
+     * 获得 unionId 对应的某个社交平台的“所有”社交用户
+     * 注意,这里的“所有”,指的是类似【微信】平台,包括了小程序、公众号、PC 网站,他们的 unionId 是一致的
+     *
+     * @param type 社交平台的类型 {@link SysSocialTypeEnum}
+     * @param unionId 社交平台的 unionId
+     * @return 社交用户列表
+     */
+    List<SysSocialUserDO> getAllSocialUserList(Integer type, String unionId);
+
+    /**
+     * 获得指定用户的社交用户列表
+     *
+     * @param userId 用户编号
+     * @return 社交用户列表
+     */
+    List<SysSocialUserDO> getSocialUserList(Long userId);
+
+    /**
+     * 绑定社交用户
+     *
+     * @param userId 用户编号
+     * @param type 社交平台的类型 {@link SysSocialTypeEnum}
+     * @param authUser 授权用户
+     */
+    void bindSocialUser(Long userId, Integer type, AuthUser authUser);
+
+    /**
+     * 取消绑定社交用户
+     *
+     * @param userId 用户编号
+     * @param type 社交平台的类型 {@link SysSocialTypeEnum}
+     * @param unionId 社交平台的 unionId
+     */
+    void unbindSocialUser(Long userId, Integer type, String unionId);
+
+}

+ 178 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/social/impl/SysSocialServiceImpl.java

@@ -0,0 +1,178 @@
+package cn.iocoder.yudao.adminserver.modules.system.service.social.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.adminserver.modules.system.dal.dataobject.social.SysSocialUserDO;
+import cn.iocoder.yudao.adminserver.modules.system.dal.mysql.social.SysSocialUserMapper;
+import cn.iocoder.yudao.adminserver.modules.system.dal.redis.social.SysSocialAuthUserRedisDAO;
+import cn.iocoder.yudao.adminserver.modules.system.enums.social.SysSocialTypeEnum;
+import cn.iocoder.yudao.adminserver.modules.system.service.social.SysSocialService;
+import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
+import com.google.common.annotations.VisibleForTesting;
+import com.xkcoding.justauth.AuthRequestFactory;
+import lombok.extern.slf4j.Slf4j;
+import me.zhyd.oauth.model.AuthCallback;
+import me.zhyd.oauth.model.AuthResponse;
+import me.zhyd.oauth.model.AuthUser;
+import me.zhyd.oauth.request.AuthRequest;
+import me.zhyd.oauth.utils.AuthStateUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+import java.util.List;
+import java.util.Objects;
+
+import static cn.iocoder.yudao.adminserver.modules.system.enums.SysErrorCodeConstants.SOCIAL_AUTH_FAILURE;
+import static cn.iocoder.yudao.adminserver.modules.system.enums.SysErrorCodeConstants.SOCIAL_UNBIND_NOT_SELF;
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
+
+/**
+ * 社交 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Valid
+@Slf4j
+public class SysSocialServiceImpl implements SysSocialService {
+
+    @Resource
+    private AuthRequestFactory authRequestFactory;
+
+    @Resource
+    private SysSocialAuthUserRedisDAO authSocialUserRedisDAO;
+
+    @Resource
+    private SysSocialUserMapper socialUserMapper;
+
+    @Override
+    public String getAuthorizeUrl(Integer type, String redirectUri) {
+        // 获得对应的 AuthRequest 实现
+        AuthRequest authRequest = authRequestFactory.get(SysSocialTypeEnum.valueOfType(type).getSource());
+        // 生成跳转地址
+        String authorizeUri = authRequest.authorize(AuthStateUtils.createState());
+        return HttpUtils.replaceUrlQuery(authorizeUri, "redirect_uri", redirectUri);
+    }
+
+    @Override
+    public AuthUser getAuthUser(Integer type, String code, String state) {
+        AuthCallback authCallback = buildAuthCallback(code, state);
+        // 从缓存中获取
+        AuthUser authUser = authSocialUserRedisDAO.get(type, authCallback);
+        if (authUser != null) {
+            return authUser;
+        }
+
+        // 请求获取
+        authUser = this.getAuthUser0(type, authCallback);
+        // 缓存。原因是 code 有且可以使用一次。在社交登录时,当未绑定 User 时,需要绑定登录,此时需要 code 使用两次
+        authSocialUserRedisDAO.set(type, authCallback, authUser);
+        return authUser;
+    }
+
+    @Override
+    public List<SysSocialUserDO> getAllSocialUserList(Integer type, String unionId) {
+        List<Integer> types = SysSocialTypeEnum.getRelationTypes(type);
+        return socialUserMapper.selectListByTypeAndUnionId(UserTypeEnum.ADMIN.getValue(), types, unionId);
+    }
+
+    @Override
+    public List<SysSocialUserDO> getSocialUserList(Long userId) {
+        return socialUserMapper.selectListByUserId(UserTypeEnum.ADMIN.getValue(), userId);
+    }
+
+    @Override
+    @Transactional
+    public void bindSocialUser(Long userId, Integer type, AuthUser authUser) {
+        // 获得 unionId 对应的 SysSocialUserDO 列表
+        String unionId = getAuthUserUnionId(authUser);
+        List<SysSocialUserDO> socialUsers = this.getAllSocialUserList(type, unionId);
+
+        // 逻辑一:如果 userId 之前绑定过该 type 的其它账号,需要进行解绑
+        this.unbindOldSocialUser(userId, type, unionId);
+
+        // 逻辑二:如果 socialUsers 指定的 userId 改变,需要进行更新
+        // 例如说,一个微信 unionId 对应了多个社交账号,结果其中有个关联了新的 userId,则其它也要跟着修改
+        // 考虑到 socialUsers 一般比较少,直接 for 循环更新即可
+        socialUsers.forEach(socialUser -> {
+            if (Objects.equals(socialUser.getUserId(), userId)) {
+                return;
+            }
+            socialUserMapper.updateById(new SysSocialUserDO().setId(socialUser.getId()).setUserId(userId));
+        });
+
+        // 逻辑三:如果 authUser 不存在于 socialUsers 中,则进行新增;否则,进行更新
+        SysSocialUserDO socialUser = CollUtil.findOneByField(socialUsers, "openid", authUser.getUuid());
+        SysSocialUserDO saveSocialUser = SysSocialUserDO.builder() // 新增和更新的通用属性
+                .token(authUser.getToken().getAccessToken()).rawTokenInfo(toJsonString(authUser.getToken()))
+                .nickname(authUser.getNickname()).avatar(authUser.getAvatar()).rawUserInfo(toJsonString(authUser.getRawUserInfo()))
+                .build();
+        if (socialUser == null) {
+            saveSocialUser.setUserId(userId).setUserType(UserTypeEnum.ADMIN.getValue())
+                    .setType(type).setOpenid(authUser.getUuid()).setUnionId(unionId);
+            socialUserMapper.insert(saveSocialUser);
+        } else {
+            saveSocialUser.setId(socialUser.getId());
+            socialUserMapper.updateById(saveSocialUser);
+        }
+    }
+
+    @Override
+    public void unbindSocialUser(Long userId, Integer type, String unionId) {
+        // 获得 unionId 对应的所有 SysSocialUserDO 社交用户
+        List<SysSocialUserDO> socialUsers = this.getAllSocialUserList(type, unionId);
+        if (CollUtil.isEmpty(socialUsers)) {
+            return;
+        }
+        // 校验,是否解绑的是非自己的
+        socialUsers.forEach(socialUser -> {
+            if (!Objects.equals(socialUser.getUserId(), userId)) {
+                throw exception(SOCIAL_UNBIND_NOT_SELF);
+            }
+        });
+
+        // 解绑
+        socialUserMapper.deleteBatchIds(CollectionUtils.convertSet(socialUsers, SysSocialUserDO::getId));
+    }
+
+    @VisibleForTesting
+    public void unbindOldSocialUser(Long userId, Integer type, String newUnionId) {
+        List<Integer> types = SysSocialTypeEnum.getRelationTypes(type);
+        List<SysSocialUserDO> oldSocialUsers = socialUserMapper.selectListByTypeAndUserId(
+                UserTypeEnum.ADMIN.getValue(), types, userId);
+        // 如果新老的 unionId 是一致的,说明无需解绑
+        if (CollUtil.isEmpty(oldSocialUsers) || Objects.equals(newUnionId, oldSocialUsers.get(0).getUnionId())) {
+            return;
+        }
+
+        // 解绑
+        socialUserMapper.deleteBatchIds(CollectionUtils.convertSet(oldSocialUsers, SysSocialUserDO::getId));
+    }
+
+    /**
+     * 请求社交平台,获得授权的用户
+     *
+     * @param type 社交平台的类型
+     * @param authCallback 授权回调
+     * @return 授权的用户
+     */
+    private AuthUser getAuthUser0(Integer type, AuthCallback authCallback) {
+        AuthRequest authRequest = authRequestFactory.get(SysSocialTypeEnum.valueOfType(type).getSource());
+        AuthResponse<?> authResponse = authRequest.login(authCallback);
+        log.info("[getAuthUser0][请求社交平台 type({}) request({}) response({})]", type, toJsonString(authCallback),
+                toJsonString(authResponse));
+        if (!authResponse.ok()) {
+            throw exception(SOCIAL_AUTH_FAILURE, authResponse.getMsg());
+        }
+        return (AuthUser) authResponse.getData();
+    }
+
+    private static AuthCallback buildAuthCallback(String code, String state) {
+        return AuthCallback.builder().code(code).state(state).build();
+    }
+
+}

+ 1 - 1
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/user/SysUserService.java

@@ -63,7 +63,7 @@ public interface SysUserService {
      * @param id         用户 id
      * @param avatarFile 头像文件
      */
-    void updateUserAvatar(Long id, InputStream avatarFile);
+    String updateUserAvatar(Long id, InputStream avatarFile);
 
     /**
      * 修改密码

+ 15 - 14
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/user/SysUserServiceImpl.java → yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/user/impl/SysUserServiceImpl.java

@@ -1,12 +1,9 @@
-package cn.iocoder.yudao.adminserver.modules.system.service.user;
+package cn.iocoder.yudao.adminserver.modules.system.service.user.impl;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.io.IoUtil;
 import cn.hutool.core.util.IdUtil;
 import cn.hutool.core.util.StrUtil;
-import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
-import cn.iocoder.yudao.framework.common.exception.ServiceException;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.adminserver.modules.infra.service.file.InfFileService;
 import cn.iocoder.yudao.adminserver.modules.system.controller.user.vo.profile.SysUserProfileUpdatePasswordReqVO;
 import cn.iocoder.yudao.adminserver.modules.system.controller.user.vo.profile.SysUserProfileUpdateReqVO;
@@ -19,6 +16,10 @@ import cn.iocoder.yudao.adminserver.modules.system.dal.mysql.user.SysUserMapper;
 import cn.iocoder.yudao.adminserver.modules.system.service.dept.SysDeptService;
 import cn.iocoder.yudao.adminserver.modules.system.service.dept.SysPostService;
 import cn.iocoder.yudao.adminserver.modules.system.service.permission.SysPermissionService;
+import cn.iocoder.yudao.adminserver.modules.system.service.user.SysUserService;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.exception.ServiceException;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import com.google.common.annotations.VisibleForTesting;
 import lombok.extern.slf4j.Slf4j;
@@ -31,9 +32,8 @@ import javax.annotation.Resource;
 import java.io.InputStream;
 import java.util.*;
 
-import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.adminserver.modules.system.enums.SysErrorCodeConstants.*;
-
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 
 /**
  * 用户 Service 实现类
@@ -105,7 +105,7 @@ public class SysUserServiceImpl implements SysUserService {
     }
 
     @Override
-    public void updateUserAvatar(Long id, InputStream avatarFile) {
+    public String updateUserAvatar(Long id, InputStream avatarFile) {
         this.checkUserExists(id);
         // 存储文件
         String avatar = fileService.createFile(IdUtil.fastUUID(), IoUtil.readBytes(avatarFile));
@@ -114,6 +114,7 @@ public class SysUserServiceImpl implements SysUserService {
         sysUserDO.setId(id);
         sysUserDO.setAvatar(avatar);
         userMapper.updateById(sysUserDO);
+        return avatar;
     }
 
     @Override
@@ -216,7 +217,7 @@ public class SysUserServiceImpl implements SysUserService {
     }
 
     @VisibleForTesting
-    void checkUserExists(Long id) {
+    public void checkUserExists(Long id) {
         if (id == null) {
             return;
         }
@@ -227,7 +228,7 @@ public class SysUserServiceImpl implements SysUserService {
     }
 
     @VisibleForTesting
-    void checkUsernameUnique(Long id, String username) {
+    public void checkUsernameUnique(Long id, String username) {
         if (StrUtil.isBlank(username)) {
             return;
         }
@@ -245,7 +246,7 @@ public class SysUserServiceImpl implements SysUserService {
     }
 
     @VisibleForTesting
-    void checkEmailUnique(Long id, String email) {
+    public void checkEmailUnique(Long id, String email) {
         if (StrUtil.isBlank(email)) {
             return;
         }
@@ -263,7 +264,7 @@ public class SysUserServiceImpl implements SysUserService {
     }
 
     @VisibleForTesting
-    void checkMobileUnique(Long id, String mobile) {
+    public void checkMobileUnique(Long id, String mobile) {
         if (StrUtil.isBlank(mobile)) {
             return;
         }
@@ -281,7 +282,7 @@ public class SysUserServiceImpl implements SysUserService {
     }
 
     @VisibleForTesting
-    void checkDeptEnable(Long deptId) {
+    public void checkDeptEnable(Long deptId) {
         if (deptId == null) { // 允许不选择
             return;
         }
@@ -295,7 +296,7 @@ public class SysUserServiceImpl implements SysUserService {
     }
 
     @VisibleForTesting
-    void checkPostEnable(Set<Long> postIds) {
+    public void checkPostEnable(Set<Long> postIds) {
         if (CollUtil.isEmpty(postIds)) { // 允许不选择
             return;
         }
@@ -322,7 +323,7 @@ public class SysUserServiceImpl implements SysUserService {
      * @param oldPassword 旧密码
      */
     @VisibleForTesting
-    void checkOldPassword(Long id, String oldPassword) {
+    public void checkOldPassword(Long id, String oldPassword) {
         SysUserDO user = userMapper.selectById(id);
         if (user == null) {
             throw exception(USER_NOT_EXISTS);

+ 2 - 2
yudao-admin-server/src/main/resources/application-dev.yaml

@@ -44,13 +44,13 @@ spring:
       datasource:
         master:
           name: ruoyi-vue-pro
-          url: jdbc:mysql://400-infra.server.iocoder.cn:3306/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT
+          url: jdbc:mysql://400-infra.server.iocoder.cn:3306/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT
           driver-class-name: com.mysql.jdbc.Driver
           username: root
           password: 3WLiVUBEwTbvAfsh
         slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改
           name: ruoyi-vue-pro
-          url: jdbc:mysql://400-infra.server.iocoder.cn:3306/${spring.datasource.dynamic.datasource.slave.name}?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT
+          url: jdbc:mysql://400-infra.server.iocoder.cn:3306/${spring.datasource.dynamic.datasource.slave.name}?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT
           driver-class-name: com.mysql.jdbc.Driver
           username: root
           password: 3WLiVUBEwTbvAfsh

+ 23 - 2
yudao-admin-server/src/main/resources/application-local.yaml

@@ -44,13 +44,13 @@ spring:
       datasource:
         master:
           name: ruoyi-vue-pro
-          url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT
+          url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT
           driver-class-name: com.mysql.jdbc.Driver
           username: root
           password: 123456
         slave: # 模拟从库,可根据自己需要修改
           name: ruoyi-vue-pro
-          url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.slave.name}?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT
+          url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.slave.name}?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT
           driver-class-name: com.mysql.jdbc.Driver
           username: root
           password: 123456
@@ -167,3 +167,24 @@ yudao:
       - ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求
       - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求
   demo: false # 关闭演示模式
+
+justauth:
+  enabled: true
+  type:
+    GITEE: # Gitee
+      client-id: ee61f0374a4c6c404a8717094caa7a410d76950e45ff60348015830c519ba5c1
+      client-secret: 7c044a5671be3b051414db0cf2cec6ad702dd298d2416ba24ceaf608e6fa26f9
+      ignore-check-redirect-uri: true
+    DINGTALK: # 钉钉
+      client-id: dingvrnreaje3yqvzhxg
+      client-secret: i8E6iZyDvZj51JIb0tYsYfVQYOks9Cq1lgryEjFRqC79P3iJcrxEwT6Qk2QvLrLI
+      ignore-check-redirect-uri: true
+    WECHAT_ENTERPRISE: # 企业微信
+      client-id: wwd411c69a39ad2e54
+      client-secret: 1wTb7hYxnpT2TUbIeHGXGo7T0odav1ic10mLdyyATOw
+      agent-id: 1000004
+      ignore-check-redirect-uri: true
+  cache:
+    type: REDIS
+    prefix: 'social_auth_state:' # 缓存前缀,目前只对 Redis 缓存生效,默认 JUSTAUTH::STATE::
+    timeout: 24h # 超时时长,目前只对 Redis 缓存生效,默认 3 分钟

+ 1 - 1
yudao-admin-server/src/test-integration/java/cn/iocoder/yudao/adminserver/modules/system/service/sms/SysSmsServiceIntegrationTest.java

@@ -41,7 +41,7 @@ public class SysSmsServiceIntegrationTest extends BaseDbAndRedisIntegrationTest
         Integer userType = UserTypeEnum.ADMIN.getValue();
         String templateCode = "test_01";
         Map<String, Object> templateParams = MapUtil.<String, Object>builder()
-                .put("operation", "登").put("code", "1234").build();
+                .put("operation", "登").put("code", "1234").build();
         // 调用
         smsService.sendSingleSms(mobile, userId, userType, templateCode, templateParams);
 

+ 0 - 13
yudao-admin-server/src/test/java/cn/iocoder/yudao/adminserver/BaseMockitoUnitTest.java

@@ -1,13 +0,0 @@
-package cn.iocoder.yudao.adminserver;
-
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.junit.jupiter.MockitoExtension;
-
-/**
- * 纯 Mockito 的单元测试
- *
- * @author 芋道源码
- */
-@ExtendWith(MockitoExtension.class)
-public class BaseMockitoUnitTest {
-}

+ 1 - 1
yudao-admin-server/src/test/java/cn/iocoder/yudao/adminserver/modules/system/service/auth/SysAuthServiceImplTest.java

@@ -236,7 +236,7 @@ public class SysAuthServiceImplTest extends BaseDbUnitTest {
         when(authentication.getPrincipal()).thenReturn(loginUser);
         // mock 获得 User 拥有的角色编号数组
         when(permissionService.getUserRoleIds(userId, singleton(CommonStatusEnum.ENABLE.getStatus()))).thenReturn(userRoleIds);
-        // mock 缓存登用户到 Redis
+        // mock 缓存登用户到 Redis
         when(userSessionService.createUserSession(loginUser, userIp, userAgent)).thenReturn(sessionId);
         // 调用, 并断言异常
         String login = authService.login(reqVO, userIp, userAgent);

+ 1 - 1
yudao-admin-server/src/test/java/cn/iocoder/yudao/adminserver/modules/system/service/auth/SysUserSessionServiceImplTest.java

@@ -41,7 +41,7 @@ import cn.iocoder.yudao.adminserver.modules.system.enums.common.SysSexEnum;
 import cn.iocoder.yudao.adminserver.modules.system.service.auth.impl.SysUserSessionServiceImpl;
 import cn.iocoder.yudao.adminserver.modules.system.service.dept.impl.SysDeptServiceImpl;
 import cn.iocoder.yudao.adminserver.modules.system.service.logger.impl.SysLoginLogServiceImpl;
-import cn.iocoder.yudao.adminserver.modules.system.service.user.SysUserServiceImpl;
+import cn.iocoder.yudao.adminserver.modules.system.service.user.impl.SysUserServiceImpl;
 import cn.iocoder.yudao.framework.test.core.util.AssertUtils;
 import cn.iocoder.yudao.framework.test.core.util.RandomUtils;
 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;

+ 1 - 1
yudao-admin-server/src/test/java/cn/iocoder/yudao/adminserver/modules/system/service/sms/SysSmsServiceTest.java

@@ -1,7 +1,6 @@
 package cn.iocoder.yudao.adminserver.modules.system.service.sms;
 
 import cn.hutool.core.map.MapUtil;
-import cn.iocoder.yudao.adminserver.BaseMockitoUnitTest;
 import cn.iocoder.yudao.framework.common.core.KeyValue;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
@@ -14,6 +13,7 @@ import cn.iocoder.yudao.adminserver.modules.system.dal.dataobject.sms.SysSmsTemp
 import cn.iocoder.yudao.adminserver.modules.system.mq.message.sms.SysSmsSendMessage;
 import cn.iocoder.yudao.adminserver.modules.system.mq.producer.sms.SysSmsProducer;
 import cn.iocoder.yudao.adminserver.modules.system.service.sms.impl.SysSmsServiceImpl;
+import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
 import org.assertj.core.util.Lists;
 import org.junit.jupiter.api.Test;
 import org.mockito.InjectMocks;

+ 171 - 0
yudao-admin-server/src/test/java/cn/iocoder/yudao/adminserver/modules/system/service/social/SysSocialServiceTest.java

@@ -0,0 +1,171 @@
+package cn.iocoder.yudao.adminserver.modules.system.service.social;
+
+import cn.iocoder.yudao.adminserver.BaseDbAndRedisUnitTest;
+import cn.iocoder.yudao.adminserver.modules.system.dal.dataobject.social.SysSocialUserDO;
+import cn.iocoder.yudao.adminserver.modules.system.dal.mysql.social.SysSocialUserMapper;
+import cn.iocoder.yudao.adminserver.modules.system.dal.redis.social.SysSocialAuthUserRedisDAO;
+import cn.iocoder.yudao.adminserver.modules.system.enums.social.SysSocialTypeEnum;
+import cn.iocoder.yudao.adminserver.modules.system.service.social.impl.SysSocialServiceImpl;
+import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
+import com.xkcoding.justauth.AuthRequestFactory;
+import me.zhyd.oauth.model.AuthUser;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.context.annotation.Import;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+import static cn.hutool.core.util.RandomUtil.randomEle;
+import static cn.hutool.core.util.RandomUtil.randomString;
+import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * {@link SysSocialServiceImpl} 的单元测试类
+ *
+ * @author 芋道源码
+ */
+@Import({SysSocialServiceImpl.class, SysSocialAuthUserRedisDAO.class})
+public class SysSocialServiceTest extends BaseDbAndRedisUnitTest {
+
+    @Resource
+    private SysSocialServiceImpl socialService;
+
+    @Resource
+    private SysSocialUserMapper socialUserMapper;
+
+    @MockBean
+    private AuthRequestFactory authRequestFactory;
+
+    /**
+     * 情况一,创建 SysSocialUserDO 的情况
+     */
+    @Test
+    public void testBindSocialUser_create() {
+        // mock 数据
+        // 准备参数
+        Long userId = randomLongId();
+        Integer type = randomEle(SysSocialTypeEnum.values()).getType();
+        AuthUser authUser = randomPojo(AuthUser.class);
+        // mock 方法
+
+        // 调用
+        socialService.bindSocialUser(userId, type, authUser);
+        // 断言
+        List<SysSocialUserDO> socialUsers = socialUserMapper.selectList("user_id", userId);
+        assertEquals(1, socialUsers.size());
+        assertBindSocialUser(socialUsers.get(0), authUser, userId, type);
+    }
+
+    /**
+     * 情况二,更新 SysSocialUserDO 的情况
+     */
+    @Test
+    public void testBindSocialUser_update() {
+        // mock 数据
+        SysSocialUserDO dbSocialUser = randomPojo(SysSocialUserDO.class, socialUserDO -> {
+            socialUserDO.setUserType(UserTypeEnum.ADMIN.getValue());
+            socialUserDO.setType(randomEle(SysSocialTypeEnum.values()).getType());
+        });
+        socialUserMapper.insert(dbSocialUser);
+        // 准备参数
+        Long userId = dbSocialUser.getUserId();
+        Integer type = dbSocialUser.getType();
+        AuthUser authUser = randomPojo(AuthUser.class);
+        // mock 方法
+
+        // 调用
+        socialService.bindSocialUser(userId, type, authUser);
+        // 断言
+        List<SysSocialUserDO> socialUsers = socialUserMapper.selectList("user_id", userId);
+        assertEquals(1, socialUsers.size());
+        assertBindSocialUser(socialUsers.get(0), authUser, userId, type);
+    }
+
+    /**
+     * 情况一和二都存在的,逻辑二的场景
+     */
+    @Test
+    public void testBindSocialUser_userId() {
+        // mock 数据
+        SysSocialUserDO dbSocialUser = randomPojo(SysSocialUserDO.class, socialUserDO -> {
+            socialUserDO.setUserType(UserTypeEnum.ADMIN.getValue());
+            socialUserDO.setType(randomEle(SysSocialTypeEnum.values()).getType());
+        });
+        socialUserMapper.insert(dbSocialUser);
+        // 准备参数
+        Long userId = randomLongId();
+        Integer type = dbSocialUser.getType();
+        AuthUser authUser = randomPojo(AuthUser.class);
+        // mock 方法
+
+        // 调用
+        socialService.bindSocialUser(userId, type, authUser);
+        // 断言
+        List<SysSocialUserDO> socialUsers = socialUserMapper.selectList("user_id", userId);
+        assertEquals(1, socialUsers.size());
+    }
+
+    private void assertBindSocialUser(SysSocialUserDO socialUser, AuthUser authUser, Long userId,
+                                             Integer type) {
+        assertEquals(authUser.getToken().getAccessToken(), socialUser.getToken());
+        assertEquals(toJsonString(authUser.getToken()), socialUser.getRawTokenInfo());
+        assertEquals(authUser.getNickname(), socialUser.getNickname());
+        assertEquals(authUser.getAvatar(), socialUser.getAvatar());
+        assertEquals(toJsonString(authUser.getRawUserInfo()), socialUser.getRawUserInfo());
+        assertEquals(userId, socialUser.getUserId());
+        assertEquals(UserTypeEnum.ADMIN.getValue(), socialUser.getUserType());
+        assertEquals(type, socialUser.getType());
+        assertEquals(authUser.getUuid(), socialUser.getOpenid());
+        assertEquals(socialService.getAuthUserUnionId(authUser), socialUser.getUnionId());
+    }
+
+    /**
+     * 情况一,如果新老的 unionId 是一致的,无需解绑
+     */
+    @Test
+    public void testUnbindOldSocialUser_no() {
+        // mock 数据
+        SysSocialUserDO oldSocialUser = randomPojo(SysSocialUserDO.class, socialUserDO -> {
+            socialUserDO.setUserType(UserTypeEnum.ADMIN.getValue());
+            socialUserDO.setType(randomEle(SysSocialTypeEnum.values()).getType());
+        });
+        socialUserMapper.insert(oldSocialUser);
+        // 准备参数
+        Long userId = oldSocialUser.getUserId();
+        Integer type = oldSocialUser.getType();
+        String newUnionId = oldSocialUser.getUnionId();
+
+        // 调用
+        socialService.unbindOldSocialUser(userId, type, newUnionId);
+        // 断言
+        assertEquals(1L, socialUserMapper.selectCount(null).longValue());
+    }
+
+
+    /**
+     * 情况二,如果新老的 unionId 不一致的,需解绑
+     */
+    @Test
+    public void testUnbindOldSocialUser_yes() {
+        // mock 数据
+        SysSocialUserDO oldSocialUser = randomPojo(SysSocialUserDO.class, socialUserDO -> {
+            socialUserDO.setUserType(UserTypeEnum.ADMIN.getValue());
+            socialUserDO.setType(randomEle(SysSocialTypeEnum.values()).getType());
+        });
+        socialUserMapper.insert(oldSocialUser);
+        // 准备参数
+        Long userId = oldSocialUser.getUserId();
+        Integer type = oldSocialUser.getType();
+        String newUnionId = randomString(10);
+
+        // 调用
+        socialService.unbindOldSocialUser(userId, type, newUnionId);
+        // 断言
+        assertEquals(0L, socialUserMapper.selectCount(null).longValue());
+    }
+
+}

+ 1 - 0
yudao-admin-server/src/test/java/cn/iocoder/yudao/adminserver/modules/system/service/user/SysUserServiceImplTest.java

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.adminserver.modules.system.service.user;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.RandomUtil;
 import cn.iocoder.yudao.adminserver.BaseDbUnitTest;
+import cn.iocoder.yudao.adminserver.modules.system.service.user.impl.SysUserServiceImpl;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.adminserver.modules.infra.service.file.InfFileService;

+ 1 - 0
yudao-admin-server/src/test/resources/sql/clean.sql

@@ -23,3 +23,4 @@ DELETE FROM "sys_sms_channel";
 DELETE FROM "sys_sms_template";
 DELETE FROM "sys_sms_log";
 DELETE FROM "sys_error_code";
+DELETE FROM "sys_social_user";

+ 20 - 0
yudao-admin-server/src/test/resources/sql/create_tables.sql

@@ -426,3 +426,23 @@ CREATE TABLE IF NOT EXISTS "sys_error_code" (
   "deleted" bit NOT NULL DEFAULT FALSE,
   PRIMARY KEY ("id")
 ) COMMENT '错误码表';
+
+CREATE TABLE IF NOT EXISTS "sys_social_user" (
+   "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+   "user_id" bigint NOT NULL,
+   "user_type" tinyint NOT NULL DEFAULT '0',
+   "type" tinyint NOT NULL,
+   "openid" varchar(32) NOT NULL,
+   "token" varchar(256) DEFAULT NULL,
+   "union_id" varchar(32) NOT NULL,
+   "raw_token_info" varchar(1024) NOT NULL,
+   "nickname" varchar(32) NOT NULL,
+   "avatar" varchar(255) DEFAULT NULL,
+   "raw_user_info" varchar(1024) NOT NULL,
+   "creator" varchar(64) DEFAULT '',
+   "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+   "updater" varchar(64) DEFAULT '',
+   "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+   "deleted" bit NOT NULL DEFAULT FALSE,
+   PRIMARY KEY ("id")
+) COMMENT '社交用户';

+ 1 - 1
yudao-admin-ui/.env.production

@@ -4,7 +4,7 @@ ENV = 'production'
 # 芋道管理系统/生产环境
 VUE_APP_BASE_API = '/prod-api'
 # 根据服务器或域名修改
-PUBLIC_PATH = 'http://you_ip.cn/yudao-admin/'
+PUBLIC_PATH = 'http://my-pi.com:8888/yudao-admin/'
 # 二级部署路径
 VUE_APP_APP_NAME ='yudao-admin'
 

+ 61 - 0
yudao-admin-ui/src/api/login.js

@@ -38,3 +38,64 @@ export function getCodeImg() {
     method: 'get'
   })
 }
+
+// 社交授权的跳转
+export function socialAuthRedirect(type, redirectUri) {
+  return request({
+    url: '/social-auth-redirect?type=' + type + '&redirectUri=' + redirectUri,
+    method: 'get'
+  })
+}
+
+// 社交登录,使用 code 授权码
+export function socialLogin(type, code, state) {
+  return request({
+    url: '/social-login',
+    method: 'post',
+    data: {
+      type,
+      code,
+      state
+    }
+  })
+}
+
+// 社交登录,使用 code 授权码 + + 账号密码
+export function socialLogin2(type, code, state, username, password) {
+  return request({
+    url: '/social-login2',
+    method: 'post',
+    data: {
+      type,
+      code,
+      state,
+      username,
+      password
+    }
+  })
+}
+
+// 社交绑定,使用 code 授权码
+export function socialBind(type, code, state) {
+  return request({
+    url: '/social-bind',
+    method: 'post',
+    data: {
+      type,
+      code,
+      state,
+    }
+  })
+}
+
+// 取消社交绑定
+export function socialUnbind(type, unionId) {
+  return request({
+    url: '/social-unbind',
+    method: 'delete',
+    data: {
+      type,
+      unionId
+    }
+  })
+}

+ 1 - 1
yudao-admin-ui/src/api/system/user.js

@@ -113,7 +113,7 @@ export function updateUserPwd(oldPassword, newPassword) {
 // 用户头像上传
 export function uploadAvatar(data) {
   return request({
-    url: '/system/user/profile/avatar',
+    url: '/system/user/profile/update-avatar',
     method: 'put',
     data: data
   })

+ 1 - 1
yudao-admin-ui/src/permission.js

@@ -7,7 +7,7 @@ import { getToken } from '@/utils/auth'
 
 NProgress.configure({ showSpinner: false })
 
-const whiteList = ['/login', '/auth-redirect', '/bind', '/register']
+const whiteList = ['/login', '/social-login',  '/auth-redirect', '/bind', '/register', '/oauthLogin/gitee']
 
 router.beforeEach((to, from, next) => {
   NProgress.start()

+ 5 - 0
yudao-admin-ui/src/router/index.js

@@ -43,6 +43,11 @@ export const constantRoutes = [
     component: (resolve) => require(['@/views/login'], resolve),
     hidden: true
   },
+  {
+    path: '/social-login',
+    component: (resolve) => require(['@/views/socialLogin'], resolve),
+    hidden: true
+  },
   {
     path: '/404',
     component: (resolve) => require(['@/views/error/404'], resolve),

+ 37 - 1
yudao-admin-ui/src/store/modules/user.js

@@ -1,4 +1,4 @@
-import { login, logout, getInfo } from '@/api/login'
+import {login, logout, getInfo, socialLogin, socialLogin2} from '@/api/login'
 import { getToken, setToken, removeToken } from '@/utils/auth'
 
 const user = {
@@ -47,6 +47,42 @@ const user = {
       })
     },
 
+    // 社交登录
+    SocialLogin({ commit }, userInfo) {
+      const code = userInfo.code
+      const state = userInfo.state
+      const type = userInfo.type
+      return new Promise((resolve, reject) => {
+        socialLogin(type, code, state).then(res => {
+          res = res.data;
+          setToken(res.token)
+          commit('SET_TOKEN', res.token)
+          resolve()
+        }).catch(error => {
+          reject(error)
+        })
+      })
+    },
+
+    // 社交登录
+    SocialLogin2({ commit }, userInfo) {
+      const code = userInfo.code
+      const state = userInfo.state
+      const type = userInfo.type
+      const username = userInfo.username.trim()
+      const password = userInfo.password
+      return new Promise((resolve, reject) => {
+        socialLogin2(type, code, state, username, password).then(res => {
+          res = res.data;
+          setToken(res.token)
+          commit('SET_TOKEN', res.token)
+          resolve()
+        }).catch(error => {
+          reject(error)
+        })
+      })
+    },
+
     // 获取用户信息
     GetInfo({ commit, state }) {
       return new Promise((resolve, reject) => {

+ 24 - 0
yudao-admin-ui/src/utils/constants.js

@@ -66,3 +66,27 @@ export const InfApiErrorLogProcessStatusEnum = {
   DONE: 1, // 已处理
   IGNORE: 2, // 已忽略
 }
+
+/**
+ * 用户的社交平台的类型枚举
+ */
+export const SysUserSocialTypeEnum = {
+  // GITEE: {
+  //   title: "码云",
+  //   type: 10,
+  //   source: "gitee",
+  //   img: "https://cdn.jsdelivr.net/gh/justauth/justauth-oauth-logo@1.11/gitee.png",
+  // },
+  DINGTALK: {
+    title: "钉钉",
+    type: 20,
+    source: "dingtalk",
+    img: "https://cdn.jsdelivr.net/gh/justauth/justauth-oauth-logo@1.11/dingtalk.png",
+  },
+  WECHAT_ENTERPRISE: {
+    title: "企业微信",
+    type: 30,
+    source: "wechat_enterprise",
+    img: "https://cdn.jsdelivr.net/gh/justauth/justauth-oauth-logo@1.11/wechat_enterprise.png",
+  }
+}

+ 1 - 0
yudao-admin-ui/src/utils/dict.js

@@ -15,6 +15,7 @@ export const DICT_TYPE = {
   SYS_USER_SEX: 'sys_user_sex',
   SYS_NOTICE_TYPE: 'sys_notice_type',
   SYS_OPERATE_TYPE: 'sys_operate_type',
+  SYS_LOGIN_TYPE: 'sys_login_type',
   SYS_LOGIN_RESULT: 'sys_login_result',
   SYS_CONFIG_TYPE: 'sys_config_type',
   SYS_SMS_CHANNEL_CODE: 'sys_sms_channel_code',

+ 1 - 1
yudao-admin-ui/src/views/index_old.vue

@@ -419,7 +419,7 @@
                 <li>修复表格时间为空出现的异常</li>
                 <li>添加Jackson日期反序列化时区配置</li>
                 <li>调整根据用户权限加载菜单数据树形结构</li>
-                <li>调整成功登不恢复按钮,防止多次点击</li>
+                <li>调整成功登不恢复按钮,防止多次点击</li>
                 <li>修改用户个人资料同步缓存信息</li>
                 <li>修复页面同时出现el-upload和Editor不显示处理</li>
                 <li>修复在角色管理页修改菜单权限偶尔未选中问题</li>

+ 58 - 32
yudao-admin-ui/src/views/login.vue

@@ -8,24 +8,12 @@
         </el-input>
       </el-form-item>
       <el-form-item prop="password">
-        <el-input
-          v-model="loginForm.password"
-          type="password"
-          auto-complete="off"
-          placeholder="密码"
-          @keyup.enter.native="handleLogin"
-        >
+        <el-input v-model="loginForm.password" type="password" auto-complete="off" placeholder="密码" @keyup.enter.native="handleLogin">
           <svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon" />
         </el-input>
       </el-form-item>
       <el-form-item prop="code">
-        <el-input
-          v-model="loginForm.code"
-          auto-complete="off"
-          placeholder="验证码"
-          style="width: 63%"
-          @keyup.enter.native="handleLogin"
-        >
+        <el-input v-model="loginForm.code" auto-complete="off" placeholder="验证码" style="width: 63%" @keyup.enter.native="handleLogin">
           <svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon" />
         </el-input>
         <div class="login-code">
@@ -34,17 +22,20 @@
       </el-form-item>
       <el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox>
       <el-form-item style="width:100%;">
-        <el-button
-          :loading="loading"
-          size="medium"
-          type="primary"
-          style="width:100%;"
-          @click.native.prevent="handleLogin"
-        >
+        <el-button :loading="loading" size="medium" type="primary" style="width:100%;" @click.native.prevent="handleLogin">
           <span v-if="!loading">登 录</span>
           <span v-else>登 录 中...</span>
         </el-button>
       </el-form-item>
+
+      <el-form-item style="width:100%;">
+          <div class="oauth-login" style="display:flex">
+            <div class="oauth-login-item" v-for="item in SysUserSocialTypeEnum" :key="item.type" @click="doSocialLogin(item)">
+              <img :src="item.img" height="25px" width="25px" alt="登录" >
+              <span>{{item.title}}</span>
+            </div>
+        </div>
+      </el-form-item>
     </el-form>
     <!--  底部  -->
     <div class="el-login-footer">
@@ -54,16 +45,16 @@
 </template>
 
 <script>
-import { getCodeImg } from "@/api/login";
+import { getCodeImg,socialAuthRedirect } from "@/api/login";
 import Cookies from "js-cookie";
 import { encrypt, decrypt } from '@/utils/jsencrypt'
+import {InfApiErrorLogProcessStatusEnum, SysUserSocialTypeEnum} from "@/utils/constants";
 
 export default {
   name: "Login",
   data() {
     return {
       codeUrl: "",
-      cookiePassword: "",
       loginForm: {
         username: "admin",
         password: "admin123",
@@ -81,18 +72,22 @@ export default {
         code: [{ required: true, trigger: "change", message: "验证码不能为空" }]
       },
       loading: false,
-      redirect: undefined
+      redirect: undefined,
+      // 枚举
+      SysUserSocialTypeEnum: SysUserSocialTypeEnum,
     };
   },
-  watch: {
-    $route: {
-      handler: function(route) {
-        this.redirect = route.query && route.query.redirect;
-      },
-      immediate: true
-    }
-  },
+  // watch: {
+  //   $route: {
+  //     handler: function(route) {
+  //       this.redirect = route.query && route.query.redirect;
+  //     },
+  //     immediate: true
+  //   }
+  // },
   created() {
+    // 重定向地址
+    this.redirect = this.$route.query.redirect;
     this.getCode();
     this.getCookie();
   },
@@ -135,6 +130,20 @@ export default {
           });
         }
       });
+    },
+    doSocialLogin(socialTypeEnum) {
+      // console.log("开始Oauth登录...%o", socialTypeEnum.code);
+      // 设置登录中
+      this.loading = true;
+      // 计算 redirectUri
+      const redirectUri = location.origin + '/social-login?type=' + socialTypeEnum.type + '&redirect=' + (this.redirect || "/"); // 重定向不能丢
+      // const redirectUri = 'http://127.0.0.1:48080/api/gitee/callback';
+      // const redirectUri = 'http://127.0.0.1:48080/api/dingtalk/callback';
+      // 进行跳转
+      socialAuthRedirect(socialTypeEnum.type, encodeURIComponent(redirectUri)).then((res) => {
+        // console.log(res.url);
+        window.location.href = res.data;
+      });
     }
   }
 };
@@ -201,4 +210,21 @@ export default {
 .login-code-img {
   height: 38px;
 }
+.oauth-login {
+  display: flex;
+  cursor:pointer;
+}
+.oauth-login-item {
+  display: flex;
+  align-items: center;
+  margin-right: 10px;
+}
+.oauth-login-item img {
+  height: 25px;
+  width: 25px;
+}
+.oauth-login-item span:hover {
+  text-decoration: underline red;
+  color: red;
+}
 </style>

+ 187 - 0
yudao-admin-ui/src/views/socialLogin.vue

@@ -0,0 +1,187 @@
+<template>
+  <div class="login">
+    <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
+      <h3 class="title">绑定账号</h3>
+      <el-form-item prop="username">
+        <el-input v-model="loginForm.username" type="text" auto-complete="off" placeholder="账号">
+          <svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" />
+        </el-input>
+      </el-form-item>
+      <el-form-item prop="password">
+        <el-input v-model="loginForm.password" type="password" auto-complete="off" placeholder="密码" @keyup.enter.native="handleLogin">
+          <svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon" />
+        </el-input>
+      </el-form-item>
+      <el-form-item style="width:100%;">
+        <el-button :loading="loading" size="medium" type="primary" style="width:100%;" @click.native.prevent="handleLogin">
+          <span v-if="!loading">提 交</span>
+          <span v-else>提 交 中...</span>
+        </el-button>
+      </el-form-item>
+
+    </el-form>
+    <!--  底部  -->
+    <div class="el-login-footer">
+      <span>Copyright © 2020-2021 iocoder.cn All Rights Reserved.</span>
+    </div>
+  </div>
+</template>
+
+<script>
+import { socialLogin } from "@/api/login";
+import Cookies from "js-cookie";
+import { encrypt, decrypt } from '@/utils/jsencrypt'
+
+export default {
+  name: "ThirdLogin",
+  data() {
+    return {
+      loginForm: {
+        username: "admin",
+        password: "admin123",
+        rememberMe: false, // TODO 芋艿:后面看情况,去掉这块
+      },
+      loginRules: {
+        username: [
+          { required: true, trigger: "blur", message: "用户名不能为空" }
+        ],
+        password: [
+          { required: true, trigger: "blur", message: "密码不能为空" }
+        ],
+      },
+      loading: false,
+      redirect: undefined,
+      // 社交登录相关
+      type: undefined,
+      code: undefined,
+      state: undefined,
+    };
+  },
+  // watch: {
+  //   $route: {
+  //     handler: function(route) {
+  //       this.redirect = route.query && route.query.redirect;
+  //     },
+  //     immediate: true
+  //   }
+  // },
+  created() {
+    this.getCookie();
+    // 重定向地址
+    this.redirect = this.$route.query.redirect;
+    // 社交登录相关
+    this.type = this.$route.query.type;
+    this.code = this.$route.query.code;
+    this.state = this.$route.query.state;
+    this.$store.dispatch("SocialLogin", {
+      code: this.code,
+      state: this.state,
+      type: this.type
+    }).then(() => {
+      this.$router.push({ path: this.redirect || "/" }).catch(()=>{});
+    }).catch(() => {
+      this.loading = false;
+    });
+  },
+  methods: {
+    getCookie() {
+      const username = Cookies.get("username");
+      const password = Cookies.get("password");
+      const rememberMe = Cookies.get('rememberMe')
+      this.loginForm = {
+        username: username === undefined ? this.loginForm.username : username,
+        password: password === undefined ? this.loginForm.password : decrypt(password),
+        rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
+      };
+    },
+    handleLogin() {
+      this.$refs.loginForm.validate(valid => {
+        if (valid) {
+          this.loading = true;
+          if (this.loginForm.rememberMe) {
+            Cookies.set("username", this.loginForm.username, { expires: 30 });
+            Cookies.set("password", encrypt(this.loginForm.password), { expires: 30 });
+          } else {
+            Cookies.remove("username");
+            Cookies.remove("password");
+          }
+          this.$store.dispatch("SocialLogin2", {
+            code: this.code,
+            state: this.state,
+            type: this.type,
+            username: this.loginForm.username,
+            password: this.loginForm.password
+          }).then(() => {
+            this.$router.push({ path: this.redirect || "/" }).catch(()=>{});
+          }).catch(() => {
+            this.loading = false;
+          });
+        }
+      });
+    }
+  }
+};
+</script>
+
+<style rel="stylesheet/scss" lang="scss">
+.login {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 100%;
+  background-image: url("http://static.yudao.iocoder.cn/login-background.jpg");
+  background-size: cover;
+}
+.title {
+  margin: 0px auto 30px auto;
+  text-align: center;
+  color: #707070;
+}
+
+.login-form {
+  border-radius: 6px;
+  background: #ffffff;
+  width: 400px;
+  padding: 25px 25px 5px 25px;
+  .el-input {
+    height: 38px;
+    input {
+      height: 38px;
+    }
+  }
+  .input-icon {
+    height: 39px;
+    width: 14px;
+    margin-left: 2px;
+  }
+}
+.login-tip {
+  font-size: 13px;
+  text-align: center;
+  color: #bfbfbf;
+}
+.login-code {
+  width: 33%;
+  height: 38px;
+  float: right;
+  img {
+    cursor: pointer;
+    vertical-align: middle;
+  }
+}
+.el-login-footer {
+  height: 40px;
+  line-height: 40px;
+  position: fixed;
+  bottom: 0;
+  width: 100%;
+  text-align: center;
+  color: #fff;
+  font-family: Arial;
+  font-size: 12px;
+  letter-spacing: 1px;
+}
+.login-code-img {
+  height: 38px;
+}
+</style>

+ 2 - 2
yudao-admin-ui/src/views/system/loginlog/index.vue

@@ -37,7 +37,7 @@
       <el-table-column label="访问编号" align="center" prop="id" />
       <el-table-column label="日志类型" align="center" prop="logType">
         <template slot-scope="scope">
-          <span>{{ scope.row.logType === 100 ? '登录' : '退出' }}</span>
+          <span>{{ getDictDataLabel(DICT_TYPE.SYS_LOGIN_TYPE, scope.row.logType) }}</span>
         </template>
       </el-table-column>
       <el-table-column label="用户名称" align="center" prop="username" />
@@ -132,7 +132,7 @@ export default {
         }).then(function() {
           return exportLoginLog(queryParams);
         }).then(response => {
-          this.downloadExcel(response, '登日志.xls');
+          this.downloadExcel(response, '登日志.xls');
         })
     }
   }

+ 6 - 27
yudao-admin-ui/src/views/system/session/index.vue

@@ -2,22 +2,10 @@
   <div class="app-container">
     <el-form :model="queryParams" ref="queryForm" :inline="true" label-width="68px">
       <el-form-item label="登录地址" prop="userIp">
-        <el-input
-          v-model="queryParams.userIp"
-          placeholder="请输入登录地址"
-          clearable
-          size="small"
-          @keyup.enter.native="handleQuery"
-        />
+        <el-input v-model="queryParams.userIp" placeholder="请输入登录地址" clearable size="small" @keyup.enter.native="handleQuery"/>
       </el-form-item>
       <el-form-item label="用户名称" prop="username">
-        <el-input
-          v-model="queryParams.username"
-          placeholder="请输入用户名称"
-          clearable
-          size="small"
-          @keyup.enter.native="handleQuery"
-        />
+        <el-input v-model="queryParams.username" placeholder="请输入用户名称" clearable size="small" @keyup.enter.native="handleQuery"/>
       </el-form-item>
       <el-form-item>
         <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
@@ -25,15 +13,11 @@
       </el-form-item>
 
     </el-form>
-    <el-table
-      v-loading="loading"
-      :data="list"
-      style="width: 100%;"
-    >
+    <el-table v-loading="loading" :data="list" style="width: 100%;">
       <el-table-column label="会话编号" align="center" prop="id" width="300" />
       <el-table-column label="登录名称" align="center" prop="username" width="100" />
       <el-table-column label="部门名称" align="center" prop="deptName" width="100" />
-      <el-table-column label="登地址" align="center" prop="userIp" width="100" />
+      <el-table-column label="登录地址" align="center" prop="userIp" width="100" />
       <el-table-column label="userAgent" align="center" prop="userAgent" :show-overflow-tooltip="true" />
       <el-table-column label="登录时间" align="center" prop="createTime" width="180">
         <template slot-scope="scope">
@@ -42,13 +26,8 @@
       </el-table-column>
       <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
         <template slot-scope="scope">
-          <el-button
-            size="mini"
-            type="text"
-            icon="el-icon-delete"
-            @click="handleForceLogout(scope.row)"
-            v-hasPermi="['system:user-session:delete']"
-          >强退</el-button>
+          <el-button size="mini" type="text" icon="el-icon-delete" @click="handleForceLogout(scope.row)"
+            v-hasPermi="['system:user-session:delete']">强退</el-button>
         </template>
       </el-table-column>
     </el-table>

+ 9 - 2
yudao-admin-ui/src/views/system/user/profile/index.vue

@@ -33,7 +33,7 @@
               </li>
               <li class="list-group-item">
                 <svg-icon icon-class="peoples" />所属角色
-                <div class="pull-right">{{ user.roles.map(post => post.name).join(',') }}</div>
+                <div class="pull-right" v-if="user.roles">{{ user.roles.map(role => role.name).join(',') }}</div>
               </li>
               <li class="list-group-item">
                 <svg-icon icon-class="date" />创建日期
@@ -55,6 +55,9 @@
             <el-tab-pane label="修改密码" name="resetPwd">
               <resetPwd :user="user" />
             </el-tab-pane>
+            <el-tab-pane label="社交信息" name="userSocial">
+              <userSocial :user="user" :getUser="getUser" :setActiveTab="setActiveTab" />
+            </el-tab-pane>
           </el-tabs>
         </el-card>
       </el-col>
@@ -66,11 +69,12 @@
 import userAvatar from "./userAvatar";
 import userInfo from "./userInfo";
 import resetPwd from "./resetPwd";
+import userSocial from "./userSocial";
 import { getUserProfile } from "@/api/system/user";
 
 export default {
   name: "Profile",
-  components: { userAvatar, userInfo, resetPwd },
+  components: { userAvatar, userInfo, resetPwd, userSocial },
   data() {
     return {
       user: {},
@@ -87,6 +91,9 @@ export default {
       getUserProfile().then(response => {
         this.user = response.data;
       });
+    },
+    setActiveTab(activeTab) {
+      this.activeTab = activeTab
     }
   }
 };

+ 5 - 5
yudao-admin-ui/src/views/system/user/profile/userAvatar.vue

@@ -123,11 +123,11 @@ export default {
     uploadImg() {
       this.$refs.cropper.getCropBlob(data => {
         let formData = new FormData();
-        formData.append("avatarfile", data);
-        uploadAvatar(formData).then(response => {
+        formData.append("avatarFile", data);
+        uploadAvatar(formData).then(resp => {
           this.open = false;
-          this.options.img = process.env.VUE_APP_BASE_API + response.imgUrl;
-          store.commit('SET_AVATAR', this.options.img);
+          // this.options.img = process.env.VUE_APP_BASE_API + response.imgUrl;
+          store.commit('SET_AVATAR', resp.data);
           this.msgSuccess("修改成功");
           this.visible = false;
         });
@@ -164,4 +164,4 @@ export default {
   line-height: 110px;
   border-radius: 50%;
 }
-</style>
+</style>

+ 100 - 0
yudao-admin-ui/src/views/system/user/profile/userSocial.vue

@@ -0,0 +1,100 @@
+<template>
+  <el-table :data="socialUsers" :show-header="false">
+    <el-table-column label="社交平台" align="left" width="120">
+      <template slot-scope="scope">
+        <img style="height:20px;vertical-align: middle;" :src="scope.row.img" /> {{ scope.row.title }}
+      </template>
+    </el-table-column>
+    <el-table-column label="操作" align="left" >
+      <template slot-scope="scope">
+        <div v-if="scope.row.unionId">
+          已绑定
+          <el-button size="large" type="text" @click="unbind(scope.row)">(解绑)</el-button>
+        </div>
+        <div v-else>
+          未绑定
+          <el-button size="large" type="text" @click="bind(scope.row)">(绑定)</el-button>
+        </div>
+      </template>
+    </el-table-column>
+  </el-table>
+</template>
+
+<script>
+
+import {SysUserSocialTypeEnum} from "@/utils/constants";
+import {socialAuthRedirect, socialBind, socialUnbind} from "@/api/login";
+
+export default {
+  props: {
+    user: {
+      type: Object
+    },
+    getUser: { // 刷新用户
+      type: Function
+    },
+    setActiveTab: { // 设置激活的
+      type: Function
+    }
+  },
+  data() {
+    return {
+    };
+  },
+  computed: {
+    socialUsers (){
+      const socialUsers = [];
+      for (const i in SysUserSocialTypeEnum) {
+        const socialUser = {...SysUserSocialTypeEnum[i]};
+        socialUsers.push(socialUser);
+        if (this.user.socialUsers) {
+          for (const j in this.user.socialUsers) {
+            if (socialUser.type === this.user.socialUsers[j].type) {
+              socialUser.unionId = this.user.socialUsers[j].unionId;
+              break;
+            }
+          }
+        }
+      }
+      return socialUsers;
+    }
+  },
+  created() {
+    // 社交绑定
+    const type = this.$route.query.type;
+    const code = this.$route.query.code;
+    const state = this.$route.query.state;
+    if (!code) {
+      return;
+    }
+    socialBind(type, code, state).then(resp => {
+      this.msgSuccess("绑定成功");
+      this.$router.replace('/user/profile');
+      // 调用父组件, 刷新
+      this.getUser();
+      this.setActiveTab('userSocial');
+    });
+  },
+  methods: {
+    bind(socialUser) {
+      // 计算 redirectUri
+      const redirectUri = location.origin + '/user/profile?type=' + socialUser.type;
+      // 进行跳转
+      socialAuthRedirect(socialUser.type, encodeURIComponent(redirectUri)).then((res) => {
+        // console.log(res.url);
+        window.location.href = res.data;
+      });
+    },
+    unbind(socialUser) {
+      socialUnbind(socialUser.type, socialUser.unionId).then(resp => {
+        this.msgSuccess("解绑成功");
+        socialUser.unionId = undefined;
+      });
+    },
+    close() {
+      this.$store.dispatch("tagsView/delView", this.$route);
+      this.$router.push({ path: "/index" });
+    }
+  }
+};
+</script>

+ 1 - 1
yudao-dependencies/pom.xml

@@ -41,7 +41,7 @@
         <podam.version>7.2.6.RELEASE</podam.version>
         <jedis-mock.version>0.1.16</jedis-mock.version>
         <!-- 工具类相关 -->
-        <lombok.version>1.18.12</lombok.version>
+        <lombok.version>1.18.20</lombok.version>
         <mapstruct.version>1.4.1.Final</mapstruct.version>
         <hutool.version>5.6.1</hutool.version>
         <easyexcel.verion>2.2.7</easyexcel.verion>

+ 30 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.framework.common.util.http;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.map.TableMap;
+import cn.hutool.core.net.url.UrlBuilder;
+import cn.hutool.core.util.ReferenceUtil;
+import cn.hutool.core.util.ReflectUtil;
+
+import java.nio.charset.Charset;
+
+/**
+ * HTTP 工具类
+ *
+ * @author 芋道源码
+ */
+public class HttpUtils {
+
+    @SuppressWarnings("unchecked")
+    public static String replaceUrlQuery(String url, String key, String value) {
+        UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset());
+        // 先移除
+        TableMap<CharSequence, CharSequence> query = (TableMap<CharSequence, CharSequence>)
+                ReflectUtil.getFieldValue(builder.getQuery(), "query");
+        query.remove(key);
+        // 后添加
+        builder.addQuery(key, value);
+        return builder.build();
+    }
+
+}

+ 5 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java

@@ -5,6 +5,7 @@ import cn.hutool.core.util.StrUtil;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -19,6 +20,10 @@ public class JsonUtils {
 
     private static ObjectMapper objectMapper = new ObjectMapper();
 
+    static {
+        objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
+    }
+
     /**
      * 初始化 objectMapper 属性
      * <p>

+ 30 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/validation/ValidationUtils.java

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.framework.common.util.validation;
+
+import cn.hutool.core.util.StrUtil;
+import org.springframework.util.StringUtils;
+
+import java.util.regex.Pattern;
+
+/**
+ * 校验工具类
+ *
+ * @author 芋道源码
+ */
+public class ValidationUtils {
+
+    private static Pattern PATTERN_URL = Pattern.compile("^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]");
+
+    public static boolean isMobile(String mobile) {
+        if (StrUtil.length(mobile) != 11) {
+            return false;
+        }
+        // TODO 芋艿,后面完善手机校验
+        return true;
+    }
+
+    public static boolean isURL(String url) {
+        return StringUtils.hasText(url)
+                && PATTERN_URL.matcher(url).matches();
+    }
+
+}

+ 28 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/Mobile.java

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.framework.common.validation;
+
+import javax.validation.Constraint;
+import javax.validation.Payload;
+import java.lang.annotation.*;
+
+@Target({
+        ElementType.METHOD,
+        ElementType.FIELD,
+        ElementType.ANNOTATION_TYPE,
+        ElementType.CONSTRUCTOR,
+        ElementType.PARAMETER,
+        ElementType.TYPE_USE
+})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Constraint(
+        validatedBy = MobileValidator.class
+)
+public @interface Mobile {
+
+    String message() default "手机号格式不正确";
+
+    Class<?>[] groups() default {};
+
+    Class<? extends Payload>[] payload() default {};
+
+}

+ 25 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/validation/MobileValidator.java

@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.framework.common.validation;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+
+public class MobileValidator implements ConstraintValidator<Mobile, String> {
+
+    @Override
+    public void initialize(Mobile annotation) {
+    }
+
+    @Override
+    public boolean isValid(String value, ConstraintValidatorContext context) {
+        // 如果手机号为空,默认不校验,即校验通过
+        if (StrUtil.isEmpty(value)) {
+            return true;
+        }
+        // 校验手机
+        return ValidationUtils.isMobile(value);
+    }
+
+}

+ 4 - 0
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java

@@ -36,4 +36,8 @@ public interface BaseMapperX<T> extends BaseMapper<T> {
         return selectList(new QueryWrapper<>());
     }
 
+    default List<T> selectList(String field, Object value) {
+        return selectList(new QueryWrapper<T>().eq(field, value));
+    }
+
 }

+ 4 - 4
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoSecurityAutoConfiguration.java

@@ -1,7 +1,7 @@
 package cn.iocoder.yudao.framework.security.config;
 
 import cn.iocoder.yudao.framework.security.core.aop.PreAuthenticatedAspect;
-import cn.iocoder.yudao.framework.security.core.filter.JwtAuthenticationTokenFilter;
+import cn.iocoder.yudao.framework.security.core.filter.JWTAuthenticationTokenFilter;
 import cn.iocoder.yudao.framework.security.core.handler.AccessDeniedHandlerImpl;
 import cn.iocoder.yudao.framework.security.core.handler.AuthenticationEntryPointImpl;
 import cn.iocoder.yudao.framework.security.core.handler.LogoutSuccessHandlerImpl;
@@ -34,7 +34,7 @@ public class YudaoSecurityAutoConfiguration {
     private SecurityProperties securityProperties;
 
     /**
-     * 处理用户未登拦截的切面的 Bean
+     * 处理用户未登拦截的切面的 Bean
      */
     @Bean
     public PreAuthenticatedAspect preAuthenticatedAspect() {
@@ -80,9 +80,9 @@ public class YudaoSecurityAutoConfiguration {
      * Token 认证过滤器 Bean
      */
     @Bean
-    public JwtAuthenticationTokenFilter authenticationTokenFilter(SecurityAuthFrameworkService securityFrameworkService,
+    public JWTAuthenticationTokenFilter authenticationTokenFilter(SecurityAuthFrameworkService securityFrameworkService,
                                                                   GlobalExceptionHandler globalExceptionHandler) {
-        return new JwtAuthenticationTokenFilter(securityProperties, securityFrameworkService, globalExceptionHandler);
+        return new JWTAuthenticationTokenFilter(securityProperties, securityFrameworkService, globalExceptionHandler);
     }
 
 }

+ 10 - 13
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java

@@ -1,15 +1,9 @@
 package cn.iocoder.yudao.framework.security.config;
 
-import cn.iocoder.yudao.framework.security.core.filter.JwtAuthenticationTokenFilter;
-import cn.iocoder.yudao.framework.security.core.handler.AccessDeniedHandlerImpl;
-import cn.iocoder.yudao.framework.security.core.handler.AuthenticationEntryPointImpl;
-import cn.iocoder.yudao.framework.security.core.handler.LogoutSuccessHandlerImpl;
+import cn.iocoder.yudao.framework.security.core.filter.JWTAuthenticationTokenFilter;
 import cn.iocoder.yudao.framework.security.core.service.SecurityAuthFrameworkService;
 import cn.iocoder.yudao.framework.web.config.WebProperties;
-import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
-import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.http.HttpMethod;
@@ -21,8 +15,6 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
 import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
 import org.springframework.security.config.http.SessionCreationPolicy;
-import org.springframework.security.core.userdetails.UserDetailsService;
-import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.security.web.AuthenticationEntryPoint;
 import org.springframework.security.web.access.AccessDeniedHandler;
@@ -47,7 +39,7 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap
      * 自定义用户【认证】逻辑
      */
     @Resource
-    private UserDetailsService userDetailsService;
+    private SecurityAuthFrameworkService userDetailsService;
     /**
      * Spring Security 加密器
      */
@@ -72,7 +64,7 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap
      * Token 认证过滤器 Bean
      */
     @Resource
-    private JwtAuthenticationTokenFilter authenticationTokenFilter;
+    private JWTAuthenticationTokenFilter authenticationTokenFilter;
     /**
      * 自定义的权限映射 Bean
      *
@@ -135,7 +127,7 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap
 
         // 设置每个请求的权限 ①:全局共享规则
         httpSecurity.authorizeRequests()
-                    // 登的接口,可匿名访问
+                    // 登的接口,可匿名访问
                     .antMatchers(api("/login")).anonymous()
                     // 静态资源,可匿名访问
                     .antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll()
@@ -151,10 +143,15 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap
                     .antMatchers("/actuator/**").anonymous()
                     // Druid 监控 TODO 芋艿:等对接了 druid admin 后,在调整下。
                     .antMatchers("/druid/**").anonymous()
+                    // oAuth2 auth2/login/gitee
+                    .antMatchers(api("/auth2/login/**")).anonymous()
+                    .antMatchers(api("/auth2/authorization/**")).anonymous()
+                    .antMatchers("/api/callback/**").anonymous()
                 // 设置每个请求的权限 ②:每个项目的自定义规则
                 .and().authorizeRequests(authorizeRequestsCustomizer)
                 // 设置每个请求的权限 ③:兜底规则,必须认证
-                .authorizeRequests().anyRequest().authenticated();
+                .authorizeRequests().anyRequest().authenticated()
+        ;
         // 添加 JWT Filter
         httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
     }

+ 3 - 2
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java

@@ -8,10 +8,11 @@ import org.springframework.security.core.userdetails.UserDetails;
 
 import java.util.Collection;
 import java.util.Date;
+import java.util.HashSet;
 import java.util.Set;
 
 /**
- * 登用户信息
+ * 登用户信息
  *
  * @author 芋道源码
  */
@@ -69,7 +70,7 @@ public class LoginUser implements UserDetails {
     @Override
     @JsonIgnore// 避免序列化
     public Collection<? extends GrantedAuthority> getAuthorities() {
-        return null;
+        return new HashSet<>();
     }
 
     @Override

+ 2 - 2
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/annotations/PreAuthenticated.java

@@ -3,9 +3,9 @@ package cn.iocoder.yudao.framework.security.core.annotations;
 import java.lang.annotation.*;
 
 /**
- * 声明用户需要登
+ * 声明用户需要登
  *
- * 为什么不使用 {@link org.springframework.security.access.prepost.PreAuthorize} 注解,原因是不通过时,抛出的是认证不通过,而不是未登
+ * 为什么不使用 {@link org.springframework.security.access.prepost.PreAuthorize} 注解,原因是不通过时,抛出的是认证不通过,而不是未登
  *
  * @author 芋道源码
  */

+ 3 - 3
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/JwtAuthenticationTokenFilter.java → yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/JWTAuthenticationTokenFilter.java

@@ -23,10 +23,10 @@ import java.io.IOException;
  * JWT 过滤器,验证 token 的有效性
  * 验证通过后,获得 {@link LoginUser} 信息,并加入到 Spring Security 上下文
  *
- * @author ruoyi
+ * @author 芋道源码
  */
 @AllArgsConstructor
-public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
+public class JWTAuthenticationTokenFilter extends OncePerRequestFilter {
 
     private final SecurityProperties securityProperties;
 
@@ -63,7 +63,7 @@ public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
     }
 
     /**
-     * 模拟登用户,方便日常开发调试
+     * 模拟登用户,方便日常开发调试
      *
      * 注意,在线上环境下,一定要关闭该功能!!!
      *

+ 53 - 0
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/handler/AbstractSignUpUrlAuthenticationSuccessHandler.java

@@ -0,0 +1,53 @@
+/*
+ * MIT License
+ * Copyright (c) 2020-2029 YongWu zheng (dcenter.top and gitee.com/pcore and github.com/ZeroOrInfinity)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package cn.iocoder.yudao.framework.security.core.handler;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
+import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
+import org.springframework.security.web.savedrequest.RequestCache;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * @author weir
+ */
+public class AbstractSignUpUrlAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
+    private RequestCache requestCache = new HttpSessionRequestCache();
+
+    @Override
+    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
+        if (requestCache.getRequest(request, response) != null) {
+            requestCache.getRequest(request, response);
+        }
+        super.onAuthenticationSuccess(request,response,authentication);
+    }
+
+    @Override
+    public void setRequestCache(RequestCache requestCache) {
+        this.requestCache = requestCache;
+    }
+}

+ 1 - 1
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java

@@ -84,7 +84,7 @@ public class SecurityFrameworkUtils {
     /**
      * 设置当前用户
      *
-     * @param loginUser 登用户
+     * @param loginUser 登用户
      * @param request 请求
      */
     public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) {

+ 1 - 1
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/DemoFilter.java

@@ -23,7 +23,7 @@ public class DemoFilter extends OncePerRequestFilter {
     protected boolean shouldNotFilter(HttpServletRequest request) {
         String method = request.getMethod();
         return !StrUtil.equalsAnyIgnoreCase(method, "POST", "PUT", "DELETE")  // 写操作时,不进行过滤率
-                || WebFrameworkUtils.getLoginUserId(request) == null; // 非登用户时,不进行过滤
+                || WebFrameworkUtils.getLoginUserId(request) == null; // 非登用户时,不进行过滤
     }
 
     @Override

+ 1 - 1
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/infra/service/auth/SysAuthService.java

@@ -5,7 +5,7 @@ import cn.iocoder.yudao.framework.security.core.service.SecurityAuthFrameworkSer
 /**
  * 认证 Service 接口
  *
- * 提供用户的账号密码登、token 的校验等认证相关的功能
+ * 提供用户的账号密码登、token 的校验等认证相关的功能
  *
  * @author 芋道源码
  */

+ 100 - 0
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/controller/auth/MbrAuthController.java

@@ -0,0 +1,100 @@
+package cn.iocoder.yudao.userserver.modules.member.controller.auth;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.userserver.modules.member.controller.auth.vo.*;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.Valid;
+
+@Api(tags = "认证")
+@RestController
+@RequestMapping("/")
+@Validated
+@Slf4j
+public class MbrAuthController {
+
+    @PostMapping("/login")
+    @ApiOperation("使用手机 + 密码登录")
+    public CommonResult<MbrAuthLoginRespVO> login(@RequestBody @Valid MbrAuthLoginReqVO reqVO) {
+//        String token = authService.login(reqVO, getClientIP(), getUserAgent());
+//        // 返回结果
+//        return success(MbrAuthLoginRespVO.builder().token(token).build());
+        return null;
+    }
+
+    @PostMapping("/sms-login")
+    @ApiOperation("使用手机 + 验证码登录")
+    public CommonResult<MbrAuthLoginRespVO> smsLogin(@RequestBody @Valid MbrAuthLoginReqVO reqVO) {
+        return null;
+    }
+
+    @PostMapping("/send-sms-code")
+    @ApiOperation("发送手机验证码")
+    public CommonResult<Boolean> sendSmsCode(@RequestBody @Valid MbrAuthSendSmsReqVO reqVO) {
+//        passportManager.sendSmsCode(sendSmsCodeDTO, HttpUtil.getIp(request));
+//        // 返回成功
+//        return success(true);
+        return null;
+    }
+
+    @PostMapping("/reset-password")
+    @ApiOperation(value = "重置密码", notes = "用户忘记密码时使用")
+    public CommonResult<Boolean> resetPassword(@RequestBody @Valid MbrAuthResetPasswordReqVO reqVO) {
+        return null;
+    }
+
+    // ========== 社交登录相关 ==========
+
+    @GetMapping("/social-auth-redirect")
+    @ApiOperation("社交授权的跳转")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "type", value = "社交类型", required = true, dataTypeClass = Integer.class),
+            @ApiImplicitParam(name = "redirectUri", value = "回调路径", dataTypeClass = String.class)
+    })
+    public CommonResult<String> socialAuthRedirect(@RequestParam("type") Integer type,
+                                                   @RequestParam("redirectUri") String redirectUri) {
+//        return CommonResult.success(socialService.getAuthorizeUrl(type, redirectUri));
+        return null;
+    }
+
+    @PostMapping("/social-login")
+    @ApiOperation("社交登录,使用 code 授权码")
+        public CommonResult<MbrAuthLoginRespVO> socialLogin(@RequestBody @Valid MbrAuthSocialLoginReqVO reqVO) {
+//        String token = authService.socialLogin(reqVO, getClientIP(), getUserAgent());
+//        // 返回结果
+//        return success(MbrAuthLoginRespVO.builder().token(token).build());
+        return null;
+    }
+
+    @PostMapping("/social-login2")
+    @ApiOperation("社交登录,使用 code 授权码 + 账号密码")
+    public CommonResult<MbrAuthLoginRespVO> socialLogin2(@RequestBody @Valid MbrAuthSocialLogin2ReqVO reqVO) {
+//        String token = authService.socialLogin2(reqVO, getClientIP(), getUserAgent());
+//        // 返回结果
+//        return success(MbrAuthLoginRespVO.builder().token(token).build());
+        return null;
+    }
+
+    @PostMapping("/social-bind")
+    @ApiOperation("社交绑定,使用 code 授权码")
+    public CommonResult<Boolean> socialBind(@RequestBody @Valid MbrAuthSocialBindReqVO reqVO) {
+//        authService.socialBind(getLoginUserId(), reqVO);
+//        return CommonResult.success(true);
+        return null;
+    }
+
+    @DeleteMapping("/social-unbind")
+    @ApiOperation("取消社交绑定")
+    public CommonResult<Boolean> socialUnbind(@RequestBody MbrAuthSocialUnbindReqVO reqVO) {
+//        socialService.unbindSocialUser(getLoginUserId(), reqVO.getType(), reqVO.getUnionId());
+//        return CommonResult.success(true);
+        return null;
+    }
+
+}

+ 31 - 0
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/controller/auth/vo/MbrAuthLoginReqVO.java

@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.userserver.modules.member.controller.auth.vo;
+
+import cn.iocoder.yudao.framework.common.validation.Mobile;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.hibernate.validator.constraints.Length;
+
+import javax.validation.constraints.NotEmpty;
+
+@ApiModel("手机 + 密码登录 Request VO")
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class MbrAuthLoginReqVO {
+
+    @ApiModelProperty(value = "手机号", required = true, example = "15601691300")
+    @NotEmpty(message = "手机号不能为空")
+    @Mobile
+    private String mobile;
+
+    @ApiModelProperty(value = "密码", required = true, example = "buzhidao")
+    @NotEmpty(message = "密码不能为空")
+    @Length(min = 4, max = 16, message = "密码长度为 4-16 位")
+    private String password;
+
+}

+ 20 - 0
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/controller/auth/vo/MbrAuthLoginRespVO.java

@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.userserver.modules.member.controller.auth.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@ApiModel("手机密码登录 Response VO")
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class MbrAuthLoginRespVO {
+
+    @ApiModelProperty(value = "token", required = true, example = "yudaoyuanma")
+    private String token;
+
+}

+ 32 - 0
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/controller/auth/vo/MbrAuthResetPasswordReqVO.java

@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.userserver.modules.member.controller.auth.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.hibernate.validator.constraints.Length;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.Pattern;
+
+@ApiModel("重置密码 Request VO")
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class MbrAuthResetPasswordReqVO {
+
+    @ApiModelProperty(value = "新密码", required = true, example = "buzhidao")
+    @NotEmpty(message = "新密码不能为空")
+    @Length(min = 4, max = 16, message = "密码长度为 4-16 位")
+    private String password;
+
+    @ApiModelProperty(value = "手机验证码", required = true, example = "1024")
+    @NotEmpty(message = "手机验证码不能为空")
+    @Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位")
+    @Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字")
+    private String code;
+
+}

+ 24 - 0
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/controller/auth/vo/MbrAuthSendSmsReqVO.java

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.userserver.modules.member.controller.auth.vo;
+
+import cn.iocoder.yudao.framework.common.validation.Mobile;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import javax.validation.constraints.NotNull;
+
+@ApiModel("发送手机验证码 Response VO")
+@Data
+@Accessors(chain = true)
+public class MbrAuthSendSmsReqVO {
+
+    @ApiModelProperty(value = "手机号", example = "15601691234")
+    @Mobile
+    private String mobile;
+
+    @ApiModelProperty(value = "发送场景", example = "1", notes = "对应 MbrSmsSceneEnum 枚举")
+    @NotNull(message = "发送场景不能为空")
+    private Integer scene;
+
+}

+ 38 - 0
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/member/controller/auth/vo/MbrAuthSmsLoginReqVO.java

@@ -0,0 +1,38 @@
+package cn.iocoder.yudao.userserver.modules.member.controller.auth.vo;
+
+import cn.iocoder.yudao.framework.common.validation.Mobile;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.hibernate.validator.constraints.Length;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.Pattern;
+
+@ApiModel("手机 + 验证码登录 Request VO")
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class MbrAuthSmsLoginReqVO {
+
+    @ApiModelProperty(value = "手机号", required = true, example = "15601691300")
+    @NotEmpty(message = "手机号不能为空")
+    @Mobile
+    private String mobile;
+
+    @ApiModelProperty(value = "密码", required = true, example = "buzhidao")
+    @NotEmpty(message = "密码不能为空")
+    @Length(min = 4, max = 16, message = "密码长度为 4-16 位")
+    private String password;
+
+    @ApiModelProperty(value = "手机验证码", required = true, example = "1024")
+    @NotEmpty(message = "手机验证码不能为空")
+    @Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位")
+    @Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字")
+    private String code;
+
+}

Some files were not shown because too many files changed in this diff