From 012e66bfc8fc5238280589d35304741b4cc603d5 Mon Sep 17 00:00:00 2001 From: zhangsan <646228430@qq.com> Date: Sat, 12 Apr 2025 19:47:05 +0800 Subject: [PATCH] =?UTF-8?q?4.12=20sa-token=E9=89=B4=E6=9D=83+=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E5=89=8D=E9=9D=A2=E7=9A=84=E9=83=A8=E5=88=86=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 17 ++ .../config/HttpRequestWrapperFilter.java | 35 +++ .../config/RequestWrapper.java | 70 ++++++ .../controller/PictureController.java | 38 ++- .../controller/SpaceUserController.java | 7 + .../manager/auth/SpaceUserAuthContext.java | 48 ++++ .../manager/auth/SpaceUserAuthManager.java | 111 +++++++++ .../manager/auth/StpInterfaceImpl.java | 226 ++++++++++++++++++ .../manager/auth/StpKit.java | 25 ++ .../annotation/SaSpaceCheckPermission.java | 57 +++++ .../auth/annotation/SaTokenConfigure.java | 32 +++ .../auth/model/SpaceUserAuthConfig.java | 25 ++ .../auth/model/SpaceUserPermission.java | 30 +++ .../model/SpaceUserPermissionConstant.java | 32 +++ .../manager/auth/model/SpaceUserRole.java | 35 +++ .../service/impl/PictureServiceImpl.java | 8 +- .../service/impl/UserServiceImpl.java | 5 + .../resources/biz/spaceUserAuthConfig.json | 62 +++++ src/main/resources/static/index.html | 6 + 19 files changed, 854 insertions(+), 15 deletions(-) create mode 100644 src/main/java/edu/whut/smilepicturebackend/config/HttpRequestWrapperFilter.java create mode 100644 src/main/java/edu/whut/smilepicturebackend/config/RequestWrapper.java create mode 100644 src/main/java/edu/whut/smilepicturebackend/manager/auth/SpaceUserAuthContext.java create mode 100644 src/main/java/edu/whut/smilepicturebackend/manager/auth/SpaceUserAuthManager.java create mode 100644 src/main/java/edu/whut/smilepicturebackend/manager/auth/StpInterfaceImpl.java create mode 100644 src/main/java/edu/whut/smilepicturebackend/manager/auth/StpKit.java create mode 100644 src/main/java/edu/whut/smilepicturebackend/manager/auth/annotation/SaSpaceCheckPermission.java create mode 100644 src/main/java/edu/whut/smilepicturebackend/manager/auth/annotation/SaTokenConfigure.java create mode 100644 src/main/java/edu/whut/smilepicturebackend/manager/auth/model/SpaceUserAuthConfig.java create mode 100644 src/main/java/edu/whut/smilepicturebackend/manager/auth/model/SpaceUserPermission.java create mode 100644 src/main/java/edu/whut/smilepicturebackend/manager/auth/model/SpaceUserPermissionConstant.java create mode 100644 src/main/java/edu/whut/smilepicturebackend/manager/auth/model/SpaceUserRole.java create mode 100644 src/main/resources/biz/spaceUserAuthConfig.json create mode 100644 src/main/resources/static/index.html diff --git a/pom.xml b/pom.xml index ad99baa..c75be93 100644 --- a/pom.xml +++ b/pom.xml @@ -79,6 +79,23 @@ org.springframework.session spring-session-data-redis + + + cn.dev33 + sa-token-spring-boot-starter + 1.39.0 + + + + cn.dev33 + sa-token-redis-jackson + 1.39.0 + + + + org.apache.commons + commons-pool2 + org.springframework.boot spring-boot-configuration-processor diff --git a/src/main/java/edu/whut/smilepicturebackend/config/HttpRequestWrapperFilter.java b/src/main/java/edu/whut/smilepicturebackend/config/HttpRequestWrapperFilter.java new file mode 100644 index 0000000..31135af --- /dev/null +++ b/src/main/java/edu/whut/smilepicturebackend/config/HttpRequestWrapperFilter.java @@ -0,0 +1,35 @@ +package edu.whut.smilepicturebackend.config; + +import cn.hutool.http.ContentType; +import cn.hutool.http.Header; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +/** + * 请求包装过滤器 + * + * @author pine + */ +@Order(1) +@Component +public class HttpRequestWrapperFilter implements Filter { + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException { + if (request instanceof HttpServletRequest) { + HttpServletRequest servletRequest = (HttpServletRequest) request; + String contentType = servletRequest.getHeader(Header.CONTENT_TYPE.getValue()); + if (ContentType.JSON.getValue().equals(contentType)) { + // 可以再细粒度一些,只有需要进行空间权限校验的接口才需要包一层 + chain.doFilter(new RequestWrapper(servletRequest), response); + } else { + chain.doFilter(request, response); + } + } + } + +} \ No newline at end of file diff --git a/src/main/java/edu/whut/smilepicturebackend/config/RequestWrapper.java b/src/main/java/edu/whut/smilepicturebackend/config/RequestWrapper.java new file mode 100644 index 0000000..a176dce --- /dev/null +++ b/src/main/java/edu/whut/smilepicturebackend/config/RequestWrapper.java @@ -0,0 +1,70 @@ +package edu.whut.smilepicturebackend.config; + +import lombok.extern.slf4j.Slf4j; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.*; + +/** + * 包装请求,使 InputStream 可以重复读取 + * + * @author pine + */ +@Slf4j +public class RequestWrapper extends HttpServletRequestWrapper { + + private final String body; + + public RequestWrapper(HttpServletRequest request) { + super(request); + StringBuilder stringBuilder = new StringBuilder(); + try (InputStream inputStream = request.getInputStream(); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) { + char[] charBuffer = new char[128]; + int bytesRead = -1; + while ((bytesRead = bufferedReader.read(charBuffer)) > 0) { + stringBuilder.append(charBuffer, 0, bytesRead); + } + } catch (IOException ignored) { + } + body = stringBuilder.toString(); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes()); + return new ServletInputStream() { + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setReadListener(ReadListener readListener) { + } + + @Override + public int read() throws IOException { + return byteArrayInputStream.read(); + } + }; + + } + + @Override + public BufferedReader getReader() throws IOException { + return new BufferedReader(new InputStreamReader(this.getInputStream())); + } + + public String getBody() { + return this.body; + } + +} \ No newline at end of file diff --git a/src/main/java/edu/whut/smilepicturebackend/controller/PictureController.java b/src/main/java/edu/whut/smilepicturebackend/controller/PictureController.java index c6e0ec9..98a9c62 100644 --- a/src/main/java/edu/whut/smilepicturebackend/controller/PictureController.java +++ b/src/main/java/edu/whut/smilepicturebackend/controller/PictureController.java @@ -15,6 +15,9 @@ import edu.whut.smilepicturebackend.constant.UserConstant; import edu.whut.smilepicturebackend.exception.BusinessException; import edu.whut.smilepicturebackend.exception.ErrorCode; import edu.whut.smilepicturebackend.exception.ThrowUtils; +import edu.whut.smilepicturebackend.manager.auth.StpKit; +import edu.whut.smilepicturebackend.manager.auth.annotation.SaSpaceCheckPermission; +import edu.whut.smilepicturebackend.manager.auth.model.SpaceUserPermissionConstant; import edu.whut.smilepicturebackend.model.dto.picture.*; import edu.whut.smilepicturebackend.model.entity.Picture; import edu.whut.smilepicturebackend.model.entity.Space; @@ -48,12 +51,12 @@ public class PictureController { private final AliYunAiApi aliYunAiApi; - /** * 上传图片(可重新上传),前端选中图片就会调用该接口,无需前端点'创建'按钮! *TODO:目前有个bug,用户上传图片需要审核,会跳转到一个空白的图片详情页 */ @PostMapping("/upload") + @SaSpaceCheckPermission(value = SpaceUserPermissionConstant.PICTURE_UPLOAD) // @AuthCheck(mustRole = UserConstant.ADMIN_ROLE) public BaseResponse uploadPicture( @RequestPart("file") MultipartFile multipartFile, @@ -68,6 +71,7 @@ public class PictureController { * 通过 URL 上传图片(可重新上传) */ @PostMapping("/upload/url") + @SaSpaceCheckPermission(value = SpaceUserPermissionConstant.PICTURE_UPLOAD) public BaseResponse uploadPictureByUrl( @RequestBody PictureUploadRequest pictureUploadRequest, HttpServletRequest request) { @@ -84,6 +88,7 @@ public class PictureController { * @return */ @PostMapping("/delete") + @SaSpaceCheckPermission(value = SpaceUserPermissionConstant.PICTURE_DELETE) public BaseResponse deletePicture(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) { if (deleteRequest == null || deleteRequest.getId() <= 0) { @@ -98,6 +103,7 @@ public class PictureController { * 编辑图片(给用户使用)或创建图片时,编辑标签、分类的时候 */ @PostMapping("/edit") + @SaSpaceCheckPermission(value = SpaceUserPermissionConstant.PICTURE_EDIT) public BaseResponse editPicture(@RequestBody PictureEditRequest pictureEditRequest, HttpServletRequest request) { if (pictureEditRequest == null || pictureEditRequest.getId() <= 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR); @@ -159,6 +165,7 @@ public class PictureController { /** * 根据 id 获取图片(封装类) + * 这里不用sa-token的注解鉴权,因为它强制要求用户登录。故这里使用编程式注解 */ @GetMapping("/get/vo") public BaseResponse getPictureVOById(long id, HttpServletRequest request) { @@ -170,8 +177,11 @@ public class PictureController { // 空间权限校验 Long spaceId = picture.getSpaceId(); if (spaceId != null) { - User loginUser = userService.getLoginUser(request); - pictureService.checkPictureAuth(loginUser, picture); + boolean hasPermission = StpKit.SPACE.hasPermission(SpaceUserPermissionConstant.PICTURE_VIEW); //编程式鉴权 + ThrowUtils.throwIf(!hasPermission, ErrorCode.NO_AUTH_ERROR); + //User loginUser = userService.getLoginUser(request); + //改为使用注解鉴权 + //pictureService.checkPictureAuth(loginUser, picture); } // 获取封装类 return ResultUtils.success(pictureService.getPictureVO(picture, request)); @@ -210,12 +220,15 @@ public class PictureController { pictureQueryRequest.setNullSpaceId(true); } else { // 私有空间 - User loginUser = userService.getLoginUser(request); - Space space = spaceService.getById(spaceId); - ThrowUtils.throwIf(space == null, ErrorCode.NOT_FOUND_ERROR, "空间不存在"); - if (!loginUser.getId().equals(space.getUserId())) { - throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "没有空间权限"); - } + boolean hasPermission = StpKit.SPACE.hasPermission(SpaceUserPermissionConstant.PICTURE_VIEW); + ThrowUtils.throwIf(!hasPermission, ErrorCode.NO_AUTH_ERROR); + //已改为编程式鉴权 +// User loginUser = userService.getLoginUser(request); +// Space space = spaceService.getById(spaceId); +// ThrowUtils.throwIf(space == null, ErrorCode.NOT_FOUND_ERROR, "空间不存在"); +// if (!loginUser.getId().equals(space.getUserId())) { +// throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "没有空间权限"); +// } } // 查询数据库 Page picturePage = pictureService.page(new Page<>(current, size), @@ -280,6 +293,7 @@ public class PictureController { * 以图搜图 */ @PostMapping("/search/picture") + @SaSpaceCheckPermission(value = SpaceUserPermissionConstant.PICTURE_VIEW) public BaseResponse> getSimilarPicture(@RequestBody SearchPictureByPictureRequest request) throws IOException { ThrowUtils.throwIf(request == null, ErrorCode.NO_AUTH_ERROR); List similarImage = pictureService.getSimilarPicture(request); @@ -290,6 +304,7 @@ public class PictureController { * 按照颜色搜索 */ @PostMapping("/search/color") + @SaSpaceCheckPermission(value = SpaceUserPermissionConstant.PICTURE_VIEW) public BaseResponse> searchPictureByColor(@RequestBody SearchPictureByColorRequest searchPictureByColorRequest, HttpServletRequest request) { ThrowUtils.throwIf(searchPictureByColorRequest == null, ErrorCode.PARAMS_ERROR); String picColor = searchPictureByColorRequest.getPicColor(); @@ -300,9 +315,10 @@ public class PictureController { } /** - * 批量编辑图片 + * 对私人/团队空间批量编辑图片 */ @PostMapping("/edit/batch") + @SaSpaceCheckPermission(value = SpaceUserPermissionConstant.PICTURE_EDIT) public BaseResponse editPictureByBatch(@RequestBody PictureEditByBatchRequest pictureEditByBatchRequest, HttpServletRequest request) { ThrowUtils.throwIf(pictureEditByBatchRequest == null, ErrorCode.PARAMS_ERROR); User loginUser = userService.getLoginUser(request); @@ -317,7 +333,7 @@ public class PictureController { * @return */ @PostMapping("/out_painting/create_task") -// @SaSpaceCheckPermission(value = SpaceUserPermissionConstant.PICTURE_EDIT) + @SaSpaceCheckPermission(value = SpaceUserPermissionConstant.PICTURE_EDIT) public BaseResponse createPictureOutPaintingTask(@RequestBody CreatePictureOutPaintingTaskRequest createPictureOutPaintingTaskRequest, HttpServletRequest request) { if (createPictureOutPaintingTaskRequest == null || createPictureOutPaintingTaskRequest.getPictureId() == null) { diff --git a/src/main/java/edu/whut/smilepicturebackend/controller/SpaceUserController.java b/src/main/java/edu/whut/smilepicturebackend/controller/SpaceUserController.java index 09d91aa..145a6a2 100644 --- a/src/main/java/edu/whut/smilepicturebackend/controller/SpaceUserController.java +++ b/src/main/java/edu/whut/smilepicturebackend/controller/SpaceUserController.java @@ -7,6 +7,8 @@ import edu.whut.smilepicturebackend.common.ResultUtils; import edu.whut.smilepicturebackend.exception.BusinessException; import edu.whut.smilepicturebackend.exception.ErrorCode; import edu.whut.smilepicturebackend.exception.ThrowUtils; +import edu.whut.smilepicturebackend.manager.auth.annotation.SaSpaceCheckPermission; +import edu.whut.smilepicturebackend.manager.auth.model.SpaceUserPermissionConstant; import edu.whut.smilepicturebackend.model.dto.spaceuser.SpaceUserAddRequest; import edu.whut.smilepicturebackend.model.dto.spaceuser.SpaceUserEditRequest; import edu.whut.smilepicturebackend.model.dto.spaceuser.SpaceUserQueryRequest; @@ -42,6 +44,7 @@ public class SpaceUserController { * 添加成员到空间 */ @PostMapping("/add") + @SaSpaceCheckPermission(value = SpaceUserPermissionConstant.SPACE_USER_MANAGE) public BaseResponse addSpaceUser(@RequestBody SpaceUserAddRequest spaceUserAddRequest, HttpServletRequest request) { ThrowUtils.throwIf(spaceUserAddRequest == null, ErrorCode.PARAMS_ERROR); long id = spaceUserService.addSpaceUser(spaceUserAddRequest); @@ -52,6 +55,7 @@ public class SpaceUserController { * 从空间移除成员 */ @PostMapping("/delete") + @SaSpaceCheckPermission(value = SpaceUserPermissionConstant.SPACE_USER_MANAGE) public BaseResponse deleteSpaceUser(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) { if (deleteRequest == null || deleteRequest.getId() <= 0) { @@ -71,6 +75,7 @@ public class SpaceUserController { * 查询某个成员在某个空间的信息 */ @PostMapping("/get") + @SaSpaceCheckPermission(value = SpaceUserPermissionConstant.SPACE_USER_MANAGE) public BaseResponse getSpaceUser(@RequestBody SpaceUserQueryRequest spaceUserQueryRequest) { // 参数校验 ThrowUtils.throwIf(spaceUserQueryRequest == null, ErrorCode.PARAMS_ERROR); @@ -87,6 +92,7 @@ public class SpaceUserController { * 查询成员信息列表 */ @PostMapping("/list") + @SaSpaceCheckPermission(value = SpaceUserPermissionConstant.SPACE_USER_MANAGE) public BaseResponse> listSpaceUser(@RequestBody SpaceUserQueryRequest spaceUserQueryRequest, HttpServletRequest request) { ThrowUtils.throwIf(spaceUserQueryRequest == null, ErrorCode.PARAMS_ERROR); @@ -100,6 +106,7 @@ public class SpaceUserController { * 编辑成员信息(设置权限) */ @PostMapping("/edit") + @SaSpaceCheckPermission(value = SpaceUserPermissionConstant.SPACE_USER_MANAGE) public BaseResponse editSpaceUser(@RequestBody SpaceUserEditRequest spaceUserEditRequest, HttpServletRequest request) { if (spaceUserEditRequest == null || spaceUserEditRequest.getId() <= 0) { diff --git a/src/main/java/edu/whut/smilepicturebackend/manager/auth/SpaceUserAuthContext.java b/src/main/java/edu/whut/smilepicturebackend/manager/auth/SpaceUserAuthContext.java new file mode 100644 index 0000000..33010c4 --- /dev/null +++ b/src/main/java/edu/whut/smilepicturebackend/manager/auth/SpaceUserAuthContext.java @@ -0,0 +1,48 @@ +package edu.whut.smilepicturebackend.manager.auth; +import edu.whut.smilepicturebackend.model.entity.Picture; +import edu.whut.smilepicturebackend.model.entity.Space; +import edu.whut.smilepicturebackend.model.entity.SpaceUser; +import lombok.Data; + +/** + * SpaceUserAuthContext + * 表示用户在特定空间内的授权上下文,包括关联的图片、空间和用户信息。 + */ +@Data +public class SpaceUserAuthContext { + + /** + * 临时参数,不同请求对应的 id 可能不同 + */ + private Long id; + + /** + * 图片 ID + */ + private Long pictureId; + + /** + * 空间 ID + */ + private Long spaceId; + + /** + * 空间用户 ID + */ + private Long spaceUserId; + + /** + * 图片信息 + */ + private Picture picture; + + /** + * 空间信息 + */ + private Space space; + + /** + * 空间用户信息 + */ + private SpaceUser spaceUser; +} \ No newline at end of file diff --git a/src/main/java/edu/whut/smilepicturebackend/manager/auth/SpaceUserAuthManager.java b/src/main/java/edu/whut/smilepicturebackend/manager/auth/SpaceUserAuthManager.java new file mode 100644 index 0000000..893dd58 --- /dev/null +++ b/src/main/java/edu/whut/smilepicturebackend/manager/auth/SpaceUserAuthManager.java @@ -0,0 +1,111 @@ +package edu.whut.smilepicturebackend.manager.auth; + +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import edu.whut.smilepicturebackend.manager.auth.model.SpaceUserAuthConfig; +import edu.whut.smilepicturebackend.manager.auth.model.SpaceUserPermissionConstant; +import edu.whut.smilepicturebackend.manager.auth.model.SpaceUserRole; +import edu.whut.smilepicturebackend.model.entity.Space; +import edu.whut.smilepicturebackend.model.entity.SpaceUser; +import edu.whut.smilepicturebackend.model.entity.User; +import edu.whut.smilepicturebackend.model.enums.SpaceRoleEnum; +import edu.whut.smilepicturebackend.model.enums.SpaceTypeEnum; +import edu.whut.smilepicturebackend.service.SpaceUserService; +import edu.whut.smilepicturebackend.service.UserService; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * 空间成员权限管理 + */ +@Component +public class SpaceUserAuthManager { + + @Resource + private UserService userService; + + @Resource + private SpaceUserService spaceUserService; + + public static final SpaceUserAuthConfig SPACE_USER_AUTH_CONFIG; + + static { + String json = ResourceUtil.readUtf8Str("biz/spaceUserAuthConfig.json"); + SPACE_USER_AUTH_CONFIG = JSONUtil.toBean(json, SpaceUserAuthConfig.class); + } + + /** + * 根据角色获取权限列表 + * + * @param spaceUserRole + * @return + */ + public List getPermissionsByRole(String spaceUserRole) { + if (StrUtil.isBlank(spaceUserRole)) { + return new ArrayList<>(); + } + SpaceUserRole role = SPACE_USER_AUTH_CONFIG.getRoles() + .stream() + .filter(r -> r.getKey().equals(spaceUserRole)) + .findFirst() + .orElse(null); + if (role == null) { + return new ArrayList<>(); + } + return role.getPermissions(); + } + + + /** + * 获取权限列表 + * + * @param space + * @param loginUser + * @return + */ + public List getPermissionList(Space space, User loginUser) { + if (loginUser == null) { + return new ArrayList<>(); + } + // 管理员权限 + List ADMIN_PERMISSIONS = getPermissionsByRole(SpaceRoleEnum.ADMIN.getValue()); + // 公共图库 + if (space == null) { + if (userService.isAdmin(loginUser)) { + return ADMIN_PERMISSIONS; + } + return Collections.singletonList(SpaceUserPermissionConstant.PICTURE_VIEW); + } + SpaceTypeEnum spaceTypeEnum = SpaceTypeEnum.getEnumByValue(space.getSpaceType()); + if (spaceTypeEnum == null) { + return new ArrayList<>(); + } + // 根据空间获取对应的权限 + switch (spaceTypeEnum) { + case PRIVATE: + // 私有空间,仅本人或管理员有所有权限 + if (space.getUserId().equals(loginUser.getId()) || userService.isAdmin(loginUser)) { + return ADMIN_PERMISSIONS; + } else { + return new ArrayList<>(); + } + case TEAM: + // 团队空间,查询 SpaceUser 并获取角色和权限 + SpaceUser spaceUser = spaceUserService.lambdaQuery() + .eq(SpaceUser::getSpaceId, space.getId()) + .eq(SpaceUser::getUserId, loginUser.getId()) + .one(); + if (spaceUser == null) { + return new ArrayList<>(); + } else { + return getPermissionsByRole(spaceUser.getSpaceRole()); + } + } + return new ArrayList<>(); + } +} diff --git a/src/main/java/edu/whut/smilepicturebackend/manager/auth/StpInterfaceImpl.java b/src/main/java/edu/whut/smilepicturebackend/manager/auth/StpInterfaceImpl.java new file mode 100644 index 0000000..00f3607 --- /dev/null +++ b/src/main/java/edu/whut/smilepicturebackend/manager/auth/StpInterfaceImpl.java @@ -0,0 +1,226 @@ +package edu.whut.smilepicturebackend.manager.auth; + +import cn.dev33.satoken.stp.StpInterface; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.servlet.ServletUtil; +import cn.hutool.http.ContentType; +import cn.hutool.http.Header; +import cn.hutool.json.JSONUtil; + +import edu.whut.smilepicturebackend.exception.BusinessException; +import edu.whut.smilepicturebackend.exception.ErrorCode; +import edu.whut.smilepicturebackend.manager.auth.model.SpaceUserPermissionConstant; +import edu.whut.smilepicturebackend.model.entity.Picture; +import edu.whut.smilepicturebackend.model.entity.Space; +import edu.whut.smilepicturebackend.model.entity.SpaceUser; +import edu.whut.smilepicturebackend.model.entity.User; +import edu.whut.smilepicturebackend.model.enums.SpaceRoleEnum; +import edu.whut.smilepicturebackend.model.enums.SpaceTypeEnum; +import edu.whut.smilepicturebackend.service.PictureService; +import edu.whut.smilepicturebackend.service.SpaceService; +import edu.whut.smilepicturebackend.service.SpaceUserService; +import edu.whut.smilepicturebackend.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import javax.servlet.http.HttpServletRequest; +import java.util.*; + +import static edu.whut.smilepicturebackend.constant.UserConstant.USER_LOGIN_STATE; + + +/** + * 自定义权限加载接口实现类 + */ +@Component // 保证此类被 SpringBoot 扫描,完成 Sa-Token 的自定义权限验证扩展 +@RequiredArgsConstructor +public class StpInterfaceImpl implements StpInterface { + + // 默认是 /api + @Value("${server.servlet.context-path}") + private String contextPath; + + private final UserService userService; + + + private final SpaceService spaceService; + + + private final SpaceUserService spaceUserService; + + private final PictureService pictureService; + + + private final SpaceUserAuthManager spaceUserAuthManager; + + /** + * 返回一个账号所拥有的权限码集合 + */ + @Override + public List getPermissionList(Object loginId, String loginType) { + // 判断 loginType,仅对类型为 "space" 进行权限校验 + if (!StpKit.SPACE_TYPE.equals(loginType)) { + return new ArrayList<>(); + } + // 管理员权限,表示权限校验通过 + List ADMIN_PERMISSIONS = spaceUserAuthManager.getPermissionsByRole(SpaceRoleEnum.ADMIN.getValue()); + // 获取上下文对象 + SpaceUserAuthContext authContext = getAuthContextByRequest(); + // 如果所有字段都为空,表示查询公共图库,可以通过 + if (isAllFieldsNull(authContext)) { + return ADMIN_PERMISSIONS; + } + // 获取 userId 前面登录的时候把信息存进去过,现在取出来。 + User loginUser = (User) StpKit.SPACE.getSessionByLoginId(loginId).get(USER_LOGIN_STATE); + if (loginUser == null) { + throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "用户未登录"); + } + Long userId = loginUser.getId(); + // 优先从上下文中获取 SpaceUser 对象 + SpaceUser spaceUser = authContext.getSpaceUser(); + if (spaceUser != null) { + return spaceUserAuthManager.getPermissionsByRole(spaceUser.getSpaceRole()); + } + // 如果有 spaceUserId,必然是团队空间,通过数据库查询 SpaceUser 对象 + Long spaceUserId = authContext.getSpaceUserId(); + if (spaceUserId != null) { + spaceUser = spaceUserService.getById(spaceUserId); + if (spaceUser == null) { + throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "未找到空间用户信息"); + } + // 取出当前登录用户对应的 spaceUser + SpaceUser loginSpaceUser = spaceUserService.lambdaQuery() + .eq(SpaceUser::getSpaceId, spaceUser.getSpaceId()) + .eq(SpaceUser::getUserId, userId) + .one(); + if (loginSpaceUser == null) { + return new ArrayList<>(); + } + // 这里会导致管理员在私有空间没有权限,可以再查一次库处理 + return spaceUserAuthManager.getPermissionsByRole(loginSpaceUser.getSpaceRole()); + } + // 如果没有 spaceUserId,尝试通过 spaceId 或 pictureId 获取 Space 对象并处理 + Long spaceId = authContext.getSpaceId(); + if (spaceId == null) { + // 如果没有 spaceId,通过 pictureId 获取 Picture 对象和 Space 对象 + Long pictureId = authContext.getPictureId(); + // 图片 id 也没有,则默认通过权限校验 + if (pictureId == null) { + return ADMIN_PERMISSIONS; + } + Picture picture = pictureService.lambdaQuery() + .eq(Picture::getId, pictureId) + .select(Picture::getId, Picture::getSpaceId, Picture::getUserId) + .one(); + if (picture == null) { + throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "未找到图片信息"); + } + spaceId = picture.getSpaceId(); + // 公共图库,仅本人或管理员可操作 + if (spaceId == null) { + if (picture.getUserId().equals(userId) || userService.isAdmin(loginUser)) { + return ADMIN_PERMISSIONS; + } else { + // 不是自己的图片,仅可查看 + return Collections.singletonList(SpaceUserPermissionConstant.PICTURE_VIEW); + } + } + } + // 获取 Space 对象 + Space space = spaceService.getById(spaceId); + if (space == null) { + throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "未找到空间信息"); + } + // 根据 Space 类型判断权限 + if (space.getSpaceType() == SpaceTypeEnum.PRIVATE.getValue()) { + // 私有空间,仅本人或管理员有权限 + if (space.getUserId().equals(userId) || userService.isAdmin(loginUser)) { + return ADMIN_PERMISSIONS; + } else { + return new ArrayList<>(); + } + } else { + // 团队空间,查询 SpaceUser 并获取角色和权限 + spaceUser = spaceUserService.lambdaQuery() + .eq(SpaceUser::getSpaceId, spaceId) + .eq(SpaceUser::getUserId, userId) + .one(); + if (spaceUser == null) { + return new ArrayList<>(); + } + return spaceUserAuthManager.getPermissionsByRole(spaceUser.getSpaceRole()); + } + } + + /** + * 本项目中不使用。返回一个账号所拥有的角色标识集合 (权限与角色可分开校验) + */ + @Override + public List getRoleList(Object loginId, String loginType) { + return new ArrayList<>(); + } + + /** + * 从请求中获取上下文对象 + */ + private SpaceUserAuthContext getAuthContextByRequest() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); + String contentType = request.getHeader(Header.CONTENT_TYPE.getValue()); + SpaceUserAuthContext authRequest; + // 获取请求参数 + if (ContentType.JSON.getValue().equals(contentType)) { + String body = ServletUtil.getBody(request); + authRequest = JSONUtil.toBean(body, SpaceUserAuthContext.class); + } else { + Map paramMap = ServletUtil.getParamMap(request); + authRequest = BeanUtil.toBean(paramMap, SpaceUserAuthContext.class); + } + // 根据请求路径区分 id 字段的含义 + Long id = authRequest.getId(); + if (ObjUtil.isNotNull(id)) { + // 获取到请求路径的业务前缀,/api/picture/aaa?a=1 + String requestURI = request.getRequestURI(); + // 先替换掉上下文,剩下的就是前缀 + String partURI = requestURI.replace(contextPath + "/", ""); + // 获取前缀的第一个斜杠前的字符串 + String moduleName = StrUtil.subBefore(partURI, "/", false); + switch (moduleName) { + case "picture": + authRequest.setPictureId(id); + break; + case "spaceUser": + authRequest.setSpaceUserId(id); + break; + case "space": + authRequest.setSpaceId(id); + break; + default: + } + } + return authRequest; + } + + /** + * 判断对象的所有字段是否为空 + * + * @param object + * @return + */ + private boolean isAllFieldsNull(Object object) { + if (object == null) { + return true; // 对象本身为空 + } + // 获取所有字段并判断是否所有字段都为空 + return Arrays.stream(ReflectUtil.getFields(object.getClass())) + // 获取字段值 + .map(field -> ReflectUtil.getFieldValue(object, field)) + // 检查是否所有字段都为空 + .allMatch(ObjectUtil::isEmpty); + } +} diff --git a/src/main/java/edu/whut/smilepicturebackend/manager/auth/StpKit.java b/src/main/java/edu/whut/smilepicturebackend/manager/auth/StpKit.java new file mode 100644 index 0000000..1eebd3b --- /dev/null +++ b/src/main/java/edu/whut/smilepicturebackend/manager/auth/StpKit.java @@ -0,0 +1,25 @@ +package edu.whut.smilepicturebackend.manager.auth; + +import cn.dev33.satoken.stp.StpLogic; +import cn.dev33.satoken.stp.StpUtil; +import org.springframework.stereotype.Component; + +/** + * StpLogic 门面类,管理项目中所有的 StpLogic 账号体系 + * 添加 @Component 注解的目的是确保静态属性 DEFAULT 和 SPACE 被初始化 + */ +@Component +public class StpKit { + + public static final String SPACE_TYPE = "space"; + + /** + * 默认原生会话对象,项目中目前没使用到 + */ + public static final StpLogic DEFAULT = StpUtil.stpLogic; + + /** + * Space 会话对象,管理 Space 表所有账号的登录、权限认证 + */ + public static final StpLogic SPACE = new StpLogic(SPACE_TYPE); +} \ No newline at end of file diff --git a/src/main/java/edu/whut/smilepicturebackend/manager/auth/annotation/SaSpaceCheckPermission.java b/src/main/java/edu/whut/smilepicturebackend/manager/auth/annotation/SaSpaceCheckPermission.java new file mode 100644 index 0000000..164555c --- /dev/null +++ b/src/main/java/edu/whut/smilepicturebackend/manager/auth/annotation/SaSpaceCheckPermission.java @@ -0,0 +1,57 @@ +package edu.whut.smilepicturebackend.manager.auth.annotation; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.annotation.SaMode; + +import edu.whut.smilepicturebackend.manager.auth.StpKit; +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 空间权限认证:必须具有指定权限才能进入该方法 + *

可标注在函数、类上(效果等同于标注在此类的所有方法上) + */ +@SaCheckPermission(type = StpKit.SPACE_TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface SaSpaceCheckPermission { + + /** + * 需要校验的权限码 + * + * @return 需要校验的权限码 + */ + @AliasFor(annotation = SaCheckPermission.class) + String[] value() default {}; + + /** + * 验证模式:AND | OR,默认AND + * + * @return 验证模式 + */ + @AliasFor(annotation = SaCheckPermission.class) + SaMode mode() default SaMode.AND; + + /** + * 在权限校验不通过时的次要选择,两者只要其一校验成功即可通过校验 + * + *

+ * 例1:@SaCheckPermission(value="user-add", orRole="admin"), + * 代表本次请求只要具有 user-add权限 或 admin角色 其一即可通过校验。 + *

+ * + *

+ * 例2: orRole = {"admin", "manager", "staff"},具有三个角色其一即可。
+ * 例3: orRole = {"admin, manager, staff"},必须三个角色同时具备。 + *

+ * + * @return / + */ + @AliasFor(annotation = SaCheckPermission.class) + String[] orRole() default {}; + +} diff --git a/src/main/java/edu/whut/smilepicturebackend/manager/auth/annotation/SaTokenConfigure.java b/src/main/java/edu/whut/smilepicturebackend/manager/auth/annotation/SaTokenConfigure.java new file mode 100644 index 0000000..af36409 --- /dev/null +++ b/src/main/java/edu/whut/smilepicturebackend/manager/auth/annotation/SaTokenConfigure.java @@ -0,0 +1,32 @@ +package edu.whut.smilepicturebackend.manager.auth.annotation; + +import cn.dev33.satoken.interceptor.SaInterceptor; +import cn.dev33.satoken.strategy.SaAnnotationStrategy; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import javax.annotation.PostConstruct; + +/** + * Sa-Token 开启注解和配置 + */ +@Configuration +public class SaTokenConfigure implements WebMvcConfigurer { + + // 注册 Sa-Token 拦截器,打开注解式鉴权功能 + @Override + public void addInterceptors(InterceptorRegistry registry) { + // 注册 Sa-Token 拦截器,打开注解式鉴权功能 + registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**"); + } + + @PostConstruct + public void rewriteSaStrategy() { + // 重写Sa-Token的注解处理器,增加注解合并功能 + SaAnnotationStrategy.instance.getAnnotation = (element, annotationClass) -> { + return AnnotatedElementUtils.getMergedAnnotation(element, annotationClass); + }; + } +} \ No newline at end of file diff --git a/src/main/java/edu/whut/smilepicturebackend/manager/auth/model/SpaceUserAuthConfig.java b/src/main/java/edu/whut/smilepicturebackend/manager/auth/model/SpaceUserAuthConfig.java new file mode 100644 index 0000000..4341a03 --- /dev/null +++ b/src/main/java/edu/whut/smilepicturebackend/manager/auth/model/SpaceUserAuthConfig.java @@ -0,0 +1,25 @@ +package edu.whut.smilepicturebackend.manager.auth.model; + +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * 空间成员权限配置 + */ +@Data +public class SpaceUserAuthConfig implements Serializable { + + /** + * 权限列表 + */ + private List permissions; + + /** + * 角色列表 + */ + private List roles; + + private static final long serialVersionUID = 1L; +} \ No newline at end of file diff --git a/src/main/java/edu/whut/smilepicturebackend/manager/auth/model/SpaceUserPermission.java b/src/main/java/edu/whut/smilepicturebackend/manager/auth/model/SpaceUserPermission.java new file mode 100644 index 0000000..458b653 --- /dev/null +++ b/src/main/java/edu/whut/smilepicturebackend/manager/auth/model/SpaceUserPermission.java @@ -0,0 +1,30 @@ +package edu.whut.smilepicturebackend.manager.auth.model; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 空间成员权限 + */ +@Data +public class SpaceUserPermission implements Serializable { + + /** + * 权限键 + */ + private String key; + + /** + * 权限名称 + */ + private String name; + + /** + * 权限描述 + */ + private String description; + + private static final long serialVersionUID = 1L; + +} \ No newline at end of file diff --git a/src/main/java/edu/whut/smilepicturebackend/manager/auth/model/SpaceUserPermissionConstant.java b/src/main/java/edu/whut/smilepicturebackend/manager/auth/model/SpaceUserPermissionConstant.java new file mode 100644 index 0000000..b6d13d9 --- /dev/null +++ b/src/main/java/edu/whut/smilepicturebackend/manager/auth/model/SpaceUserPermissionConstant.java @@ -0,0 +1,32 @@ +package edu.whut.smilepicturebackend.manager.auth.model; + +/** + * 空间成员权限常量 + */ +public interface SpaceUserPermissionConstant { + + /** + * 空间用户管理权限 + */ + String SPACE_USER_MANAGE = "spaceUser:manage"; + + /** + * 图片查看权限 + */ + String PICTURE_VIEW = "picture:view"; + + /** + * 图片上传权限 + */ + String PICTURE_UPLOAD = "picture:upload"; + + /** + * 图片编辑权限 + */ + String PICTURE_EDIT = "picture:edit"; + + /** + * 图片删除权限 + */ + String PICTURE_DELETE = "picture:delete"; +} \ No newline at end of file diff --git a/src/main/java/edu/whut/smilepicturebackend/manager/auth/model/SpaceUserRole.java b/src/main/java/edu/whut/smilepicturebackend/manager/auth/model/SpaceUserRole.java new file mode 100644 index 0000000..dd0e641 --- /dev/null +++ b/src/main/java/edu/whut/smilepicturebackend/manager/auth/model/SpaceUserRole.java @@ -0,0 +1,35 @@ +package edu.whut.smilepicturebackend.manager.auth.model; + +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * 空间成员角色 + */ +@Data +public class SpaceUserRole implements Serializable { + + /** + * 角色键 + */ + private String key; + + /** + * 角色名称 + */ + private String name; + + /** + * 权限键列表 + */ + private List permissions; + + /** + * 角色描述 + */ + private String description; + + private static final long serialVersionUID = 1L; +} \ No newline at end of file diff --git a/src/main/java/edu/whut/smilepicturebackend/service/impl/PictureServiceImpl.java b/src/main/java/edu/whut/smilepicturebackend/service/impl/PictureServiceImpl.java index 8ab359d..f965b54 100644 --- a/src/main/java/edu/whut/smilepicturebackend/service/impl/PictureServiceImpl.java +++ b/src/main/java/edu/whut/smilepicturebackend/service/impl/PictureServiceImpl.java @@ -282,8 +282,8 @@ public class PictureServiceImpl extends ServiceImpl && !userService.isAdmin(loginUser)) { throw new BusinessException(ErrorCode.NO_AUTH_ERROR); } - // 校验权限 - checkPictureAuth(loginUser, oldPicture); + // 校验权限,已改为注解鉴权 +// checkPictureAuth(loginUser, oldPicture); // 开启事务 transactionTemplate.execute(status -> { // 操作数据库 @@ -317,8 +317,8 @@ public class PictureServiceImpl extends ServiceImpl long id = pictureEditRequest.getId(); Picture oldPicture = this.getById(id); ThrowUtils.throwIf(oldPicture == null, ErrorCode.NOT_FOUND_ERROR); - // 校验权限 - checkPictureAuth(loginUser, oldPicture); + // 校验权限,已改为注解鉴权 +// checkPictureAuth(loginUser, oldPicture); // 补充审核 参数,每次编辑图片都要重新过审 this.fillReviewParams(picture, loginUser); // 操作数据库 diff --git a/src/main/java/edu/whut/smilepicturebackend/service/impl/UserServiceImpl.java b/src/main/java/edu/whut/smilepicturebackend/service/impl/UserServiceImpl.java index ccfec7c..3b6edf9 100644 --- a/src/main/java/edu/whut/smilepicturebackend/service/impl/UserServiceImpl.java +++ b/src/main/java/edu/whut/smilepicturebackend/service/impl/UserServiceImpl.java @@ -14,6 +14,7 @@ import edu.whut.smilepicturebackend.constant.UserConstant; import edu.whut.smilepicturebackend.exception.BusinessException; import edu.whut.smilepicturebackend.exception.ErrorCode; import edu.whut.smilepicturebackend.exception.ThrowUtils; +import edu.whut.smilepicturebackend.manager.auth.StpKit; import edu.whut.smilepicturebackend.model.dto.user.UserAddRequest; import edu.whut.smilepicturebackend.model.dto.user.UserQueryRequest; import edu.whut.smilepicturebackend.model.entity.User; @@ -127,6 +128,10 @@ public class UserServiceImpl extends ServiceImpl } // 4. 保存用户的登录态 request.getSession().setAttribute(UserConstant.USER_LOGIN_STATE, user); + // 记录用户登录态到 Sa-token,便于空间鉴权时使用,注意保证该用户信息与 SpringSession 中的信息过期时间一致 + + StpKit.SPACE.login(user.getId()); + StpKit.SPACE.getSession().set(UserConstant.USER_LOGIN_STATE, user); return this.getLoginUserVO(user); } diff --git a/src/main/resources/biz/spaceUserAuthConfig.json b/src/main/resources/biz/spaceUserAuthConfig.json new file mode 100644 index 0000000..0b49c6f --- /dev/null +++ b/src/main/resources/biz/spaceUserAuthConfig.json @@ -0,0 +1,62 @@ +{ + "permissions": [ + { + "key": "spaceUser:manage", + "name": "成员管理", + "description": "管理空间成员,添加或移除成员" + }, + { + "key": "picture:view", + "name": "查看图片", + "description": "查看空间中的图片内容" + }, + { + "key": "picture:upload", + "name": "上传图片", + "description": "上传图片到空间中" + }, + { + "key": "picture:edit", + "name": "修改图片", + "description": "编辑已上传的图片信息" + }, + { + "key": "picture:delete", + "name": "删除图片", + "description": "删除空间中的图片" + } + ], + "roles": [ + { + "key": "viewer", + "name": "浏览者", + "permissions": [ + "picture:view" + ], + "description": "查看图片" + }, + { + "key": "editor", + "name": "编辑者", + "permissions": [ + "picture:view", + "picture:upload", + "picture:edit", + "picture:delete" + ], + "description": "查看图片、上传图片、修改图片、删除图片" + }, + { + "key": "admin", + "name": "管理员", + "permissions": [ + "spaceUser:manage", + "picture:view", + "picture:upload", + "picture:edit", + "picture:delete" + ], + "description": "成员管理、查看图片、上传图片、修改图片、删除图片" + } + ] +} \ No newline at end of file diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html new file mode 100644 index 0000000..89bb8ba --- /dev/null +++ b/src/main/resources/static/index.html @@ -0,0 +1,6 @@ + + +

hello word!!!

+

this is a html page

+ + \ No newline at end of file