Browse Source

接入第三方登录(已接入Gitee[理论justAuth支持都可以接入]--抢先预览版

weir 3 years atrás
parent
commit
133cb49c42
27 changed files with 1049 additions and 44 deletions
  1. 18 0
      sql/ruoyi-vue-pro.sql
  2. 16 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/auth/SysAuthController.java
  3. 7 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/user/vo/user/AuthUser.java
  4. 38 3
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/convert/auth/SysAuthConvert.java
  5. 19 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/convert/auth/config/AuthConfig.java
  6. 112 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/convert/auth/handler/DefaultSignUpUrlAuthenticationSuccessHandler.java
  7. 13 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/dal/dataobject/social/SocialUserDO.java
  8. 31 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/dal/dataobject/social/SysUserSocialDO.java
  9. 11 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/dal/mysql/social/SysSysUserSocialMapper.java
  10. 11 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/dal/mysql/social/SysUserSocialMapper.java
  11. 6 4
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/enums/common/SysSexEnum.java
  12. 7 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/auth/SysUserSessionService.java
  13. 14 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/auth/impl/SysUserSessionServiceImpl.java
  14. 237 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/auth/impl/UserDetailsServiceImpl.java
  15. 8 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/user/SysUserService.java
  16. 13 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/user/SysUserServiceImpl.java
  17. 218 0
      yudao-admin-server/src/main/resources/application-local.yaml
  18. 16 0
      yudao-admin-ui/src/api/login.js
  19. 1 1
      yudao-admin-ui/src/permission.js
  20. 5 0
      yudao-admin-ui/src/router/index.js
  21. 14 1
      yudao-admin-ui/src/views/login.vue
  22. 27 0
      yudao-admin-ui/src/views/oauthLogin.vue
  23. 1 1
      yudao-dependencies/pom.xml
  24. 13 0
      yudao-framework/yudao-spring-boot-starter-security/pom.xml
  25. 66 34
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java
  26. 66 0
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/Auth2LoginUser.java
  27. 61 0
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/handler/AbstractSignUpUrlAuthenticationSuccessHandler.java

+ 18 - 0
sql/ruoyi-vue-pro.sql

@@ -2426,3 +2426,21 @@ INSERT INTO `tool_test_demo` VALUES (107, '哈哈哈哈', 1, 0, 1, 'biubiubui',
 COMMIT;
 
 SET FOREIGN_KEY_CHECKS = 1;
+
+-- ----------------------------
+-- Table structure for sys_user_social
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_user_social`;
+CREATE TABLE `sys_user_social`  (
+  `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键(自增策略)',
+  `user_id` bigint NOT NULL COMMENT '系统用户ID',
+  `social_user_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '第三用户ID',
+  `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '创建者',
+  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+  `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '更新者',
+  `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
+  `deleted` bit(1) NULL DEFAULT b'0' COMMENT '是否删除',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = DYNAMIC;
+
+SET FOREIGN_KEY_CHECKS = 1;

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

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.adminserver.modules.system.controller.auth;
 
+import cn.iocoder.yudao.adminserver.modules.system.service.auth.SysUserSessionService;
 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;
@@ -19,6 +20,8 @@ import cn.iocoder.yudao.adminserver.modules.system.service.user.SysUserService;
 import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
@@ -46,6 +49,8 @@ public class SysAuthController {
     private SysRoleService roleService;
     @Resource
     private SysPermissionService permissionService;
+    @Resource
+    private SysUserSessionService sysUserSessionService;
 
     @PostMapping("/login")
     @ApiOperation("使用账号密码登录")
@@ -56,6 +61,17 @@ public class SysAuthController {
         return success(SysAuthLoginRespVO.builder().token(token).build());
     }
 
+    @RequestMapping("/auth2/login/{oauthType}")
+    @ApiOperation("第三方登录")
+    @OperateLog(enable = false) // 避免 Post 请求被记录操作日志
+    public CommonResult<SysAuthLoginRespVO> login(@PathVariable String oauthType) {
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        //TODO NPE
+        String token = sysUserSessionService.getSessionId(authentication.getName());
+        // 返回结果
+        return success(SysAuthLoginRespVO.builder().token(token).build());
+    }
+
     @GetMapping("/get-permission-info")
     @ApiOperation("获取登陆用户的权限信息")
     public CommonResult<SysAuthPermissionInfoRespVO> getPermissionInfo() {

+ 7 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/user/vo/user/AuthUser.java

@@ -0,0 +1,7 @@
+package cn.iocoder.yudao.adminserver.modules.system.controller.user.vo.user;
+
+/**
+ * @author weir
+ */
+public class AuthUser {
+}

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

@@ -1,5 +1,8 @@
 package cn.iocoder.yudao.adminserver.modules.system.convert.auth;
 
+import cn.iocoder.yudao.adminserver.modules.system.controller.user.vo.user.SysUserCreateReqVO;
+import cn.iocoder.yudao.adminserver.modules.system.enums.common.SysSexEnum;
+import cn.iocoder.yudao.framework.security.core.Auth2LoginUser;
 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;
@@ -10,8 +13,9 @@ 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 org.mapstruct.Mapper;
-import org.mapstruct.Mapping;
+import me.zhyd.oauth.enums.AuthUserGender;
+import me.zhyd.oauth.model.AuthUser;
+import org.mapstruct.*;
 import org.mapstruct.factory.Mappers;
 import org.slf4j.LoggerFactory;
 
@@ -21,7 +25,7 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
-@Mapper
+@Mapper(uses = SysAuthConvert.UserSexTransform.class)
 public interface SysAuthConvert {
 
     SysAuthConvert INSTANCE = Mappers.getMapper(SysAuthConvert.class);
@@ -29,6 +33,7 @@ public interface SysAuthConvert {
     @Mapping(source = "updateTime", target = "updateTime", ignore = true)
         // 字段相同,但是含义不同,忽略
     LoginUser convert(SysUserDO bean);
+    Auth2LoginUser getAuth2LoginUser(SysUserDO bean);
 
     default SysAuthPermissionInfoRespVO convert(SysUserDO user, List<SysRoleDO> roleList, List<SysMenuDO> menuList) {
         return SysAuthPermissionInfoRespVO.builder()
@@ -44,6 +49,16 @@ public interface SysAuthConvert {
 
     LoginUser convert(SysUserProfileUpdatePasswordReqVO reqVO);
 
+    @Mappings(
+        @Mapping(target = "sex", source = "gender")
+    )
+    SysUserCreateReqVO convert(AuthUser authUser);
+
+    @Mappings(
+        @Mapping(target = "thirdPartyUserId", source = "uuid")
+    )
+    Auth2LoginUser getLoginUser(AuthUser authUser);
+
     /**
      * 将菜单列表,构建成菜单树
      *
@@ -76,4 +91,24 @@ public interface SysAuthConvert {
         return CollectionUtils.filterList(treeNodeMap.values(), node -> MenuIdEnum.ROOT.getId().equals(node.getParentId()));
     }
 
+    public class UserSexTransform {
+
+        public int toInt (AuthUserGender gender){
+            switch (gender) {
+                case MALE:
+                    return SysSexEnum.MALE.getSex();
+                case  FEMALE:
+                    return  SysSexEnum.FEMALE.getSex();
+                default:
+                    return SysSexEnum.UNKNOWN.getSex();
+            }
+        }
+
+        public AuthUserGender strToBoolean(int sex){
+            if(sex == SysSexEnum.UNKNOWN.getSex()) {
+                return AuthUserGender.UNKNOWN;
+            }
+            return AuthUserGender.getRealGender(String.valueOf(sex));
+        }
+    }
 }

+ 19 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/convert/auth/config/AuthConfig.java

@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.adminserver.modules.system.convert.auth.config;
+
+import cn.iocoder.yudao.adminserver.modules.system.convert.auth.handler.DefaultSignUpUrlAuthenticationSuccessHandler;
+import cn.iocoder.yudao.framework.security.core.handler.AbstractSignUpUrlAuthenticationSuccessHandler;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author weir
+ */
+@Configuration
+public class AuthConfig {
+    @Bean
+    public AbstractSignUpUrlAuthenticationSuccessHandler authenticationSuccessHandler() {
+        AbstractSignUpUrlAuthenticationSuccessHandler successHandler = new DefaultSignUpUrlAuthenticationSuccessHandler();
+        successHandler.setDefaultTargetUrl("/api/callback");
+        return successHandler;
+    }
+}

+ 112 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/convert/auth/handler/DefaultSignUpUrlAuthenticationSuccessHandler.java

@@ -0,0 +1,112 @@
+/*
+ * 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.adminserver.modules.system.convert.auth.handler;
+
+import cn.iocoder.yudao.adminserver.modules.system.controller.auth.vo.auth.SysAuthLoginRespVO;
+import cn.iocoder.yudao.adminserver.modules.system.service.auth.SysUserSessionService;
+import cn.iocoder.yudao.adminserver.modules.system.service.permission.SysPermissionService;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.security.core.Auth2LoginUser;
+import cn.iocoder.yudao.framework.security.core.LoginUser;
+import cn.iocoder.yudao.framework.security.core.handler.AbstractSignUpUrlAuthenticationSuccessHandler;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.DefaultRedirectStrategy;
+import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
+import org.springframework.security.web.savedrequest.RequestCache;
+
+import javax.annotation.Resource;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Set;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
+import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getUserAgent;
+import static java.util.Collections.singleton;
+import static top.dcenter.ums.security.core.oauth.util.MvcUtil.*;
+
+/**
+ * @author weir
+ */
+public class DefaultSignUpUrlAuthenticationSuccessHandler extends AbstractSignUpUrlAuthenticationSuccessHandler {
+    private RequestCache requestCache = new HttpSessionRequestCache();
+    @Autowired
+    private SysUserSessionService userSessionService;
+    @Resource
+    private SysPermissionService permissionService;
+
+    @Override
+    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
+        final Object principal = authentication.getPrincipal();
+        String token = userSessionService.createUserSession(defaultHandleUserRoles((LoginUser) principal), getClientIP(), getUserAgent());
+        if(StringUtils.isNotBlank(token)) {
+            SecurityContextHolder.getContext().setAuthentication(authentication);
+        }
+        if (principal instanceof Auth2LoginUser) {
+            new DefaultRedirectStrategy().sendRedirect(request, response, getUrl() + token);
+            return;
+        }
+        if (isAjaxOrJson(request)) {
+            responseWithJson(response, HttpStatus.OK.value(), toJsonString(success(SysAuthLoginRespVO.builder().token(token).build())));
+            return;
+        }
+        try {
+            requestCache.saveRequest(request, response);
+            super.setRequestCache(requestCache);
+            super.onAuthenticationSuccess(request, response, authentication);
+        } catch (ServletException e) {
+            e.printStackTrace();
+        }
+
+    }
+
+    private String getUrl() {
+        return "http://localhost/oauthLogin/gitee?token=";
+    }
+
+    /**
+     * 默认处理方式处理用户角色列表;建议角色权限前置到 UserDetails
+     *
+     * @param loginUser 用户
+     * @return
+     */
+    private LoginUser defaultHandleUserRoles(LoginUser loginUser) {
+        Set<Long> roleIds = loginUser.getRoleIds();
+        if (roleIds == null || roleIds.isEmpty()) {
+            Set<Long> userRoleIds = permissionService.getUserRoleIds(loginUser.getId(), singleton(CommonStatusEnum.ENABLE.getStatus()));
+            loginUser.setRoleIds(userRoleIds);
+        }
+        return loginUser;
+    }
+
+    @Override
+    public void setRequestCache(RequestCache requestCache) {
+        this.requestCache = requestCache;
+    }
+}

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

@@ -0,0 +1,13 @@
+package cn.iocoder.yudao.adminserver.modules.system.dal.dataobject.social;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import me.zhyd.oauth.model.AuthUser;
+
+/**
+ * @author weir
+ */
+@Data
+@TableName("user_connection")
+public class SocialUserDO extends AuthUser {
+}

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

@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.adminserver.modules.system.dal.dataobject.social;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 系统用户和第三方用户关联表
+ * @author weir
+ */
+@TableName("sys_user_social")
+@Data
+public class SysUserSocialDO extends BaseDO {
+    /**
+     * 自增主键
+     */
+    @TableId
+    private Long id;
+    /**
+     * 用户 ID
+     */
+    private Long userId;
+    /**
+     * 角色 ID
+     */
+    private String socialUserId;
+}
+
+

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

@@ -0,0 +1,11 @@
+package cn.iocoder.yudao.adminserver.modules.system.dal.mysql.social;
+
+import cn.iocoder.yudao.adminserver.modules.system.dal.dataobject.social.SysUserSocialDO;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface SysSysUserSocialMapper extends BaseMapperX<SysUserSocialDO> {
+
+
+}

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

@@ -0,0 +1,11 @@
+package cn.iocoder.yudao.adminserver.modules.system.dal.mysql.social;
+
+import cn.iocoder.yudao.adminserver.modules.system.dal.dataobject.social.SysUserSocialDO;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface SysUserSocialMapper extends BaseMapperX<SysUserSocialDO> {
+
+
+}

+ 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);
     /**
      * 性别
      */

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

@@ -46,6 +46,13 @@ public interface SysUserSessionService {
     LoginUser getLoginUser(String sessionId);
 
     /**
+     * 获取当前登录用户信息
+     * @param username 用户名称
+     * @return 在线用户
+     */
+    String getSessionId(String username);
+
+    /**
      * 获得 Session 超时时间,单位:毫秒
      *
      * @return 超时时间

+ 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 {
 
@@ -92,6 +97,15 @@ public class SysUserSessionServiceImpl implements SysUserSessionService {
     }
 
     @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();
     }

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

@@ -0,0 +1,237 @@
+/*
+ * 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.adminserver.modules.system.service.auth.impl;
+
+import cn.iocoder.yudao.adminserver.modules.system.controller.user.vo.user.SysUserCreateReqVO;
+import cn.iocoder.yudao.adminserver.modules.system.convert.auth.SysAuthConvert;
+import cn.iocoder.yudao.adminserver.modules.system.dal.dataobject.user.SysUserDO;
+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.security.core.Auth2LoginUser;
+import cn.iocoder.yudao.framework.security.core.LoginUser;
+import com.google.common.collect.Sets;
+import me.zhyd.oauth.model.AuthUser;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.lang.NonNull;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserCache;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import top.dcenter.ums.security.core.oauth.enums.ErrorCodeEnum;
+import top.dcenter.ums.security.core.oauth.exception.RegisterUserFailureException;
+import top.dcenter.ums.security.core.oauth.exception.UserNotExistException;
+import top.dcenter.ums.security.core.oauth.service.UmsUserDetailsService;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ *  用户密码与手机短信登录与注册服务:<br><br>
+ *  1. 用于第三方登录与手机短信登录逻辑。<br><br>
+ *  2. 用于用户密码登录逻辑。<br><br>
+ *  3. 用户注册逻辑。<br><br>
+ * @author YongWu zheng
+ * @version V1.0  Created by 2020/9/20 11:06
+ */
+@Service
+public class UserDetailsServiceImpl implements UmsUserDetailsService {
+
+    private final Logger log = LoggerFactory.getLogger(this.getClass());
+
+    @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
+    @Autowired(required = false)
+    private UserCache userCache;
+
+    @Resource
+    private SysUserService userService;
+    
+    /**
+     * 用于密码加解密
+     */
+    @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
+    @Autowired
+    private PasswordEncoder passwordEncoder;
+    @Autowired
+    private SysPermissionService permissionService;
+
+    @SuppressWarnings("AlibabaUndefineMagicConstant")
+    @Override
+    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+
+        try
+        {
+            // 从缓存中查询用户信息:
+            // 从缓存中查询用户信息
+            if (this.userCache != null)
+            {
+                UserDetails userDetails = this.userCache.getUserFromCache(username);
+                if (userDetails != null)
+                {
+                    return userDetails;
+                }
+            }
+            // 根据用户名获取用户信息
+            // 获取 username 对应的 SysUserDO
+            SysUserDO user = userService.getUserByUsername(username);
+            if (user == null) {
+                throw new UsernameNotFoundException(username);
+            }
+            // 创建 LoginUser 对象
+            Auth2LoginUser loginUser = SysAuthConvert.INSTANCE.getAuth2LoginUser(user);
+            //TODO 登录日志等可以和用户名密码等兼容处理
+           return loginUser;
+        }
+        catch (Exception e)
+        {
+            String msg = String.format("第三方登录 ======>: 登录用户名:%s, 登录失败: %s", username, e.getMessage());
+            log.error(msg);
+            throw new UserNotExistException(ErrorCodeEnum.QUERY_USER_INFO_ERROR, e, username);
+        }
+    }
+
+    @Override
+    public UserDetails registerUser(@NonNull AuthUser authUser, @NonNull String username,
+                                    @NonNull String defaultAuthority, String decodeState) throws RegisterUserFailureException {
+
+        // 这里的 decodeState 可以根据自己实现的 top.dcenter.ums.security.core.oauth.service.Auth2StateCoder 接口的逻辑来传递必要的参数.
+        // 比如: 第三方登录成功后的跳转地址
+        final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
+        // 假设 decodeState 就是 redirectUrl, 我们直接把 redirectUrl 设置到 request 上
+        // 后续经过成功处理器时直接从 requestAttributes.getAttribute("redirectUrl", RequestAttributes.SCOPE_REQUEST) 获取并跳转
+        if (requestAttributes != null) {
+            requestAttributes.setAttribute("redirectUrl", decodeState, RequestAttributes.SCOPE_REQUEST);
+        }
+        //返回用户
+        LoginUser loginUser = doRegistUser(authUser);
+
+        log.info("第三方用户注册 ======>: 用户名:{}, 注册成功", username);
+
+        // 把用户信息存入缓存
+        if (userCache != null)
+        {
+            userCache.putUserInCache(loginUser);
+        }
+
+        return loginUser;
+    }
+
+    private LoginUser doRegistUser(AuthUser authUser) {
+        SysUserCreateReqVO reqVO = SysAuthConvert.INSTANCE.convert(authUser);
+        if (StringUtils.isEmpty(reqVO.getPassword())) {
+            reqVO.setPassword(getDefaultPassword());
+        }
+        //添加用户
+        Long sysUserId = userService.createUser(reqVO);
+        //关联第三方用户
+        Long userId = userService.bindSocialUSer(sysUserId, authUser.getUuid());
+        //赋予默认角色权限;三方登录默认部分
+        permissionService.assignUserRole(userId, getDefaultRoles());
+        LoginUser loginUser = SysAuthConvert.INSTANCE.getLoginUser(authUser);
+        loginUser.setRoleIds(getDefaultRoles());
+        loginUser.setPassword(getDefaultPassword());
+        loginUser.setId(sysUserId);
+        return loginUser;
+    }
+
+    private String getDefaultPassword() {
+        return "123456";
+    }
+
+    protected Set<Long> getDefaultRoles() {
+        return Sets.newHashSet(1L);
+    }
+
+    @NonNull
+    public UserDetails registerUser(@NonNull String mobile, Map<String, String> otherParamMap) throws RegisterUserFailureException {
+
+        // 用户信息持久化逻辑。。。
+        // ...
+
+        log.info("Demo ======>: 手机短信登录用户 {}:注册成功", mobile);
+
+        User user = new User(mobile,
+                             passwordEncoder.encode("admin"),
+                             true,
+                             true,
+                             true,
+                             true,
+                             AuthorityUtils.commaSeparatedStringToAuthorityList("admin, ROLE_USER")
+        );
+
+        // 把用户信息存入缓存
+        if (userCache != null)
+        {
+            userCache.putUserInCache(user);
+        }
+
+        return user;
+    }
+
+    /**
+     * {@link #existedByUsernames(String...)} usernames 生成规则.
+     * 如需自定义重新实现此逻辑
+     * @param authUser     第三方用户信息
+     * @return  返回一个 username 数组
+     */
+    @Override
+    public String[] generateUsernames(AuthUser authUser) {
+        return new String[]{
+                authUser.getUsername(),
+                // providerId = authUser.getSource()
+                authUser.getUsername() + "_" + authUser.getSource(),
+                // providerUserId = authUser.getUuid()
+                authUser.getUsername() + "_" + authUser.getSource() + "_" + authUser.getUuid()
+        };
+    }
+
+    @Override
+    public UserDetails loadUserByUserId(String userId) throws UsernameNotFoundException {
+        UserDetails userDetails = loadUserByUsername(userId);
+        User.withUserDetails(userDetails);
+        return userDetails;
+    }
+
+    @Override
+    public List<Boolean> existedByUsernames(String... usernames) throws UsernameNotFoundException {
+        // ... 在本地账户上查询 userIds 是否已被使用
+        List<Boolean> list = new ArrayList<>();
+        for (String username : usernames) {
+            SysUserDO userDO = userService.getUserByUsername(username);
+            list.add(userDO != null);
+        }
+        return list;
+    }
+
+}

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

@@ -35,6 +35,14 @@ public interface SysUserService {
     Long createUser(SysUserCreateReqVO reqVO);
 
     /**
+     * 绑定第三方用户
+     * @param sysUserId 系统用户ID
+     * @param socialUSerId 第三方唯一标识
+     * @return
+     */
+    Long bindSocialUSer(Long sysUserId, String socialUSerId);
+
+    /**
      * 修改用户
      *
      * @param reqVO 用户信息

+ 13 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/user/SysUserServiceImpl.java

@@ -4,6 +4,8 @@ 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.adminserver.modules.system.dal.dataobject.social.SysUserSocialDO;
+import cn.iocoder.yudao.adminserver.modules.system.dal.mysql.social.SysUserSocialMapper;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.exception.ServiceException;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
@@ -49,6 +51,8 @@ public class SysUserServiceImpl implements SysUserService {
 
     @Resource
     private SysUserMapper userMapper;
+    @Resource
+    private SysUserSocialMapper userSocialMapper;
 
     @Resource
     private SysDeptService deptService;
@@ -75,6 +79,15 @@ public class SysUserServiceImpl implements SysUserService {
     }
 
     @Override
+    public Long bindSocialUSer(Long sysUserId, String socialUSerId) {
+        SysUserSocialDO userSocialDO = new SysUserSocialDO();
+        userSocialDO.setUserId(sysUserId);
+        userSocialDO.setSocialUserId(socialUSerId);
+        userSocialMapper.insert(userSocialDO);
+        return userSocialDO.getUserId();
+    }
+
+    @Override
     public void updateUser(SysUserUpdateReqVO reqVO) {
         // 校验正确性
         this.checkCreateOrUpdate(reqVO.getId(), reqVO.getUsername(), reqVO.getMobile(), reqVO.getEmail(),

+ 218 - 0
yudao-admin-server/src/main/resources/application-local.yaml

@@ -167,3 +167,221 @@ yudao:
       - ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求
       - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求
   demo: false # 关闭演示模式
+
+
+# ums core
+ums:
+  one-click-login:
+    # 一键登录是否开启, 默认 false
+    enable: true
+    # 一键登录请求处理 url, 默认 /authentication/one-click
+    login-processing-url: /authentication/one-click
+    # token 参数名称, 默认: accessToken
+    token-param-name: accessToken
+    # 其他请求参数名称列表(包括请求头名称), 此参数会传递到 OneClickLoginService.callback(String, Map)
+    # 与 UserDetailsRegisterService.registerUser(String, Map); 默认为: 空
+    other-param-names:
+      - imei
+  # ================ 第三方授权登录相关配置 ================
+  oauth:
+    # 第三方授权登录后如未注册用户是否支持自动注册功能, 默认: true
+    auto-sign-up: true
+    # 第三方授权登录后如未注册用户不支持自动注册功能, 则跳转到此 url 进行注册逻辑, 此 url 必须开发者自己实现; 默认: /signUp.html;
+    # 注意: 当 autoSignUp = false 时, 此属性才生效.
+    # 例如: 1. 设置值 "/signUp", 则跳转指定到 "/signUp" 进行注册.
+    # 2. 想返回自定义 json 数据到前端, 这里要设置 null , 在 Auth2LoginAuthenticationFilter 设置的 AuthenticationSuccessHandler
+    # 上处理返回 json; 判断是否为临时用户的条件是: Authentication.getPrincipal() 是否为 TemporaryUser 类型.
+    sign-up-url: null
+    # 用于第三方授权登录时, 未开启自动注册且用户是第一次授权登录的临时用户密码, 默认为: "". 注意: 生产环境更换密码
+    temporary-user-password: ""
+    # 用于第三方授权登录时, 未开启自动注册且用户是第一次授权登录的临时用户的默认权限, 多个权限用逗号分开, 默认为: "ROLE_TEMPORARY_USER"
+    temporary-user-authorities: ROLE_TEMPORARY_USER
+    # 抑制反射警告, 支持 JDK11, 默认: false , 在确认 WARNING: An illegal reflective access operation has occurred 安全后, 可以打开此设置, 可以抑制反射警告.
+    suppress-reflect-warning: true
+    # 第三方服务商: providerId, 支持所有 JustAuth 支持的第三方授权登录, 目前有 32 家第三方授权登录
+    github:
+      # 根据是否有设置 clientId 来动态加载相应 JustAuth 的 AuthXxxRequest
+      client-id: 4d4ee00e82f669f2ea8d
+      client-secret: 4050be113a83556b63bd991d606fded437b05235
+      scopes:
+    #        - 'repo:status'
+    #        - 'public_repo'
+    #        - 'repo:invite'
+    #        - 'user:follow'
+    gitee:
+      client-id: eb5e3298cc10ead57cd40f9f36e7154ab2ea54dcb519684d7e10ca3fec5b1c1a
+      client-secret: 495b5542dc007e2d46d316c527bd05dac586f2ec31b96c551e164387b5edb1cb
+      scopes:
+        - user_info
+#    # 自定义 OAuth2 Login
+#    customize:
+#      client-id: c971cf1634460e18310a5d7cb0f55d7d143a72015b2f29aee6a0e8911efac7eb
+#      client-secret: 309c9521721e3eb385a99a6bde2755f3107c7e15f3b8e0527c9f3ea4d1ce33bb
+#      # 自定义第三方授权登录时, 当 Auth2Properties#customize 时有效, 此字段必须以驼峰方式命名.
+#      # 比如此字段的值为 umsCustomize, 那么 /auth2/authorization/customize 会替换为 /auth2/authorization/umsCustomize
+#      customize-provider-id: giteeCustomize
+#      # 自定义第三方授权登录, 当 Auth2Properties#customize 时有效, 设置第三方是否在国外, 默认: false.
+#      # 如果为 false 时, 设置 {@link HttpConfig} 的超时时间为 ums.oauth.proxy.timeout 的值.
+#      # 如果为 true 时, 设置 {@link HttpConfig} 的超时时间为 ums.oauth.proxy.foreignTimeout 的值.
+#      customize-is-foreign: false
+    # 第三方登录授权登录 url 前缀, 不包含 ServletContextPath,默认为 /auth2/authorization.
+    auth-login-url-prefix: /api/auth2/authorization
+    # 第三方登录回调处理 url 前缀 ,也就是 RedirectUrl 的前缀, 不包含 ServletContextPath,默认为 /auth2/login.
+    redirect-url-prefix: /api/auth2/login
+    # 第三方登录回调的域名, 例如:http://localhost:9090 默认为 "http://127.0.0.1",
+    # redirectUrl 直接由 {domain}/{servletContextPath}/{redirectUrlPrefix}/{providerId}(ums.oauth.[qq/gitee/weibo])组成
+    domain: http://localhost:48080
+    # 第三方授权登录成功后的默认权限, 多个权限用逗号分开, 默认为: "ROLE_USER"
+    default-authorities: ROLE_USER
+    # 是否支持内置的第三方登录用户表(user_connection) 和 auth_token 表. 默认: true.
+    # 注意: 如果为 false, 则必须重新实现 ConnectionService 接口.
+    enable-user-connection-and-auth-token-table: true
+    # 是否支持内置的第三方登录 token 表(auth_token). 默认: true.
+    enable-auth-token-table: true
+
+
+    # ================ start: 定时刷新 access token 定时任务相关配置 ================
+
+    # 是否支持定时刷新 AccessToken 定时任务. 默认: false.
+    # 支持分布式(分布式 IOC 容器中必须有 RedisConnectionFactory, 也就是说, 是否分布式执行依据 IOC 容器中是否有 RedisConnectionFactory)
+    enableRefreshTokenJob: false
+    # A cron-like expression. 0 * 2 * * ? 分别对应: second/minute/hour/day of month/month/day of week,
+    # 默认为: "0 * 2 * * ?", 凌晨 2 点启动定时任务, 支持分布式(分布式 IOC 容器中必须有 {@link RedisConnectionFactory},
+    # 也就是说, 是否分布式执行依据 IOC 容器中是否有 {@link RedisConnectionFactory})
+    refresh-token-job-cron: 0 0/5 * * * ?
+    # 定时刷新 accessToken 任务时, 批处理数据库的记录数. 注意: 分布式应用时, 此配置不同服务器配置必须是一样的. batchCount 大小需要根据实际生产环境进行优化
+    batch-count: 1000
+    # accessToken 的剩余有效期内进行刷新 accessToken, 默认: 24, 单位: 小时. 注意: 需要根据实际生产环境进行优化
+    remaining-expire-in: 24
+
+    # ================ start: 定时刷新 access token 定时任务相关配置 ================
+
+    # JustAuth 内部参数设置
+    just-auth:
+      # 忽略校验 state 参数,默认不开启。当 ignoreCheckState 为 true 时, me.zhyd.oauth.request.AuthDefaultRequest.login(AuthCallback) 将不会校验 state 的合法性。
+      # 使用场景:当且仅当使用自实现 state 校验逻辑时开启
+      # 以下场景使用方案仅作参考:
+      # 1. 授权、登录为同端,并且全部使用 JustAuth 实现时,该值建议设为 false;
+      # 2. 授权和登录为不同端实现时,比如前端页面拼装 authorizeUrl,并且前端自行对state进行校验,后端只负责使用code获取用户信息时,该值建议设为 true;
+      # 如非特殊需要,不建议开启这个配置
+      # 该方案主要为了解决以下类似场景的问题:Since: 1.15.6, See Also: https://github.com/justauth/JustAuth/issues/83
+      ignoreCheckState: false
+      # 默认 state 缓存过期时间:3分钟(PT180S) 鉴于授权过程中,根据个人的操作习惯,或者授权平台的不同(google等),每个授权流程的耗时也有差异,
+      # 不过单个授权流程一般不会太长 本缓存工具默认的过期时间设置为3分钟,即程序默认认为3分钟内的授权有效,超过3分钟则默认失效,失效后删除
+      # 注意: 这是为了测试打断点时用的, 生产环境自己设置为合适数组或默认
+      timeout: PT1800S
+      # JustAuth state 缓存类型, 默认 session
+      cacheType: session
+      # JustAuth state 缓存 key 前缀
+      cacheKeyPrefix: 'JUST_AUTH:'
+
+    # 用于 JustAuth 的代理(HttpClient)设置
+    proxy:
+      # 是否支持代理, 默认为: false.
+      enable: false
+      # 针对国外服务可以单独设置代理类型, 默认 Proxy.Type.HTTP, enable = true 时生效.
+      proxy: HTTP
+      # 代理 host, enable = true 时生效.
+      hostname:
+      # 代理端口, enable = true 时生效.
+      port:
+      # 用于国内代理(HttpClient)超时, 默认 PT3S
+      timeout: PT3S
+      # 用于国外网站代理(HttpClient)超时, 默认 PT15S
+      foreign-timeout: PT150S
+
+
+  # =============== start: 第三方登录时用的数据库表 user_connection 与 auth_token 添加 redis cache ===============
+  cache:
+    # redisCacheManager 设置, 默认实现: 对查询结果 null 值进行缓存, 添加时更新缓存 null 值.
+    redis:
+      # 是否开启缓存, 默认 false
+      open: false
+      # 是否使用 spring IOC 容器中的 RedisConnectionFactory, 默认: false
+      # 如果使用 spring IOC 容器中的 RedisConnectionFactory,则要注意 cache.database-index 要与 spring.redis.database 一样。
+      use-ioc-redis-connection-factory: true
+      cache:
+        # redis cache 存放的 database index, 默认: 0
+        database-index: 1
+        # 设置缓存管理器管理的缓存的默认过期时间, 默认: 200s
+        default-expire-time: PT200S
+        # cache ttl 。使用 0 声明一个永久的缓存。 默认: 180, 单位: 秒<br>
+        # 取缓存时间的 20% 作为动态的随机变量上下浮动, 防止同时缓存失效而缓存击穿
+        entry-ttl: PT180S
+        # Names of the default caches to consider for caching operations defined in the annotated class.
+        # 此设置不对 user_connection 与 auth_token 使用的缓存名称(UCC/UCHC/UCHACC)产生影响.
+        cache-names:
+          - cacheName
+  # =============== end: 第三方登录时用的数据库表 user_connection 与 auth_token 添加 redis cache ===============
+
+  # =============== start: 线程池配置 ===============
+  executor:
+    # 启动第三方授权登录用户的 accessToken 的定时任务时的 Executor 属性, 注意: 需要根据实际生产环境进行优化
+    job-task-scheduled-executor:
+      # 线程池中空闲时保留的线程数, 默认: 0
+      core-pool-size: 0
+      # keep alive time, 默认: 10
+      keep-alive-time: 0
+      # keepAliveTime 时间单位, 默认: 毫秒
+      time-unit: milliseconds
+      # 线程池名称, 默认: accessTokenJob
+      pool-name: accessTokenJob
+      # 拒绝策略, 默认: ABORT
+      rejected-execution-handler-policy: abort
+      # 线程池关闭过程的超时时间, 默认: PT10S
+      executor-shutdown-timeout: PT10S
+    # 更新第三方授权登录用户的 accessToken 的执行逻辑, 向本地数据库 auth_token 表获取过期或在一定时间内过期的 token 记录,
+    # 用 refreshToken 向第三方服务商更新 accessToken 信息的 Executor 属性,
+    # 注意: 定时刷新 accessToken 的执行逻辑是多线程的, 需要根据实际生产环境进行优化
+    refresh-token:
+      # 程池中空闲时保留的线程数, 默认: 0
+      core-pool-size: 0
+      # 最大线程数, 默认: 本机核心数
+      maximum-pool-size: 8
+      # keep alive time, 默认: 5
+      keep-alive-time: 5
+      # keepAliveTime 时间单位, 默认: 秒
+      time-unit: seconds
+      # blocking queue capacity, 默认: maximumPoolSize * 2
+      blocking-queue-capacity: 16
+      # 线程池名称, 默认: refreshToken
+      pool-name: refreshToken
+      # 拒绝策略, 默认: CALLER_RUNS 注意: 一般情况下不要更改默认设置, 没有实现 RefreshToken 逻辑被拒绝执行后的处理逻辑, 除非自己实现RefreshTokenJob.refreshTokenJob() 对 RefreshToken 逻辑被拒绝执行后的处理逻辑.
+      rejected-execution-handler-policy: caller_runs
+      # 线程池关闭过程的超时时间, 默认: 10 秒
+      executor-shutdown-timeout: PT10S
+    # 第三方授权登录时, 异步更新用户的第三方授权用户信息与 token 信息的 Executor 属性,
+    # 注意: 第三方授权登录时是异步更新第三方用户信息与 token 信息到本地数据库时使用此配置, 需要根据实际生产环境进行优化
+    user-connection-update:
+      # 程池中空闲时保留的线程数, 默认: 5
+      core-pool-size: 5
+      # 最大线程数, 默认: 本机核心数
+      maximum-pool-size: 8
+      # keep alive time, 默认: 10
+      keep-alive-time:
+      # keepAliveTime 时间单位, 默认: 秒
+      time-unit: seconds
+      # blocking queue capacity, 默认: maximumPoolSize * 2
+      blocking-queue-capacity: 16
+      # 线程池名称, 默认: updateConnection
+      pool-name: updateConnection
+      # 拒绝策略, 默认: CALLER_RUNS 注意: 一般情况下不要更改默认设置, 除非自己实现Auth2LoginAuthenticationProvider更新逻辑;
+      # 改成 ABORT 也支持, 默认实现 Auth2LoginAuthenticationProvider 是异步更新被拒绝执行后, 会执行同步更新.
+      rejected-execution-handler-policy: caller_runs
+      # 线程池关闭过程的超时时间, 默认: PT10S
+      executor-shutdown-timeout: PT10S
+  # =============== end: 线程池配置 ===============
+
+  # =============== start: user_connection repository 配置 ===============
+  repository:
+
+    # 第三方登录用户数据库表的字段 accessToken 与 refreshToken 加密专用密码
+    text-encryptor-password: 7ca5d913a17b4942942d16a974e3fecc
+    # 第三方登录用户数据库表的字段 accessToken 与 refreshToken 加密专用 salt
+    text-encryptor-salt: cd538b1b077542aca5f86942b6507fe2
+    # 是否在启动时检查并自动创建 userConnectionTableName 与 authTokenTableName, 默认: TRUE
+    enableStartUpInitializeTable: true
+    # 其他的有: 数据库表名称, 字段名称, curd sql 语句 等设置, 一般不需要更改,
+    # 如果要添加字段: 具体查看 RepositoryProperties 与 Auth2JdbcUsersConnectionRepository
+  # =============== end: user_connection repository 配置 ===============
+

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

@@ -38,3 +38,19 @@ export function getCodeImg() {
     method: 'get'
   })
 }
+
+// 接入第三方登录
+export function giteeLogin() {
+  return request({
+    url: '/auth2/authorization/gitee',
+    method: 'get'
+  })
+}
+
+export function getToken(path) {
+  console.log({path});
+  return request({
+    url: '/auth2/login/gitee' +  path,
+    method: 'get'
+  })
+}

+ 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', '/auth-redirect', '/bind', '/register', '/oauthLogin/gitee']
 
 router.beforeEach((to, from, next) => {
   NProgress.start()

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

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

+ 14 - 1
yudao-admin-ui/src/views/login.vue

@@ -45,6 +45,11 @@
           <span v-else>登 录 中...</span>
         </el-button>
       </el-form-item>
+      <el-form-item style="width:100%;">
+        <el-button @click="doAuth2Login" title="使用 Gitee 帐号登录">
+          <span class="gitee-login-title">使用 Gitee 帐号登录</span>
+        </el-button>
+      </el-form-item>
     </el-form>
     <!--  底部  -->
     <div class="el-login-footer">
@@ -54,7 +59,7 @@
 </template>
 
 <script>
-import { getCodeImg } from "@/api/login";
+import { getCodeImg,giteeLogin } from "@/api/login";
 import Cookies from "js-cookie";
 import { encrypt, decrypt } from '@/utils/jsencrypt'
 
@@ -135,6 +140,14 @@ export default {
           });
         }
       });
+    },
+    doAuth2Login() {
+      console.log("开始Oauth登录...");
+      this.loading = true;
+      giteeLogin().then((res) => {
+        console.log(res.url);
+        window.location.href = res.url;
+      });
     }
   }
 };

+ 27 - 0
yudao-admin-ui/src/views/oauthLogin.vue

@@ -0,0 +1,27 @@
+<template>
+  <div>登录成功!</div>
+</template>
+
+<script>
+import { getToken, setToken, removeToken } from '@/utils/auth'
+
+export default {
+  created() {
+    var token = this.getOauthToken();
+    setToken(token);
+    this.$router.push({ path: this.redirect || "/" }).catch(() => {});
+  },
+  methods: {
+    getOauthToken() {
+      console.log(this);
+      var h = window.location.href;
+      var token = "token=";
+      return h.substring(h.indexOf(token)+token.length);
+    }
+  },
+}
+</script>
+
+<style>
+
+</style>

+ 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>

+ 13 - 0
yudao-framework/yudao-spring-boot-starter-security/pom.xml

@@ -39,6 +39,19 @@
             <artifactId>spring-boot-starter-security</artifactId>
         </dependency>
 
+        <!-- justAuth 集成第三方登录 -->
+        <dependency>
+            <groupId>top.dcenter</groupId>
+            <artifactId>justAuth-spring-security-starter</artifactId>
+            <version>1.1.25</version>
+            <exclusions>
+                <exclusion>
+                    <artifactId>slf4j-api</artifactId>
+                    <groupId>org.slf4j</groupId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
     </dependencies>
 
 </project>

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

@@ -1,15 +1,12 @@
 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.handler.AbstractSignUpUrlAuthenticationSuccessHandler;
 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.Autowired;
 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;
@@ -19,13 +16,15 @@ import org.springframework.security.config.annotation.method.configuration.Enabl
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
 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;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
 import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
+import top.dcenter.ums.security.core.oauth.config.Auth2AutoConfigurer;
+import top.dcenter.ums.security.core.oauth.properties.Auth2Properties;
+import top.dcenter.ums.security.core.oauth.properties.OneClickLoginProperties;
 
 import javax.annotation.Resource;
 
@@ -48,7 +47,7 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap
      * 自定义用户【认证】逻辑
      */
     @Resource
-    private UserDetailsService userDetailsService;
+    private SecurityAuthFrameworkService userDetailsService;
     /**
      * Spring Security 加密器
      */
@@ -75,6 +74,15 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap
     @Resource
     private JwtAuthenticationTokenFilter authenticationTokenFilter;
 
+    @Autowired
+    private Auth2AutoConfigurer auth2AutoConfigurer;
+    @Autowired
+    private Auth2Properties auth2Properties;
+    @Autowired
+    private OneClickLoginProperties oneClickLoginProperties;
+    @Autowired
+    private AbstractSignUpUrlAuthenticationSuccessHandler authenticationSuccessHandler;
+
     /**
      * 由于 Spring Security 创建 AuthenticationManager 对象时,没声明 @Bean 注解,导致无法被注入
      * 通过覆写父类的该方法,添加 @Bean 注解,解决该问题
@@ -114,7 +122,11 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap
      */
     @Override
     protected void configure(HttpSecurity httpSecurity) throws Exception {
+
         httpSecurity
+                // ========= start: 使用 justAuth-spring-security-starter 必须步骤 =========
+                // 添加 Auth2AutoConfigurer 使 OAuth2(justAuth) login 生效.
+                .apply(this.auth2AutoConfigurer).and()
                 // 开启跨域
                 .cors().and()
                 // CSRF 禁用,因为不使用 Session
@@ -123,39 +135,59 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap
                 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                 // 一堆自定义的 Spring Security 处理器
                 .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
-                    .accessDeniedHandler(accessDeniedHandler).and()
+                .accessDeniedHandler(accessDeniedHandler).and()
+                .formLogin().loginPage(api("/login")).successHandler(authenticationSuccessHandler).and()
                 // 设置每个请求的权限
                 .authorizeRequests()
-                    // 登陆的接口,可匿名访问
-                    .antMatchers(api("/login")).anonymous()
-                    // 通用的接口,可匿名访问 TODO 芋艿:需要抽象出去
-                    .antMatchers(api("/system/captcha/**")).anonymous()
-                    // 静态资源,可匿名访问
-                    .antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll()
-                    // 文件的获取接口,可匿名访问
-                    .antMatchers(api("/infra/file/get/**")).anonymous()
-                    // Swagger 接口文档
-                    .antMatchers("/swagger-ui.html").anonymous()
-                    .antMatchers("/swagger-resources/**").anonymous()
-                    .antMatchers("/webjars/**").anonymous()
-                    .antMatchers("/*/api-docs").anonymous()
-                    // Spring Boot Admin Server 的安全配置 TODO 芋艿:需要抽象出去
-                    .antMatchers(adminSeverContextPath).anonymous()
-                    .antMatchers(adminSeverContextPath + "/**").anonymous()
-                    // Spring Boot Actuator 的安全配置
-                    .antMatchers("/actuator").anonymous()
-                    .antMatchers("/actuator/**").anonymous()
-                    // Druid 监控 TODO 芋艿:需要抽象出去
-                    .antMatchers("/druid/**").anonymous()
-                    // 短信回调 API TODO 芋艿:需要抽象出去
-                    .antMatchers(api("/system/sms/callback/**")).anonymous()
-                    // 除上面外的所有请求全部需要鉴权认证
-                    .anyRequest().authenticated()
+                // 登陆的接口,可匿名访问
+                .antMatchers(api("/login")).anonymous()
+                // 通用的接口,可匿名访问 TODO 芋艿:需要抽象出去
+                .antMatchers(api("/system/captcha/**")).anonymous()
+                // 静态资源,可匿名访问
+                .antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll()
+                // 文件的获取接口,可匿名访问
+                .antMatchers(api("/infra/file/get/**")).anonymous()
+                // Swagger 接口文档
+                .antMatchers("/swagger-ui.html").anonymous()
+                .antMatchers("/favicon.ico").anonymous()
+                .antMatchers("/swagger-resources/**").anonymous()
+                .antMatchers("/webjars/**").anonymous()
+                .antMatchers("/*/api-docs").anonymous()
+                // Spring Boot Admin Server 的安全配置 TODO 芋艿:需要抽象出去
+                .antMatchers(adminSeverContextPath).anonymous()
+                .antMatchers(adminSeverContextPath + "/**").anonymous()
+                // Spring Boot Actuator 的安全配置
+                .antMatchers("/actuator").anonymous()
+                .antMatchers("/actuator/**").anonymous()
+                // Druid 监控 TODO 芋艿:需要抽象出去
+                .antMatchers("/druid/**").anonymous()
+                // 短信回调 API TODO 芋艿:需要抽象出去
+                .antMatchers(api("/system/sms/callback/**")).anonymous()
+                // oAuth2 auth2/login/gitee
+                .antMatchers(api("/auth2/login/**")).anonymous()
+                .antMatchers(api("/auth2/authorization/**")).anonymous()
+                .antMatchers("/api/callback/**").anonymous()
+                // 除上面外的所有请求全部需要鉴权认证
+                .anyRequest().authenticated()
                 .and()
                 .headers().frameOptions().disable();
         httpSecurity.logout().logoutUrl(api("/logout")).logoutSuccessHandler(logoutSuccessHandler);
         // 添加 JWT Filter
         httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
+
+//        // 放行第三方登录入口地址与第三方登录回调地址
+//        // @formatter:off
+//        httpSecurity.authorizeRequests()
+//                .antMatchers(HttpMethod.GET,
+//                        auth2Properties.getRedirectUrlPrefix() + "/*",
+//                        auth2Properties.getAuthLoginUrlPrefix() + "/*")
+//                .permitAll();
+//        httpSecurity.authorizeRequests()
+//                .antMatchers(HttpMethod.POST,
+//                        oneClickLoginProperties.getLoginProcessingUrl())
+//                .permitAll();
+//        // @formatter:on
+//        // ========= end: 使用 justAuth-spring-security-starter 必须步骤 =========
     }
 
     private String api(String url) {

+ 66 - 0
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/Auth2LoginUser.java

@@ -0,0 +1,66 @@
+package cn.iocoder.yudao.framework.security.core;
+
+import com.alibaba.fastjson.JSONObject;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import me.zhyd.oauth.enums.AuthUserGender;
+import me.zhyd.oauth.model.AuthToken;
+
+/**
+ * @author weir
+ */
+@Data
+public class Auth2LoginUser extends LoginUser {
+    /**
+     * 是否为临时注册的临时用户
+     */
+    private boolean isTempUser;
+    /**
+     * 用户第三方系统的唯一id。在调用方集成该组件时,可以用uuid + source唯一确定一个用户
+     */
+    private String thirdPartyUserId;
+    /**
+     * 用户昵称
+     */
+    private String nickname;
+    /**
+     * 用户头像
+     */
+    private String avatar;
+    /**
+     * 用户网址
+     */
+    private String blog;
+    /**
+     * 所在公司
+     */
+    private String company;
+    /**
+     * 位置
+     */
+    private String location;
+    /**
+     * 用户邮箱
+     */
+    private String email;
+    /**
+     * 用户备注(各平台中的用户个人介绍)
+     */
+    private String remark;
+    /**
+     * 性别
+     */
+    private AuthUserGender gender;
+    /**
+     * 用户来源
+     */
+    private String source;
+    /**
+     * 用户授权的token信息
+     */
+    private AuthToken token;
+    /**
+     * 第三方平台返回的原始用户信息
+     */
+    private JSONObject rawUserInfo;
+}

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

@@ -0,0 +1,61 @@
+/*
+ * 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 cn.iocoder.yudao.framework.security.core.LoginUser;
+import org.springframework.http.HttpStatus;
+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 org.springframework.security.web.savedrequest.SavedRequest;
+import top.dcenter.ums.security.core.oauth.userdetails.TemporaryUser;
+import top.dcenter.ums.security.core.vo.ResponseResult;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+import static org.springframework.util.StringUtils.hasText;
+import static top.dcenter.ums.security.core.oauth.util.MvcUtil.*;
+
+/**
+ * @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;
+    }
+}