feat(用户管理): 添加用户角色功能并实现权限控制

- 在用户表中添加role字段并设置默认值为'USER'
- 前端添加isAdmin getter判断用户角色
- 后端实现角色字段的VO映射和默认值设置
- 为关键接口添加@PreAuthorize权限控制
- 移除图片控制器中冗余的权限检查代码
This commit is contained in:
ikmkj
2026-03-03 21:09:42 +08:00
parent 375ccb89ff
commit a4f95e7315
10 changed files with 29 additions and 69 deletions

View File

@@ -10,6 +10,7 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@@ -23,6 +24,7 @@ public class GroupingController {
private GroupingService groupingService;
@Operation(summary = "创建分组")
@PreAuthorize("hasRole('ADMIN')")
@PostMapping
public R<Grouping> createGrouping(@RequestBody Grouping grouping) {
if (ObjectUtil.isNull(grouping.getParentId())) {
@@ -47,6 +49,7 @@ public class GroupingController {
}
@Operation(summary = "更新分组名称")
@PreAuthorize("hasRole('ADMIN')")
@PutMapping("/{id}")
public R<Grouping> updateGrouping(
@PathVariable String id,
@@ -59,6 +62,7 @@ public class GroupingController {
}
@Operation(summary = "删除分组")
@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/{id}")
public R<Void> deleteGrouping(@PathVariable String id) {
Long idLong = Long.parseLong(id);

View File

@@ -66,15 +66,9 @@ public class ImageController {
}
@Operation(summary = "根据id删除图片")
@PreAuthorize("hasRole('ADMIN')")
@PostMapping("/{id}")
public R<Void> deleteImage(@PathVariable Long id) {
if (!SecurityUtil.isUserAuthenticated()) {
return R.fail("请先登录");
}
// 修复:添加权限验证,确保用户只能删除自己的图片
if (!canModifyImage(id)) {
return R.fail("无权删除此图片");
}
boolean result = imageService.deleteImage(id);
if (result) {
return R.success();
@@ -138,15 +132,9 @@ public class ImageController {
@Operation(summary = "根据url删除图片")
@PreAuthorize("hasRole('ADMIN')")
@PostMapping("/deleteByUrl")
public R<Void> deleteImageByUrl(@RequestParam String url) {
if (!SecurityUtil.isUserAuthenticated()) {
return R.fail("请先登录");
}
// 修复:添加权限验证
if (!canModifyImageByUrl(url)) {
return R.fail("无权删除此图片");
}
boolean result = imageService.deleteImageByUrl(url);
if (result) {
return R.success();
@@ -156,17 +144,9 @@ public class ImageController {
}
@Operation(summary = "根据url批量删除图片")
@PreAuthorize("hasRole('ADMIN')")
@PostMapping("/batch")
public R<Void> deleteImageByUrls(@RequestBody List<String> urls) {
if (!SecurityUtil.isUserAuthenticated()) {
return R.fail("请先登录");
}
// 修复:添加权限验证
for (String url : urls) {
if (!canModifyImageByUrl(url)) {
return R.fail("无权删除部分图片");
}
}
boolean result = imageService.deleteImageByUrls(urls);
if (result) {
return R.success();
@@ -175,49 +155,6 @@ public class ImageController {
}
}
/**
* 检查当前用户是否有权限操作图片
*/
private boolean canModifyImage(Long imageId) {
// 从数据库查询图片所属用户
Image image = imageService.getById(imageId);
if (image == null) {
return false;
}
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (!(principal instanceof UserDetails)) {
return false;
}
String username = ((UserDetails) principal).getUsername();
User user = userService.getOne(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<User>().eq("username", username));
if (user == null) {
return false;
}
return user.getId().equals(image.getUserId());
}
/**
* 检查当前用户是否有权限操作图片通过URL
*/
private boolean canModifyImageByUrl(String url) {
// 从数据库查询图片所属用户
Image image = imageService.getOne(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<Image>().eq("stored_name", url));
if (image == null) {
return false;
}
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (!(principal instanceof UserDetails)) {
return false;
}
String username = ((UserDetails) principal).getUsername();
User user = userService.getOne(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<User>().eq("username", username));
if (user == null) {
return false;
}
return user.getId().equals(image.getUserId());
}
private String getContentTypeFromFileExtension(String fileName) {
if (StrUtil.isBlank(fileName) || !StrUtil.contains(fileName, '.')) {
return "application/octet-stream";

View File

@@ -12,6 +12,7 @@ import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
@@ -55,6 +56,7 @@ public class MarkdownController {
@Operation(summary = "更新Markdown文件")
@PreAuthorize("hasRole('ADMIN')")
@PostMapping("/updateMarkdown")
public R<MarkdownFile> updateMarkdown(@RequestBody MarkdownFile markdownFile) {
MarkdownFile file = markdownFileService.updateMarkdownContent(markdownFile);
@@ -69,6 +71,7 @@ public class MarkdownController {
}
@Operation(summary = "删除Markdown文件")
@PreAuthorize("hasRole('ADMIN')")
@Parameters({
@Parameter(name = "id", description = "Markdown文件ID", required = true),
})
@@ -95,6 +98,7 @@ public class MarkdownController {
}
@Operation(summary = "更新Markdown文件标题")
@PreAuthorize("hasRole('ADMIN')")
@PostMapping("/{id}/title")
public R<MarkdownFile> updateMarkdownTitle(
@PathVariable Long id,

View File

@@ -7,6 +7,7 @@ import com.test.bijihoudaun.service.TrashService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@@ -26,6 +27,7 @@ public class TrashController {
}
@PostMapping("/restore/{type}/{id}")
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "恢复项目")
public R<Void> restoreItem(@PathVariable String type, @PathVariable String id) {
trashService.restoreItem(id, type);
@@ -33,6 +35,7 @@ public class TrashController {
}
@DeleteMapping("/permanently/{type}/{id}")
@PreAuthorize("hasRole('ADMIN')")
@RequireCaptcha("永久删除")
@Operation(summary = "永久删除项目")
public R<Void> permanentlyDeleteItem(@PathVariable String type, @PathVariable String id) {
@@ -41,10 +44,11 @@ public class TrashController {
}
@DeleteMapping("/clean")
@PreAuthorize("hasRole('ADMIN')")
@RequireCaptcha("清空回收站")
@Operation(summary = "清空回收站")
public R<Void> cleanTrash() {
trashService.cleanTrash();
return R.success();
}
}
}

View File

@@ -85,6 +85,8 @@ public class UserController {
userInfo.put("id", String.valueOf(user.getId()));
userInfo.put("username", user.getUsername());
userInfo.put("email", user.getEmail());
String role = user.getRole();
userInfo.put("role", (role != null && !role.isEmpty()) ? role : "USER");
result.put("userInfo", userInfo);
return R.success(result);

View File

@@ -21,6 +21,9 @@ public class UserVO implements Serializable {
@Schema(description = "邮箱")
private String email;
@Schema(description = "用户角色")
private String role;
@Schema(description = "用户创建时间")
private Date createdAt;

View File

@@ -87,6 +87,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
user.setUsername(username);
user.setPassword(encrypt);
user.setEmail(email);
user.setRole("USER"); // 设置默认角色
userMapper.insert(user);
return user;
}

View File

@@ -30,6 +30,8 @@ export const useUserStore = defineStore('user', {
},
getters: {
isLoggedIn: (state) => !!state.token,
// 添加:判断是否为管理员
isAdmin: (state) => state.userInfo?.role === 'ADMIN',
},
persist: {
enabled: true,
@@ -40,4 +42,4 @@ export const useUserStore = defineStore('user', {
}
],
},
});
});

View File

@@ -168,6 +168,7 @@ CREATE TABLE "user" (
"username" TEXT NOT NULL,
"password" TEXT NOT NULL,
"email" TEXT,
"role" TEXT DEFAULT 'USER',
"created_at" DATETIME DEFAULT CURRENT_TIMESTAMP,
"updated_at" DATETIME DEFAULT CURRENT_TIMESTAMP,
"token" TEXT,

View File

@@ -84,12 +84,14 @@ CREATE TABLE `user` (
`username` VARCHAR(50) NOT NULL,
`password` VARCHAR(255) NOT NULL,
`email` VARCHAR(100) DEFAULT NULL,
`role` VARCHAR(50) DEFAULT 'USER' COMMENT '用户角色ADMIN-管理员USER-普通用户',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`token` VARCHAR(255) DEFAULT NULL,
`token_enddata` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
UNIQUE KEY `uk_username` (`username`),
KEY `idx_user_role` (`role`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4;
SET FOREIGN_KEY_CHECKS = 1;