Browse Source

增加 oauth2 的 scope 的校验方法,与使用示例

YunaiV 2 years ago
parent
commit
feff5aba07
12 changed files with 246 additions and 5 deletions
  1. 5 0
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java
  2. 1 1
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/TokenAuthenticationFilter.java
  3. 15 0
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkService.java
  4. 19 0
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkServiceImpl.java
  5. 5 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/dto/OAuth2AccessTokenCheckRespDTO.java
  6. 16 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenController.http
  7. 63 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenController.java
  8. 71 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/user/OAuth2OpenUserInfoRespVO.java
  9. 35 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/user/OAuth2OpenUserUpdateReqVO.java
  10. 0 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserProfileController.java
  11. 1 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/profile/UserProfileUpdateReqVO.java
  12. 15 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/oauth2/OAuth2OpenConvert.java

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

@@ -6,6 +6,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
 import lombok.Data;
 
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -30,6 +31,10 @@ public class LoginUser {
      * 租户编号
      */
     private Long tenantId;
+    /**
+     * 授权范围
+     */
+    private List<String> scopes;
 
     // ========== 上下文 ==========
     /**

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

@@ -79,7 +79,7 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
             }
             // 构建登录用户
             return new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType())
-                    .setTenantId(accessToken.getTenantId());
+                    .setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes());
         } catch (ServiceException serviceException) {
             // 校验 Token 不通过时,考虑到一些接口是无需登录的,所以直接返回 null 即可
             return null;

+ 15 - 0
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkService.java

@@ -41,4 +41,19 @@ public interface SecurityFrameworkService {
      */
     boolean hasAnyRoles(String... roles);
 
+    /**
+     * 判断是否有授权
+     *
+     * @param scope 授权
+     * @return 是否
+     */
+    boolean hasScope(String scope);
+
+    /**
+     * 判断是否有授权范围,任一一个即可
+     *
+     * @param scope 授权范围数组
+     * @return 是否
+     */
+    boolean hasAnyScopes(String... scope);
 }

+ 19 - 0
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkServiceImpl.java

@@ -1,8 +1,13 @@
 package cn.iocoder.yudao.framework.security.core.service;
 
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.security.core.LoginUser;
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
 import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
 import lombok.AllArgsConstructor;
 
+import java.util.Arrays;
+
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
 /**
@@ -35,4 +40,18 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService {
         return permissionApi.hasAnyRoles(getLoginUserId(), roles);
     }
 
+    @Override
+    public boolean hasScope(String scope) {
+        return hasAnyScopes(scope);
+    }
+
+    @Override
+    public boolean hasAnyScopes(String... scope) {
+        LoginUser user = SecurityFrameworkUtils.getLoginUser();
+        if (user == null) {
+            return false;
+        }
+        return CollUtil.containsAny(user.getScopes(), Arrays.asList(scope));
+    }
+
 }

+ 5 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/auth/dto/OAuth2AccessTokenCheckRespDTO.java

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.system.api.auth.dto;
 import lombok.Data;
 
 import java.io.Serializable;
+import java.util.List;
 
 /**
  * OAuth2.0 访问令牌的校验 Response DTO
@@ -24,5 +25,9 @@ public class OAuth2AccessTokenCheckRespDTO implements Serializable {
      * 租户编号
      */
     private Long tenantId;
+    /**
+     * 授权范围的数组
+     */
+    private List<String> scopes;
 
 }

+ 16 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenController.http

@@ -28,7 +28,7 @@ Content-Type: application/x-www-form-urlencoded
 Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw==
 tenant-id: {{adminTenentId}}
 
-grant_type=password&username=admin&password=admin123&scope=user_info
+grant_type=password&username=admin&password=admin123&scope=user.read
 
 ### 请求 /system/oauth2/token + refresh_token 接口 => 成功
 POST {{baseUrl}}/system/oauth2/token
@@ -47,3 +47,18 @@ tenant-id: {{adminTenentId}}
 POST {{baseUrl}}/system/oauth2/check-token?token=620d307c5b4148df8a98dd6c6c547106
 Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw==
 tenant-id: {{adminTenentId}}
+
+### 请求 /system/oauth2/user/get 接口 => 成功
+GET {{baseUrl}}/system/oauth2/user/get
+Authorization: Bearer 9502bd7a768a4ade920b90f41e2efd5c
+tenant-id: {{adminTenentId}}
+
+### 请求 /system/oauth2/user/update 接口 => 成功
+PUT {{baseUrl}}/system/oauth2/user/update
+Content-Type: application/json
+Authorization: Bearer 9502bd7a768a4ade920b90f41e2efd5c
+tenant-id: {{adminTenentId}}
+
+{
+  "nickname": "芋道源码"
+}

+ 63 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenController.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.system.controller.admin.oauth2;
 
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.lang.Assert;
 import cn.hutool.core.util.ArrayUtil;
 import cn.hutool.core.util.ObjectUtil;
