4.12 sa-token鉴权+重构前面的部分代码

This commit is contained in:
zhangsan 2025-04-12 19:47:05 +08:00
parent 41760e047d
commit 012e66bfc8
19 changed files with 854 additions and 15 deletions

17
pom.xml
View File

@ -79,6 +79,23 @@
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<!-- Sa-Token 权限认证 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.39.0</version>
</dependency>
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>1.39.0</version>
</dependency>
<!-- 提供Redis连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>

View File

@ -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);
}
}
}
}

View File

@ -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;
}
}

View File

@ -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<PictureVO> uploadPicture(
@RequestPart("file") MultipartFile multipartFile,
@ -68,6 +71,7 @@ public class PictureController {
* 通过 URL 上传图片可重新上传
*/
@PostMapping("/upload/url")
@SaSpaceCheckPermission(value = SpaceUserPermissionConstant.PICTURE_UPLOAD)
public BaseResponse<PictureVO> uploadPictureByUrl(
@RequestBody PictureUploadRequest pictureUploadRequest,
HttpServletRequest request) {
@ -84,6 +88,7 @@ public class PictureController {
* @return
*/
@PostMapping("/delete")
@SaSpaceCheckPermission(value = SpaceUserPermissionConstant.PICTURE_DELETE)
public BaseResponse<Boolean> 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<Boolean> 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<PictureVO> 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<Picture> picturePage = pictureService.page(new Page<>(current, size),
@ -280,6 +293,7 @@ public class PictureController {
* 以图搜图
*/
@PostMapping("/search/picture")
@SaSpaceCheckPermission(value = SpaceUserPermissionConstant.PICTURE_VIEW)
public BaseResponse<List<ImageSearchResult>> getSimilarPicture(@RequestBody SearchPictureByPictureRequest request) throws IOException {
ThrowUtils.throwIf(request == null, ErrorCode.NO_AUTH_ERROR);
List<ImageSearchResult> similarImage = pictureService.getSimilarPicture(request);
@ -290,6 +304,7 @@ public class PictureController {
* 按照颜色搜索
*/
@PostMapping("/search/color")
@SaSpaceCheckPermission(value = SpaceUserPermissionConstant.PICTURE_VIEW)
public BaseResponse<List<PictureVO>> 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<Boolean> 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<CreateOutPaintingTaskResponse> createPictureOutPaintingTask(@RequestBody CreatePictureOutPaintingTaskRequest createPictureOutPaintingTaskRequest,
HttpServletRequest request) {
if (createPictureOutPaintingTaskRequest == null || createPictureOutPaintingTaskRequest.getPictureId() == null) {

View File

@ -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<Long> 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<Boolean> 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<SpaceUser> 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<List<SpaceUserVO>> 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<Boolean> editSpaceUser(@RequestBody SpaceUserEditRequest spaceUserEditRequest,
HttpServletRequest request) {
if (spaceUserEditRequest == null || spaceUserEditRequest.getId() <= 0) {

View File

@ -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;
}

View File

@ -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<String> 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<String> getPermissionList(Space space, User loginUser) {
if (loginUser == null) {
return new ArrayList<>();
}
// 管理员权限
List<String> 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<>();
}
}

View File

@ -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<String> getPermissionList(Object loginId, String loginType) {
// 判断 loginType仅对类型为 "space" 进行权限校验
if (!StpKit.SPACE_TYPE.equals(loginType)) {
return new ArrayList<>();
}
// 管理员权限表示权限校验通过
List<String> 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<String> 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<String, String> 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);
}
}

View File

@ -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);
}

View File

@ -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;
/**
* 空间权限认证必须具有指定权限才能进入该方法
* <p> 可标注在函数类上效果等同于标注在此类的所有方法上
*/
@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;
/**
* 在权限校验不通过时的次要选择两者只要其一校验成功即可通过校验
*
* <p>
* 例1@SaCheckPermission(value="user-add", orRole="admin")
* 代表本次请求只要具有 user-add权限 admin角色 其一即可通过校验
* </p>
*
* <p>
* 例2 orRole = {"admin", "manager", "staff"}具有三个角色其一即可 <br>
* 例3 orRole = {"admin, manager, staff"}必须三个角色同时具备
* </p>
*
* @return /
*/
@AliasFor(annotation = SaCheckPermission.class)
String[] orRole() default {};
}

View File

@ -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);
};
}
}

View File

@ -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<SpaceUserPermission> permissions;
/**
* 角色列表
*/
private List<SpaceUserRole> roles;
private static final long serialVersionUID = 1L;
}

View File

@ -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;
}

View File

@ -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";
}

View File

@ -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<String> permissions;
/**
* 角色描述
*/
private String description;
private static final long serialVersionUID = 1L;
}

View File

@ -282,8 +282,8 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
&& !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<PictureMapper, Picture>
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);
// 操作数据库

View File

@ -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<UserMapper, User>
}
// 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);
}

View File

@ -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": "成员管理、查看图片、上传图片、修改图片、删除图片"
}
]
}

View File

@ -0,0 +1,6 @@
<html>
<body>
<h1>hello word!!!</h1>
<p>this is a html page</p>
</body>
</html>