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

View File

@@ -66,15 +66,9 @@ public class ImageController {
} }
@Operation(summary = "根据id删除图片") @Operation(summary = "根据id删除图片")
@PreAuthorize("hasRole('ADMIN')")
@PostMapping("/{id}") @PostMapping("/{id}")
public R<Void> deleteImage(@PathVariable Long 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); boolean result = imageService.deleteImage(id);
if (result) { if (result) {
return R.success(); return R.success();
@@ -138,15 +132,9 @@ public class ImageController {
@Operation(summary = "根据url删除图片") @Operation(summary = "根据url删除图片")
@PreAuthorize("hasRole('ADMIN')")
@PostMapping("/deleteByUrl") @PostMapping("/deleteByUrl")
public R<Void> deleteImageByUrl(@RequestParam String url) { public R<Void> deleteImageByUrl(@RequestParam String url) {
if (!SecurityUtil.isUserAuthenticated()) {
return R.fail("请先登录");
}
// 修复:添加权限验证
if (!canModifyImageByUrl(url)) {
return R.fail("无权删除此图片");
}
boolean result = imageService.deleteImageByUrl(url); boolean result = imageService.deleteImageByUrl(url);
if (result) { if (result) {
return R.success(); return R.success();
@@ -156,17 +144,9 @@ public class ImageController {
} }
@Operation(summary = "根据url批量删除图片") @Operation(summary = "根据url批量删除图片")
@PreAuthorize("hasRole('ADMIN')")
@PostMapping("/batch") @PostMapping("/batch")
public R<Void> deleteImageByUrls(@RequestBody List<String> urls) { 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); boolean result = imageService.deleteImageByUrls(urls);
if (result) { if (result) {
return R.success(); 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) { private String getContentTypeFromFileExtension(String fileName) {
if (StrUtil.isBlank(fileName) || !StrUtil.contains(fileName, '.')) { if (StrUtil.isBlank(fileName) || !StrUtil.contains(fileName, '.')) {
return "application/octet-stream"; 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 io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.Date; import java.util.Date;
@@ -55,6 +56,7 @@ public class MarkdownController {
@Operation(summary = "更新Markdown文件") @Operation(summary = "更新Markdown文件")
@PreAuthorize("hasRole('ADMIN')")
@PostMapping("/updateMarkdown") @PostMapping("/updateMarkdown")
public R<MarkdownFile> updateMarkdown(@RequestBody MarkdownFile markdownFile) { public R<MarkdownFile> updateMarkdown(@RequestBody MarkdownFile markdownFile) {
MarkdownFile file = markdownFileService.updateMarkdownContent(markdownFile); MarkdownFile file = markdownFileService.updateMarkdownContent(markdownFile);
@@ -69,6 +71,7 @@ public class MarkdownController {
} }
@Operation(summary = "删除Markdown文件") @Operation(summary = "删除Markdown文件")
@PreAuthorize("hasRole('ADMIN')")
@Parameters({ @Parameters({
@Parameter(name = "id", description = "Markdown文件ID", required = true), @Parameter(name = "id", description = "Markdown文件ID", required = true),
}) })
@@ -95,6 +98,7 @@ public class MarkdownController {
} }
@Operation(summary = "更新Markdown文件标题") @Operation(summary = "更新Markdown文件标题")
@PreAuthorize("hasRole('ADMIN')")
@PostMapping("/{id}/title") @PostMapping("/{id}/title")
public R<MarkdownFile> updateMarkdownTitle( public R<MarkdownFile> updateMarkdownTitle(
@PathVariable Long id, @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.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List; import java.util.List;
@@ -26,6 +27,7 @@ public class TrashController {
} }
@PostMapping("/restore/{type}/{id}") @PostMapping("/restore/{type}/{id}")
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "恢复项目") @Operation(summary = "恢复项目")
public R<Void> restoreItem(@PathVariable String type, @PathVariable String id) { public R<Void> restoreItem(@PathVariable String type, @PathVariable String id) {
trashService.restoreItem(id, type); trashService.restoreItem(id, type);
@@ -33,6 +35,7 @@ public class TrashController {
} }
@DeleteMapping("/permanently/{type}/{id}") @DeleteMapping("/permanently/{type}/{id}")
@PreAuthorize("hasRole('ADMIN')")
@RequireCaptcha("永久删除") @RequireCaptcha("永久删除")
@Operation(summary = "永久删除项目") @Operation(summary = "永久删除项目")
public R<Void> permanentlyDeleteItem(@PathVariable String type, @PathVariable String id) { public R<Void> permanentlyDeleteItem(@PathVariable String type, @PathVariable String id) {
@@ -41,10 +44,11 @@ public class TrashController {
} }
@DeleteMapping("/clean") @DeleteMapping("/clean")
@PreAuthorize("hasRole('ADMIN')")
@RequireCaptcha("清空回收站") @RequireCaptcha("清空回收站")
@Operation(summary = "清空回收站") @Operation(summary = "清空回收站")
public R<Void> cleanTrash() { public R<Void> cleanTrash() {
trashService.cleanTrash(); trashService.cleanTrash();
return R.success(); return R.success();
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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