diff --git a/backend/src/main/java/com/example/blog/common/JwtUtils.java b/backend/src/main/java/com/example/blog/common/JwtUtils.java new file mode 100644 index 0000000..c63053d --- /dev/null +++ b/backend/src/main/java/com/example/blog/common/JwtUtils.java @@ -0,0 +1,98 @@ +package com.example.blog.common; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +public class JwtUtils { + + private static final String SECRET = "XiaoDong-RuoYi-Secret-Token-Key-2026-SuperSecureJWT!@#"; + private static final long EXPIRATION_MS = 7L * 24 * 60 * 60 * 1000; // 7 天有效 + private static final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * 生成 JWT Token + */ + public static String createToken(Long userId, String username, String nickname, String role) { + try { + long now = System.currentTimeMillis(); + long exp = now + EXPIRATION_MS; + + Map header = new HashMap<>(); + header.put("alg", "HS256"); + header.put("typ", "JWT"); + + Map payload = new HashMap<>(); + payload.put("userId", userId); + payload.put("username", username); + payload.put("nickname", nickname); + payload.put("role", role); + payload.put("iat", now / 1000); + payload.put("exp", exp / 1000); + + String encodedHeader = Base64.getUrlEncoder().withoutPadding() + .encodeToString(objectMapper.writeValueAsString(header).getBytes(StandardCharsets.UTF_8)); + String encodedPayload = Base64.getUrlEncoder().withoutPadding() + .encodeToString(objectMapper.writeValueAsString(payload).getBytes(StandardCharsets.UTF_8)); + + String content = encodedHeader + "." + encodedPayload; + String signature = sign(content, SECRET); + + return content + "." + signature; + } catch (Exception e) { + throw new RuntimeException("生成 Token 失败", e); + } + } + + /** + * 解析并校验 Token,返回 Claims + */ + @SuppressWarnings("unchecked") + public static Map parseToken(String token) { + if (token == null || token.trim().isEmpty()) { + return null; + } + if (token.startsWith("Bearer ")) { + token = token.substring(7).trim(); + } + + String[] parts = token.split("\\."); + if (parts.length != 3) { + return null; + } + + try { + String content = parts[0] + "." + parts[1]; + String expectedSignature = sign(content, SECRET); + if (!expectedSignature.equals(parts[2])) { + return null; // 签名不匹配 + } + + byte[] payloadBytes = Base64.getUrlDecoder().decode(parts[1]); + Map claims = objectMapper.readValue(payloadBytes, Map.class); + + // 检查过期时间 + Number exp = (Number) claims.get("exp"); + if (exp != null && exp.longValue() * 1000 < System.currentTimeMillis()) { + return null; // Token 已过期 + } + + return claims; + } catch (Exception e) { + return null; + } + } + + private static String sign(String data, String secret) throws Exception { + Mac mac = Mac.getInstance("HmacSHA256"); + SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); + mac.init(secretKey); + byte[] rawHmac = mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); + return Base64.getUrlEncoder().withoutPadding().encodeToString(rawHmac); + } +} diff --git a/backend/src/main/java/com/example/blog/common/SecurityUtils.java b/backend/src/main/java/com/example/blog/common/SecurityUtils.java new file mode 100644 index 0000000..f99ec0e --- /dev/null +++ b/backend/src/main/java/com/example/blog/common/SecurityUtils.java @@ -0,0 +1,42 @@ +package com.example.blog.common; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.UUID; + +public class SecurityUtils { + + /** + * 生成随机盐值 + */ + public static String generateSalt() { + return UUID.randomUUID().toString().replace("-", "").substring(0, 16); + } + + /** + * SHA-256 + 盐值加密密码 (若依风格安全加盐散列) + */ + public static String encryptPassword(String password, String salt) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + String salted = password + "{" + salt + "}"; + byte[] bytes = md.digest(salted.getBytes(StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("SHA-256 算法未找到", e); + } + } + + /** + * 校验密码 + */ + public static boolean matchesPassword(String rawPassword, String encryptedPassword, String salt) { + String hash = encryptPassword(rawPassword, salt); + return hash.equalsIgnoreCase(encryptedPassword); + } +} diff --git a/backend/src/main/java/com/example/blog/config/DataInitializer.java b/backend/src/main/java/com/example/blog/config/DataInitializer.java index 8edc663..75ad0e6 100644 --- a/backend/src/main/java/com/example/blog/config/DataInitializer.java +++ b/backend/src/main/java/com/example/blog/config/DataInitializer.java @@ -1,9 +1,12 @@ package com.example.blog.config; +import com.example.blog.common.SecurityUtils; import com.example.blog.entity.Category; import com.example.blog.entity.Post; +import com.example.blog.entity.SysUser; import com.example.blog.repository.CategoryRepository; import com.example.blog.repository.PostRepository; +import com.example.blog.repository.SysUserRepository; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; @@ -15,14 +18,21 @@ public class DataInitializer implements CommandLineRunner { private final PostRepository postRepository; private final CategoryRepository categoryRepository; + private final SysUserRepository sysUserRepository; - public DataInitializer(PostRepository postRepository, CategoryRepository categoryRepository) { + public DataInitializer(PostRepository postRepository, + CategoryRepository categoryRepository, + SysUserRepository sysUserRepository) { this.postRepository = postRepository; this.categoryRepository = categoryRepository; + this.sysUserRepository = sysUserRepository; } @Override public void run(String... args) { + // 0. 初始化系统内置若依风格用户 (admin / admin123 与测试读者) + initSystemUsers(); + // 1. 初始化分类 if (categoryRepository.count() == 0) { List categories = Arrays.asList( @@ -113,4 +123,36 @@ public class DataInitializer implements CommandLineRunner { postRepository.saveAll(existingPosts); } } + + private void initSystemUsers() { + // 1. 初始化内置超级管理员 admin / admin123 + if (!sysUserRepository.existsByUsername("admin")) { + SysUser admin = new SysUser(); + admin.setUsername("admin"); + String salt = SecurityUtils.generateSalt(); + admin.setSalt(salt); + admin.setPassword(SecurityUtils.encryptPassword("admin123", salt)); + admin.setNickname("小东 (管理员)"); + admin.setEmail("j2570852518@gmail.com"); + admin.setAvatar("👨‍💻"); + admin.setRole("ADMIN"); + admin.setStatus("0"); + sysUserRepository.save(admin); + } + + // 2. 初始化内置测试访客读者 ry_user / user123 + if (!sysUserRepository.existsByUsername("ry_user")) { + SysUser visitor = new SysUser(); + visitor.setUsername("ry_user"); + String salt = SecurityUtils.generateSalt(); + visitor.setSalt(salt); + visitor.setPassword(SecurityUtils.encryptPassword("user123", salt)); + visitor.setNickname("若依读者"); + visitor.setEmail("visitor@example.com"); + visitor.setAvatar("🚀"); + visitor.setRole("USER"); + visitor.setStatus("0"); + sysUserRepository.save(visitor); + } + } } diff --git a/backend/src/main/java/com/example/blog/controller/AuthController.java b/backend/src/main/java/com/example/blog/controller/AuthController.java new file mode 100644 index 0000000..9446d98 --- /dev/null +++ b/backend/src/main/java/com/example/blog/controller/AuthController.java @@ -0,0 +1,59 @@ +package com.example.blog.controller; + +import com.example.blog.common.JwtUtils; +import com.example.blog.common.Result; +import com.example.blog.dto.LoginBody; +import com.example.blog.dto.RegisterBody; +import com.example.blog.service.SysUserService; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@RestController +@RequestMapping("/api/auth") +@CrossOrigin(origins = "*") +public class AuthController { + + private final SysUserService sysUserService; + + public AuthController(SysUserService sysUserService) { + this.sysUserService = sysUserService; + } + + @PostMapping("/login") + public Result> login(@RequestBody LoginBody loginBody, HttpServletRequest request) { + String ip = request.getHeader("X-Forwarded-For"); + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + return Result.success(sysUserService.login(loginBody, ip)); + } + + @PostMapping("/register") + public Result register(@RequestBody RegisterBody registerBody) { + sysUserService.register(registerBody); + return Result.success(); + } + + @GetMapping("/info") + public Result> getInfo(@RequestHeader(value = "Authorization", required = false) String authHeader) { + if (authHeader == null || authHeader.isEmpty()) { + return Result.error(401, "请先登录"); + } + Map claims = JwtUtils.parseToken(authHeader); + if (claims == null) { + return Result.error(401, "登录凭证无效或已过期,请重新登录"); + } + Number userIdNum = (Number) claims.get("userId"); + if (userIdNum == null) { + return Result.error(401, "用户凭证异常"); + } + return Result.success(sysUserService.getUserInfo(userIdNum.longValue())); + } + + @PostMapping("/logout") + public Result logout() { + return Result.success(); + } +} diff --git a/backend/src/main/java/com/example/blog/controller/CommentController.java b/backend/src/main/java/com/example/blog/controller/CommentController.java index 644ab5a..477e511 100644 --- a/backend/src/main/java/com/example/blog/controller/CommentController.java +++ b/backend/src/main/java/com/example/blog/controller/CommentController.java @@ -1,11 +1,13 @@ package com.example.blog.controller; +import com.example.blog.common.JwtUtils; import com.example.blog.common.Result; import com.example.blog.entity.Comment; import com.example.blog.service.CommentService; import org.springframework.web.bind.annotation.*; import java.util.List; +import java.util.Map; @RestController @RequestMapping("/api/posts/{postId}/comments") @@ -24,7 +26,24 @@ public class CommentController { } @PostMapping - public Result addComment(@PathVariable Long postId, @RequestBody Comment comment) { + public Result addComment(@PathVariable Long postId, + @RequestBody Comment comment, + @RequestHeader(value = "Authorization", required = false) String authHeader) { + if (authHeader != null && !authHeader.isEmpty()) { + Map claims = JwtUtils.parseToken(authHeader); + if (claims != null) { + String nickname = (String) claims.get("nickname"); + String role = (String) claims.get("role"); + if (nickname != null && !nickname.trim().isEmpty()) { + comment.setAuthor(nickname); + } + if (role != null) { + comment.setRole(role); + } + comment.setAvatar("ADMIN".equalsIgnoreCase(role) ? "👑" : "👨‍💻"); + } + } return Result.success(commentService.addComment(postId, comment)); } } + diff --git a/backend/src/main/java/com/example/blog/controller/PostController.java b/backend/src/main/java/com/example/blog/controller/PostController.java index f52876a..7f1ebd3 100644 --- a/backend/src/main/java/com/example/blog/controller/PostController.java +++ b/backend/src/main/java/com/example/blog/controller/PostController.java @@ -1,11 +1,13 @@ package com.example.blog.controller; +import com.example.blog.common.JwtUtils; import com.example.blog.common.Result; import com.example.blog.entity.Post; import com.example.blog.service.PostService; import org.springframework.web.bind.annotation.*; import java.util.List; +import java.util.Map; @RestController @RequestMapping("/api/posts") @@ -18,6 +20,20 @@ public class PostController { this.postService = postService; } + private void checkAdmin(String authHeader) { + if (authHeader == null || authHeader.isEmpty()) { + throw new RuntimeException("无权操作:请使用管理员账号登录"); + } + Map claims = JwtUtils.parseToken(authHeader); + if (claims == null) { + throw new RuntimeException("登录已失效,请重新登录"); + } + String role = (String) claims.get("role"); + if (!"ADMIN".equalsIgnoreCase(role)) { + throw new RuntimeException("无权操作:此功能仅限超级管理员使用"); + } + } + @GetMapping public Result> listPosts( @RequestParam(required = false) String category, @@ -31,17 +47,31 @@ public class PostController { } @PostMapping - public Result createPost(@RequestBody Post post) { + public Result createPost(@RequestBody Post post, + @RequestHeader(value = "Authorization", required = false) String authHeader) { + checkAdmin(authHeader); + Map claims = JwtUtils.parseToken(authHeader); + if (claims != null) { + String nickname = (String) claims.get("nickname"); + if (nickname != null && !nickname.trim().isEmpty()) { + post.setAuthor(nickname); + } + } return Result.success(postService.createPost(post)); } @PutMapping("/{id}") - public Result updatePost(@PathVariable Long id, @RequestBody Post post) { + public Result updatePost(@PathVariable Long id, + @RequestBody Post post, + @RequestHeader(value = "Authorization", required = false) String authHeader) { + checkAdmin(authHeader); return Result.success(postService.updatePost(id, post)); } @DeleteMapping("/{id}") - public Result deletePost(@PathVariable Long id) { + public Result deletePost(@PathVariable Long id, + @RequestHeader(value = "Authorization", required = false) String authHeader) { + checkAdmin(authHeader); postService.deletePost(id); return Result.success(); } @@ -51,3 +81,4 @@ public class PostController { return Result.success(postService.likePost(id)); } } + diff --git a/backend/src/main/java/com/example/blog/controller/SysUserController.java b/backend/src/main/java/com/example/blog/controller/SysUserController.java new file mode 100644 index 0000000..e05b948 --- /dev/null +++ b/backend/src/main/java/com/example/blog/controller/SysUserController.java @@ -0,0 +1,91 @@ +package com.example.blog.controller; + +import com.example.blog.common.JwtUtils; +import com.example.blog.common.Result; +import com.example.blog.dto.UpdateProfileBody; +import com.example.blog.dto.UpdatePwdBody; +import com.example.blog.entity.SysUser; +import com.example.blog.service.SysUserService; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/users") +@CrossOrigin(origins = "*") +public class SysUserController { + + private final SysUserService sysUserService; + + public SysUserController(SysUserService sysUserService) { + this.sysUserService = sysUserService; + } + + private Map checkAuth(String authHeader) { + if (authHeader == null || authHeader.isEmpty()) { + throw new RuntimeException("请先登录后操作"); + } + Map claims = JwtUtils.parseToken(authHeader); + if (claims == null) { + throw new RuntimeException("登录凭证已失效,请重新登录"); + } + return claims; + } + + private void checkAdmin(String authHeader) { + Map claims = checkAuth(authHeader); + String role = (String) claims.get("role"); + if (!"ADMIN".equalsIgnoreCase(role)) { + throw new RuntimeException("无权操作,此功能仅限超级管理员使用"); + } + } + + @GetMapping + public Result> listUsers(@RequestHeader(value = "Authorization", required = false) String authHeader) { + checkAdmin(authHeader); + return Result.success(sysUserService.listUsers()); + } + + @PostMapping + public Result createUser(@RequestHeader(value = "Authorization", required = false) String authHeader, + @RequestBody SysUser user) { + checkAdmin(authHeader); + return Result.success(sysUserService.createUser(user)); + } + + @PutMapping("/{id}/status") + public Result changeStatus(@RequestHeader(value = "Authorization", required = false) String authHeader, + @PathVariable Long id, + @RequestParam String status) { + checkAdmin(authHeader); + sysUserService.changeStatus(id, status); + return Result.success(); + } + + @DeleteMapping("/{id}") + public Result deleteUser(@RequestHeader(value = "Authorization", required = false) String authHeader, + @PathVariable Long id) { + checkAdmin(authHeader); + sysUserService.deleteUser(id); + return Result.success(); + } + + @PutMapping("/profile") + public Result updateProfile(@RequestHeader(value = "Authorization", required = false) String authHeader, + @RequestBody UpdateProfileBody body) { + Map claims = checkAuth(authHeader); + Number userId = (Number) claims.get("userId"); + sysUserService.updateProfile(userId.longValue(), body); + return Result.success(); + } + + @PutMapping("/password") + public Result updatePassword(@RequestHeader(value = "Authorization", required = false) String authHeader, + @RequestBody UpdatePwdBody body) { + Map claims = checkAuth(authHeader); + Number userId = (Number) claims.get("userId"); + sysUserService.updatePassword(userId.longValue(), body); + return Result.success(); + } +} diff --git a/backend/src/main/java/com/example/blog/dto/LoginBody.java b/backend/src/main/java/com/example/blog/dto/LoginBody.java new file mode 100644 index 0000000..aa1cb8d --- /dev/null +++ b/backend/src/main/java/com/example/blog/dto/LoginBody.java @@ -0,0 +1,11 @@ +package com.example.blog.dto; + +public class LoginBody { + private String username; + private String password; + + public String getUsername() { return username; } + public void setUsername(String username) { this.username = username; } + public String getPassword() { return password; } + public void setPassword(String password) { this.password = password; } +} diff --git a/backend/src/main/java/com/example/blog/dto/RegisterBody.java b/backend/src/main/java/com/example/blog/dto/RegisterBody.java new file mode 100644 index 0000000..cd7fa91 --- /dev/null +++ b/backend/src/main/java/com/example/blog/dto/RegisterBody.java @@ -0,0 +1,17 @@ +package com.example.blog.dto; + +public class RegisterBody { + private String username; + private String password; + private String nickname; + private String email; + + public String getUsername() { return username; } + public void setUsername(String username) { this.username = username; } + public String getPassword() { return password; } + public void setPassword(String password) { this.password = password; } + public String getNickname() { return nickname; } + public void setNickname(String nickname) { this.nickname = nickname; } + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } +} diff --git a/backend/src/main/java/com/example/blog/dto/UpdateProfileBody.java b/backend/src/main/java/com/example/blog/dto/UpdateProfileBody.java new file mode 100644 index 0000000..8afc18d --- /dev/null +++ b/backend/src/main/java/com/example/blog/dto/UpdateProfileBody.java @@ -0,0 +1,14 @@ +package com.example.blog.dto; + +public class UpdateProfileBody { + private String nickname; + private String email; + private String avatar; + + public String getNickname() { return nickname; } + public void setNickname(String nickname) { this.nickname = nickname; } + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + public String getAvatar() { return avatar; } + public void setAvatar(String avatar) { this.avatar = avatar; } +} diff --git a/backend/src/main/java/com/example/blog/dto/UpdatePwdBody.java b/backend/src/main/java/com/example/blog/dto/UpdatePwdBody.java new file mode 100644 index 0000000..e0f92a2 --- /dev/null +++ b/backend/src/main/java/com/example/blog/dto/UpdatePwdBody.java @@ -0,0 +1,11 @@ +package com.example.blog.dto; + +public class UpdatePwdBody { + private String oldPassword; + private String newPassword; + + public String getOldPassword() { return oldPassword; } + public void setOldPassword(String oldPassword) { this.oldPassword = oldPassword; } + public String getNewPassword() { return newPassword; } + public void setNewPassword(String newPassword) { this.newPassword = newPassword; } +} diff --git a/backend/src/main/java/com/example/blog/entity/Comment.java b/backend/src/main/java/com/example/blog/entity/Comment.java index 7074a26..0fd01cd 100644 --- a/backend/src/main/java/com/example/blog/entity/Comment.java +++ b/backend/src/main/java/com/example/blog/entity/Comment.java @@ -17,6 +17,12 @@ public class Comment { @Column(nullable = false, length = 50) private String author = "访客"; + @Column(length = 255) + private String avatar = "👨‍💻"; + + @Column(length = 20) + private String role = "USER"; + @Column(nullable = false, columnDefinition = "TEXT") private String content; @@ -43,6 +49,10 @@ public class Comment { public void setPostId(Long postId) { this.postId = postId; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } + public String getAvatar() { return avatar; } + public void setAvatar(String avatar) { this.avatar = avatar; } + public String getRole() { return role; } + public void setRole(String role) { this.role = role; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public LocalDateTime getCreatedAt() { return createdAt; } diff --git a/backend/src/main/java/com/example/blog/entity/SysUser.java b/backend/src/main/java/com/example/blog/entity/SysUser.java new file mode 100644 index 0000000..35631bf --- /dev/null +++ b/backend/src/main/java/com/example/blog/entity/SysUser.java @@ -0,0 +1,114 @@ +package com.example.blog.entity; + +import jakarta.persistence.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "sys_user") +public class SysUser { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true, length = 50) + private String username; + + @Column(nullable = false, length = 128) + private String password; + + @Column(length = 50) + private String nickname; + + @Column(length = 100) + private String email; + + @Column(length = 255) + private String avatar; + + @Column(nullable = false, length = 20) + private String role = "USER"; // ADMIN, USER + + @Column(nullable = false, length = 1) + private String status = "0"; // 0: 正常, 1: 停用 + + @Column(length = 64) + private String salt; + + @Column(name = "login_ip", length = 50) + private String loginIp; + + @Column(name = "login_date") + private LocalDateTime loginDate; + + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @PrePersist + public void prePersist() { + if (createdAt == null) createdAt = LocalDateTime.now(); + if (updatedAt == null) updatedAt = LocalDateTime.now(); + if (role == null) role = "USER"; + if (status == null) status = "0"; + if (nickname == null || nickname.trim().isEmpty()) nickname = username; + if (avatar == null) avatar = "👨‍💻"; + } + + @PreUpdate + public void preUpdate() { + updatedAt = LocalDateTime.now(); + } + + public SysUser() {} + + public SysUser(String username, String password, String nickname, String email, String role) { + this.username = username; + this.password = password; + this.nickname = nickname; + this.email = email; + this.role = role; + } + + // Getters and Setters + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getUsername() { return username; } + public void setUsername(String username) { this.username = username; } + + public String getPassword() { return password; } + public void setPassword(String password) { this.password = password; } + + public String getNickname() { return nickname; } + public void setNickname(String nickname) { this.nickname = nickname; } + + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + + public String getAvatar() { return avatar; } + public void setAvatar(String avatar) { this.avatar = avatar; } + + public String getRole() { return role; } + public void setRole(String role) { this.role = role; } + + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + + public String getSalt() { return salt; } + public void setSalt(String salt) { this.salt = salt; } + + public String getLoginIp() { return loginIp; } + public void setLoginIp(String loginIp) { this.loginIp = loginIp; } + + public LocalDateTime getLoginDate() { return loginDate; } + public void setLoginDate(LocalDateTime loginDate) { this.loginDate = loginDate; } + + public LocalDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } + + public LocalDateTime getUpdatedAt() { return updatedAt; } + public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; } +} diff --git a/backend/src/main/java/com/example/blog/repository/SysUserRepository.java b/backend/src/main/java/com/example/blog/repository/SysUserRepository.java new file mode 100644 index 0000000..63f87ac --- /dev/null +++ b/backend/src/main/java/com/example/blog/repository/SysUserRepository.java @@ -0,0 +1,13 @@ +package com.example.blog.repository; + +import com.example.blog.entity.SysUser; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface SysUserRepository extends JpaRepository { + Optional findByUsername(String username); + boolean existsByUsername(String username); +} diff --git a/backend/src/main/java/com/example/blog/service/SysUserService.java b/backend/src/main/java/com/example/blog/service/SysUserService.java new file mode 100644 index 0000000..0015f8c --- /dev/null +++ b/backend/src/main/java/com/example/blog/service/SysUserService.java @@ -0,0 +1,31 @@ +package com.example.blog.service; + +import com.example.blog.dto.LoginBody; +import com.example.blog.dto.RegisterBody; +import com.example.blog.dto.UpdateProfileBody; +import com.example.blog.dto.UpdatePwdBody; +import com.example.blog.entity.SysUser; + +import java.util.List; +import java.util.Map; + +public interface SysUserService { + + Map login(LoginBody loginBody, String ip); + + void register(RegisterBody registerBody); + + Map getUserInfo(Long userId); + + List listUsers(); + + void changeStatus(Long userId, String status); + + void updateProfile(Long userId, UpdateProfileBody body); + + void updatePassword(Long userId, UpdatePwdBody body); + + void deleteUser(Long userId); + + SysUser createUser(SysUser user); +} diff --git a/backend/src/main/java/com/example/blog/service/impl/SysUserServiceImpl.java b/backend/src/main/java/com/example/blog/service/impl/SysUserServiceImpl.java new file mode 100644 index 0000000..54511b2 --- /dev/null +++ b/backend/src/main/java/com/example/blog/service/impl/SysUserServiceImpl.java @@ -0,0 +1,226 @@ +package com.example.blog.service.impl; + +import com.example.blog.common.JwtUtils; +import com.example.blog.common.SecurityUtils; +import com.example.blog.dto.LoginBody; +import com.example.blog.dto.RegisterBody; +import com.example.blog.dto.UpdateProfileBody; +import com.example.blog.dto.UpdatePwdBody; +import com.example.blog.entity.SysUser; +import com.example.blog.repository.SysUserRepository; +import com.example.blog.service.SysUserService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.*; + +@Service +public class SysUserServiceImpl implements SysUserService { + + private final SysUserRepository sysUserRepository; + + public SysUserServiceImpl(SysUserRepository sysUserRepository) { + this.sysUserRepository = sysUserRepository; + } + + @Override + @Transactional + public Map login(LoginBody loginBody, String ip) { + if (loginBody.getUsername() == null || loginBody.getUsername().trim().isEmpty()) { + throw new IllegalArgumentException("用户名不能为空"); + } + if (loginBody.getPassword() == null || loginBody.getPassword().trim().isEmpty()) { + throw new IllegalArgumentException("密码不能为空"); + } + + SysUser user = sysUserRepository.findByUsername(loginBody.getUsername().trim()) + .orElseThrow(() -> new RuntimeException("用户名或密码错误")); + + if ("1".equals(user.getStatus())) { + throw new RuntimeException("对不起,该账号已被停用,请联系管理员"); + } + + if (!SecurityUtils.matchesPassword(loginBody.getPassword(), user.getPassword(), user.getSalt())) { + throw new RuntimeException("用户名或密码错误"); + } + + // 更新登录信息 + user.setLoginIp(ip != null ? ip : "127.0.0.1"); + user.setLoginDate(LocalDateTime.now()); + sysUserRepository.save(user); + + // 生成 RuoYi 风格 Token 与鉴权负载 + String token = JwtUtils.createToken(user.getId(), user.getUsername(), user.getNickname(), user.getRole()); + + Map result = new HashMap<>(); + result.put("token", token); + + Map safeUser = new HashMap<>(); + safeUser.put("id", user.getId()); + safeUser.put("username", user.getUsername()); + safeUser.put("nickname", user.getNickname()); + safeUser.put("email", user.getEmail()); + safeUser.put("avatar", user.getAvatar()); + safeUser.put("role", user.getRole()); + safeUser.put("status", user.getStatus()); + safeUser.put("loginDate", user.getLoginDate()); + result.put("user", safeUser); + + // 角色与权限标识 (若依标准格式) + result.put("roles", Collections.singletonList(user.getRole())); + if ("ADMIN".equalsIgnoreCase(user.getRole())) { + result.put("permissions", Collections.singletonList("*:*:*")); + } else { + result.put("permissions", Arrays.asList("post:view", "post:like", "comment:add")); + } + + return result; + } + + @Override + @Transactional + public void register(RegisterBody registerBody) { + String username = registerBody.getUsername() != null ? registerBody.getUsername().trim() : ""; + String password = registerBody.getPassword() != null ? registerBody.getPassword().trim() : ""; + + if (username.length() < 3 || username.length() > 20) { + throw new IllegalArgumentException("用户名长度必须在 3 到 20 个字符之间"); + } + if (password.length() < 5 || password.length() > 30) { + throw new IllegalArgumentException("密码长度必须在 5 到 30 个字符之间"); + } + + if (sysUserRepository.existsByUsername(username)) { + throw new RuntimeException("该用户名已被注册,请尝试其他用户名"); + } + + String salt = SecurityUtils.generateSalt(); + String encryptedPassword = SecurityUtils.encryptPassword(password, salt); + + SysUser user = new SysUser(); + user.setUsername(username); + user.setPassword(encryptedPassword); + user.setSalt(salt); + user.setNickname(registerBody.getNickname() != null && !registerBody.getNickname().trim().isEmpty() ? registerBody.getNickname().trim() : username); + user.setEmail(registerBody.getEmail() != null ? registerBody.getEmail().trim() : ""); + user.setAvatar("👨‍💻"); + user.setRole("USER"); + user.setStatus("0"); + + sysUserRepository.save(user); + } + + @Override + @Transactional(readOnly = true) + public Map getUserInfo(Long userId) { + SysUser user = sysUserRepository.findById(userId) + .orElseThrow(() -> new RuntimeException("用户不存在")); + + Map result = new HashMap<>(); + Map safeUser = new HashMap<>(); + safeUser.put("id", user.getId()); + safeUser.put("username", user.getUsername()); + safeUser.put("nickname", user.getNickname()); + safeUser.put("email", user.getEmail()); + safeUser.put("avatar", user.getAvatar()); + safeUser.put("role", user.getRole()); + safeUser.put("status", user.getStatus()); + safeUser.put("loginDate", user.getLoginDate()); + safeUser.put("createdAt", user.getCreatedAt()); + + result.put("user", safeUser); + result.put("roles", Collections.singletonList(user.getRole())); + if ("ADMIN".equalsIgnoreCase(user.getRole())) { + result.put("permissions", Collections.singletonList("*:*:*")); + } else { + result.put("permissions", Arrays.asList("post:view", "post:like", "comment:add")); + } + + return result; + } + + @Override + @Transactional(readOnly = true) + public List listUsers() { + List users = sysUserRepository.findAll(); + // 清除敏感字段 + for (SysUser u : users) { + u.setPassword(null); + u.setSalt(null); + } + return users; + } + + @Override + @Transactional + public void changeStatus(Long userId, String status) { + SysUser user = sysUserRepository.findById(userId) + .orElseThrow(() -> new RuntimeException("用户不存在")); + if ("admin".equalsIgnoreCase(user.getUsername())) { + throw new RuntimeException("不允许停用内置超级管理员账号"); + } + user.setStatus("1".equals(status) ? "1" : "0"); + sysUserRepository.save(user); + } + + @Override + @Transactional + public void updateProfile(Long userId, UpdateProfileBody body) { + SysUser user = sysUserRepository.findById(userId) + .orElseThrow(() -> new RuntimeException("用户不存在")); + if (body.getNickname() != null && !body.getNickname().trim().isEmpty()) { + user.setNickname(body.getNickname().trim()); + } + if (body.getEmail() != null) { + user.setEmail(body.getEmail().trim()); + } + if (body.getAvatar() != null && !body.getAvatar().trim().isEmpty()) { + user.setAvatar(body.getAvatar().trim()); + } + sysUserRepository.save(user); + } + + @Override + @Transactional + public void updatePassword(Long userId, UpdatePwdBody body) { + SysUser user = sysUserRepository.findById(userId) + .orElseThrow(() -> new RuntimeException("用户不存在")); + + if (!SecurityUtils.matchesPassword(body.getOldPassword(), user.getPassword(), user.getSalt())) { + throw new RuntimeException("原密码错误,修改失败"); + } + if (body.getNewPassword() == null || body.getNewPassword().length() < 5) { + throw new IllegalArgumentException("新密码长度不能少于 5 位"); + } + + String newSalt = SecurityUtils.generateSalt(); + String newHash = SecurityUtils.encryptPassword(body.getNewPassword(), newSalt); + user.setSalt(newSalt); + user.setPassword(newHash); + sysUserRepository.save(user); + } + + @Override + @Transactional + public void deleteUser(Long userId) { + SysUser user = sysUserRepository.findById(userId) + .orElseThrow(() -> new RuntimeException("用户不存在")); + if ("admin".equalsIgnoreCase(user.getUsername())) { + throw new RuntimeException("不允许删除内置超级管理员账号"); + } + sysUserRepository.deleteById(userId); + } + + @Override + @Transactional + public SysUser createUser(SysUser user) { + if (sysUserRepository.existsByUsername(user.getUsername())) { + throw new RuntimeException("用户名已存在"); + } + String salt = SecurityUtils.generateSalt(); + user.setSalt(salt); + user.setPassword(SecurityUtils.encryptPassword(user.getPassword(), salt)); + return sysUserRepository.save(user); + } +} diff --git a/frontend/index.html b/frontend/index.html index 4bdf01b..03d7d18 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -299,13 +299,69 @@ {{ backendConnected ? 'Spring Boot 3 + MySQL 8.0 在线' : '本地安全模式' }} - - + +
+ + + +
+
+
+ {{ currentUser.avatar || '👨‍💻' }} + {{ currentUser.nickname }} +
+
@{{ currentUser.username }}
+
{{ currentUser.email || '未绑定邮箱' }}
+
+ + + + + + + +
+ + +
+
+ + + + + + @@ -630,14 +701,20 @@ class="p-4 rounded-2xl bg-slate-50/60 dark:bg-slate-800/30 border border-slate-100 dark:border-slate-800 text-xs sm:text-sm">
-
- {{ c.author ? c.author.charAt(0).toUpperCase() : 'U' }} +
+ {{ c.avatar || (c.role === 'ADMIN' ? '👑' : '👨‍💻') }}
{{ c.author || '匿名访客' }} + + 👑 博主/管理员 + + + 读者 +
{{ formatDate(c.createdAt) }}
-

{{ c.content }}

+

{{ c.content }}

@@ -949,6 +1026,249 @@
+ +
+
+ +
+ +
+ RY +
+

小东全栈博客 · 认证中心

+

若依 (RuoYi) 规范 RBAC 权限与用户体系

+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+
⚡ 快捷填入内置测试账号:
+
+ + +
+
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+
+
+ + +
+
+
+
+ 👥 +
+

若依风格 · 用户权限管理中心 (RBAC)

+

管理全站注册用户、启用/停用状态及权限角色

+
+
+ +
+ + +
+
+
共 {{ userList.length }} 位注册用户
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
ID用户名昵称角色权限账号状态最近登录操作
{{ u.id }}{{ u.username }} + {{ u.avatar || '👨‍💻' }} + {{ u.nickname }} + + + {{ u.role === 'ADMIN' ? '👑 超级管理员' : '👤 访客读者' }} + + + + {{ u.status === '0' ? '正常' : '已停用' }} + + {{ u.loginDate ? formatDate(u.loginDate) : '未登录' }} + + 系统保护 +
+
+
+
+
+ + +
+
+
+

+ 👤 个人中心设置 +

+ +
+ + +
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+