@@ -11,25 +12,35 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
 import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAccessTokenRespVO;
 import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenCheckTokenRespVO;
+import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.user.OAuth2OpenUserInfoRespVO;
+import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO;
 import cn.iocoder.yudao.module.system.convert.oauth2.OAuth2OpenConvert;
 import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO;
 import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2ClientDO;
+import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO;
+import cn.iocoder.yudao.module.system.dal.dataobject.dept.PostDO;
+import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
 import cn.iocoder.yudao.module.system.enums.auth.OAuth2GrantTypeEnum;
+import cn.iocoder.yudao.module.system.service.dept.DeptService;
+import cn.iocoder.yudao.module.system.service.dept.PostService;
 import cn.iocoder.yudao.module.system.service.oauth2.OAuth2ApproveService;
 import cn.iocoder.yudao.module.system.service.oauth2.OAuth2ClientService;
 import cn.iocoder.yudao.module.system.service.oauth2.OAuth2GrantService;
 import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService;
+import cn.iocoder.yudao.module.system.service.user.AdminUserService;
 import cn.iocoder.yudao.module.system.util.oauth2.OAuth2Utils;
 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.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
 import javax.servlet.http.HttpServletRequest;
+import javax.validation.Valid;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -40,7 +51,19 @@ import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
-@Api(tags = "管理后台 - OAuth2.0 授权") // 提供给外部应用调用为主
+/**
+ * 提供给外部应用调用为主
+ *
+ * 一般来说,管理后台的 /system-api/* 是不直接提供给外部应用使用,主要是外部应用能够访问的数据与接口是有限的,而管理后台的 RBAC 无法很好的控制。
+ * 参考大量的开放平台,都是独立的一套 OpenAPI,对应到【本系统】就是在 Controller 下新建 open 包,实现 /open-api/* 接口,然后通过 scope 进行控制。
+ * 另外,一个公司如果有多个管理后台,它们 client_id 产生的 access token 相互之间是无法互通的,即无法访问它们系统的 API 接口,直到两个 client_id 产生信任授权。
+ *
+ * 考虑到【本系统】暂时不想做的过于复杂,默认只有获取到 access token 之后,可以访问【本系统】管理后台的 /system-api/* 所有接口,除非手动添加 scope 控制。
+ * scope 的使用示例,可见当前类的 getUserInfo 和 updateUserInfo 方法,上面有 @PreAuthorize("@ss.hasScope('user.read')") 和 @PreAuthorize("@ss.hasScope('user.write')") 注解
+ *
+ * @author 芋道源码
+ */
+@Api(tags = "管理后台 - OAuth2.0 授权")
 @RestController
 @RequestMapping("/system/oauth2")
 @Validated
@@ -291,4 +314,43 @@ public class OAuth2OpenController {
         return clientIdAndSecret;
     }
 
+    // ============ 用户操作的示例,展示 scope 的使用 ============
+
+    @Resource
+    private AdminUserService userService;
+    @Resource
+    private DeptService deptService;
+    @Resource
+    private PostService postService;
+
+    @GetMapping("/user/get")
+    @ApiOperation("获得用户基本信息")
+    @PreAuthorize("@ss.hasScope('user.read')")
+    public CommonResult<OAuth2OpenUserInfoRespVO> getUserInfo() {
+        // 获得用户基本信息
+        AdminUserDO user = userService.getUser(getLoginUserId());
+        OAuth2OpenUserInfoRespVO resp = OAuth2OpenConvert.INSTANCE.convert(user);
+        // 获得部门信息
+        if (user.getDeptId() != null) {
+            DeptDO dept = deptService.getDept(user.getDeptId());
+            resp.setDept(OAuth2OpenConvert.INSTANCE.convert(dept));
+        }
+        // 获得岗位信息
+        if (CollUtil.isNotEmpty(user.getPostIds())) {
+            List<PostDO> posts = postService.getPosts(user.getPostIds());
+            resp.setPosts(OAuth2OpenConvert.INSTANCE.convertList(posts));
+        }
+        return success(resp);
+    }
+
+    @PutMapping("/user/update")
+    @ApiOperation("更新用户基本信息")
+    @PreAuthorize("@ss.hasScope('user.write')")
+    public CommonResult<Boolean> updateUserInfo(@Valid @RequestBody UserProfileUpdateReqVO reqVO) {
+        // 这里将 UserProfileUpdateReqVO =》UserProfileUpdateReqVO 对象,实现接口的复用。
+        // 主要是,AdminUserService 没有自己的 BO 对象,所以复用只能这么做
+        userService.updateUserProfile(getLoginUserId(), OAuth2OpenConvert.INSTANCE.convert(reqVO));
+        return success(true);
+    }
+
 }

+ 71 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/user/OAuth2OpenUserInfoRespVO.java

@@ -0,0 +1,71 @@
+package cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.user;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@ApiModel("管理后台 - 【开放接口】获得用户基本信息 Response VO")
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class OAuth2OpenUserInfoRespVO {
+
+    @ApiModelProperty(value = "用户编号", required = true, example = "1")
+    private Long id;
+
+    @ApiModelProperty(value = "用户昵称", required = true, example = "芋艿")
+    private String username;
+
+    @ApiModelProperty(value = "用户昵称", required = true, example = "芋道")
+    private String nickname;
+
+    @ApiModelProperty(value = "用户邮箱", example = "yudao@iocoder.cn")
+    private String email;
+    @ApiModelProperty(value = "手机号码", example = "15601691300")
+    private String mobile;
+
+    @ApiModelProperty(value = "用户性别", example = "1", notes = "参见 SexEnum 枚举类")
+    private Integer sex;
+
+    @ApiModelProperty(value = "用户头像", example = "https://www.iocoder.cn/xxx.png")
+    private String avatar;
+
+    /**
+     * 所在部门
+     */
+    private Dept dept;
+
+    /**
+     * 所属岗位数组
+     */
+    private List<Post> posts;
+
+    @ApiModel("部门")
+    @Data
+    public static class Dept {
+
+        @ApiModelProperty(value = "部门编号", required = true, example = "1")
+        private Long id;
+
+        @ApiModelProperty(value = "部门名称", required = true, example = "研发部")
+        private String name;
+
+    }
+
+    @ApiModel("岗位")
+    @Data
+    public static class Post {
+
+        @ApiModelProperty(value = "岗位编号", required = true, example = "1")
+        private Long id;
+
+        @ApiModelProperty(value = "岗位名称", required = true, example = "开发")
+        private String name;
+
+    }
+
+}

+ 35 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/user/OAuth2OpenUserUpdateReqVO.java

@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.user;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.hibernate.validator.constraints.Length;
+
+import javax.validation.constraints.Email;
+import javax.validation.constraints.Size;
+
+@ApiModel("管理后台 - 【开放接口】更新用户基本信息 Request VO")
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class OAuth2OpenUserUpdateReqVO {
+
+    @ApiModelProperty(value = "用户昵称", required = true, example = "芋艿")
+    @Size(max = 30, message = "用户昵称长度不能超过 30 个字符")
+    private String nickname;
+
+    @ApiModelProperty(value = "用户邮箱", example = "yudao@iocoder.cn")
+    @Email(message = "邮箱格式不正确")
+    @Size(max = 50, message = "邮箱长度不能超过 50 个字符")
+    private String email;
+
+    @ApiModelProperty(value = "手机号码", example = "15601691300")
+    @Length(min = 11, max = 11, message = "手机号长度必须 11 位")
+    private String mobile;
+
+    @ApiModelProperty(value = "用户性别", example = "1", notes = "参见 SexEnum 枚举类")
+    private Integer sex;
+
+}

+ 0 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserProfileController.java

@@ -46,7 +46,6 @@ public class UserProfileController {
     private AdminUserService userService;
     @Resource
     private DeptService deptService;
-
     @Resource
     private PostService postService;
     @Resource

+ 1 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/profile/UserProfileUpdateReqVO.java

@@ -13,7 +13,7 @@ import javax.validation.constraints.Size;
 public class UserProfileUpdateReqVO {
 
     @ApiModelProperty(value = "用户昵称", required = true, example = "芋艿")
-    @Size(max = 30, message = "用户昵称长度不能超过30个字符")
+    @Size(max = 30, message = "用户昵称长度不能超过 30 个字符")
     private String nickname;
 
     @ApiModelProperty(value = "用户邮箱", example = "yudao@iocoder.cn")

+ 15 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/oauth2/OAuth2OpenConvert.java

@@ -4,11 +4,18 @@ import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
 import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
 import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAccessTokenRespVO;
 import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.OAuth2OpenCheckTokenRespVO;
+import cn.iocoder.yudao.module.system.controller.admin.oauth2.vo.open.user.OAuth2OpenUserInfoRespVO;
+import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO;
 import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO;
+import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO;
+import cn.iocoder.yudao.module.system.dal.dataobject.dept.PostDO;
+import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
 import cn.iocoder.yudao.module.system.util.oauth2.OAuth2Utils;
 import org.mapstruct.Mapper;
 import org.mapstruct.factory.Mappers;
 
+import java.util.List;
+
 @Mapper
 public interface OAuth2OpenConvert {
 
@@ -31,4 +38,12 @@ public interface OAuth2OpenConvert {
     }
     OAuth2OpenCheckTokenRespVO convert3(OAuth2AccessTokenDO bean);
 
+    // ============ 用户操作的示例 ============
+
+    OAuth2OpenUserInfoRespVO convert(AdminUserDO bean);
+    OAuth2OpenUserInfoRespVO.Dept convert(DeptDO dept);
+    List<OAuth2OpenUserInfoRespVO.Post> convertList(List<PostDO> list);
+
+    UserProfileUpdateReqVO convert(UserProfileUpdateReqVO bean);
+
 }