getAllUsers() {
+ return usersService.getAllList();
}
+}
+
}
diff --git a/src/main/java/com/yaoyuan/jiscuss/dto/UserCreateRequest.java b/src/main/java/com/yaoyuan/jiscuss/dto/UserCreateRequest.java
new file mode 100644
index 0000000..c5326a1
--- /dev/null
+++ b/src/main/java/com/yaoyuan/jiscuss/dto/UserCreateRequest.java
@@ -0,0 +1,31 @@
+package com.yaoyuan.jiscuss.dto;
+
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+
+/**
+ * Request payload for creating a new user account.
+ * Validated at the controller boundary before reaching the service layer.
+ */
+public record UserCreateRequest(
+
+ @NotBlank(message = "用户名不能为空")
+ @Size(min = 3, max = 50, message = "用户名长度必须在 3-50 个字符之间")
+ String username,
+
+ /**
+ * Raw password — will be BCrypt-hashed before persistence.
+ * Never log or return this field.
+ */
+ @NotBlank(message = "密码不能为空")
+ @Size(min = 8, max = 100, message = "密码长度至少 8 个字符")
+ String password,
+
+ @Email(message = "邮箱格式不正确")
+ @Size(max = 100)
+ String email,
+
+ @Size(max = 50)
+ String realname
+) {}
diff --git a/src/main/java/com/yaoyuan/jiscuss/dto/UserMapper.java b/src/main/java/com/yaoyuan/jiscuss/dto/UserMapper.java
new file mode 100644
index 0000000..ea9eb80
--- /dev/null
+++ b/src/main/java/com/yaoyuan/jiscuss/dto/UserMapper.java
@@ -0,0 +1,44 @@
+package com.yaoyuan.jiscuss.dto;
+
+import com.yaoyuan.jiscuss.entity.User;
+import org.springframework.stereotype.Component;
+
+/**
+ * Manual mapper between {@link User} entity and DTO objects.
+ *
+ * Intentionally excludes the {@code password} field from all output DTOs.
+ * Replace with a generated MapStruct mapper if mapping complexity grows.
+ */
+@Component
+public class UserMapper {
+
+ /** Map a {@link User} entity to a safe {@link UserResponse} (no password). */
+ public UserResponse toResponse(User user) {
+ if (user == null) return null;
+ return new UserResponse(
+ user.getId(),
+ user.getUsername(),
+ user.getRealname(),
+ user.getEmail(),
+ user.getAvatar(),
+ user.getGender(),
+ user.getPhone(),
+ user.getAge(),
+ user.getDiscussionsCount(),
+ user.getCommentsCount(),
+ user.getJoinTime(),
+ user.getLastSeenTime(),
+ user.getLevel()
+ );
+ }
+
+ /** Map a {@link UserCreateRequest} to a new {@link User} entity (password left unset — caller must hash it). */
+ public User fromCreateRequest(UserCreateRequest request) {
+ User user = new User();
+ user.setUsername(request.username());
+ user.setEmail(request.email());
+ user.setRealname(request.realname());
+ // Caller is responsible for BCrypt-hashing the password before calling usersService.insert()
+ return user;
+ }
+}
diff --git a/src/main/java/com/yaoyuan/jiscuss/dto/UserResponse.java b/src/main/java/com/yaoyuan/jiscuss/dto/UserResponse.java
new file mode 100644
index 0000000..ae353ac
--- /dev/null
+++ b/src/main/java/com/yaoyuan/jiscuss/dto/UserResponse.java
@@ -0,0 +1,23 @@
+package com.yaoyuan.jiscuss.dto;
+
+import java.util.Date;
+
+/**
+ * Read-only user projection exposed to the frontend / API consumers.
+ * Never exposes the {@code password} field.
+ */
+public record UserResponse(
+ Integer id,
+ String username,
+ String realname,
+ String email,
+ String avatar,
+ String gender,
+ String phone,
+ Integer age,
+ Integer discussionsCount,
+ Integer commentsCount,
+ Date joinTime,
+ Date lastSeenTime,
+ Integer level
+) {}
diff --git a/src/main/java/com/yaoyuan/jiscuss/entity/Discussion.java b/src/main/java/com/yaoyuan/jiscuss/entity/Discussion.java
index e29cdbc..4a28940 100644
--- a/src/main/java/com/yaoyuan/jiscuss/entity/Discussion.java
+++ b/src/main/java/com/yaoyuan/jiscuss/entity/Discussion.java
@@ -1,13 +1,15 @@
package com.yaoyuan.jiscuss.entity;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
import lombok.Data;
-import javax.persistence.Column;
-import javax.persistence.Entity;
-import javax.persistence.GeneratedValue;
-import javax.persistence.GenerationType;
-import javax.persistence.Id;
-import javax.persistence.Table;
import java.io.Serializable;
import java.util.Date;
@@ -22,6 +24,8 @@ public class Discussion implements Serializable {
@Column(name = "id", unique = true)
private Integer id;
+ @NotBlank
+ @Size(max = 200)
@Column(name = "title")
private String title;
@@ -64,7 +68,7 @@ public class Discussion implements Serializable {
@Column(name = "like_count")
private Integer likeCount;
-
+ /** IP is resolved server-side via IpUtils.getClientIp() — never trusted from X-Forwarded-For alone. */
@Column(name = "ip_address")
private String ipAddress;
@@ -73,5 +77,5 @@ public class Discussion implements Serializable {
@Column(name = "create_time")
private Date createTime;
-
}
+
diff --git a/src/main/java/com/yaoyuan/jiscuss/entity/DiscussionTag.java b/src/main/java/com/yaoyuan/jiscuss/entity/DiscussionTag.java
index b272be9..084dff0 100644
--- a/src/main/java/com/yaoyuan/jiscuss/entity/DiscussionTag.java
+++ b/src/main/java/com/yaoyuan/jiscuss/entity/DiscussionTag.java
@@ -1,13 +1,13 @@
package com.yaoyuan.jiscuss.entity;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
import lombok.Data;
-import javax.persistence.Column;
-import javax.persistence.Entity;
-import javax.persistence.GeneratedValue;
-import javax.persistence.GenerationType;
-import javax.persistence.Id;
-import javax.persistence.Table;
import java.io.Serializable;
@Data
diff --git a/src/main/java/com/yaoyuan/jiscuss/entity/LikeCollect.java b/src/main/java/com/yaoyuan/jiscuss/entity/LikeCollect.java
index b7bce26..4f3acde 100644
--- a/src/main/java/com/yaoyuan/jiscuss/entity/LikeCollect.java
+++ b/src/main/java/com/yaoyuan/jiscuss/entity/LikeCollect.java
@@ -1,22 +1,18 @@
package com.yaoyuan.jiscuss.entity;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
import lombok.Data;
-import javax.persistence.Column;
-import javax.persistence.Entity;
-import javax.persistence.GeneratedValue;
-import javax.persistence.GenerationType;
-import javax.persistence.Id;
-import javax.persistence.Table;
import java.io.Serializable;
import java.util.Date;
/**
* @author yaoyuan2.chu
- * @Title:
- * @Package com.yaoyuan.jiscuss.entity
- * @Description:
- * @date 2020/10/21 12:00
*/
@Data
@Entity
diff --git a/src/main/java/com/yaoyuan/jiscuss/entity/Post.java b/src/main/java/com/yaoyuan/jiscuss/entity/Post.java
index 8dda459..c6eadda 100644
--- a/src/main/java/com/yaoyuan/jiscuss/entity/Post.java
+++ b/src/main/java/com/yaoyuan/jiscuss/entity/Post.java
@@ -1,13 +1,14 @@
package com.yaoyuan.jiscuss.entity;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import jakarta.validation.constraints.NotBlank;
import lombok.Data;
-import javax.persistence.Column;
-import javax.persistence.Entity;
-import javax.persistence.GeneratedValue;
-import javax.persistence.GenerationType;
-import javax.persistence.Id;
-import javax.persistence.Table;
import java.io.Serializable;
import java.util.Date;
@@ -36,6 +37,7 @@ public class Post implements Serializable {
@Column(name = "type")
private String type;
+ @NotBlank
@Column(name = "content")
private String content;
@@ -48,6 +50,7 @@ public class Post implements Serializable {
@Column(name = "edit_user_id")
private Integer editUserId;
+ /** IP resolved server-side via IpUtils.getClientIp(). */
@Column(name = "ip_address")
private String ipAddress;
diff --git a/src/main/java/com/yaoyuan/jiscuss/entity/Setting.java b/src/main/java/com/yaoyuan/jiscuss/entity/Setting.java
index b280ed4..1262591 100644
--- a/src/main/java/com/yaoyuan/jiscuss/entity/Setting.java
+++ b/src/main/java/com/yaoyuan/jiscuss/entity/Setting.java
@@ -1,13 +1,13 @@
package com.yaoyuan.jiscuss.entity;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
import lombok.Data;
-import javax.persistence.Column;
-import javax.persistence.Entity;
-import javax.persistence.GeneratedValue;
-import javax.persistence.GenerationType;
-import javax.persistence.Id;
-import javax.persistence.Table;
import java.io.Serializable;
@Data
diff --git a/src/main/java/com/yaoyuan/jiscuss/entity/Tag.java b/src/main/java/com/yaoyuan/jiscuss/entity/Tag.java
index 1606c25..07a870c 100644
--- a/src/main/java/com/yaoyuan/jiscuss/entity/Tag.java
+++ b/src/main/java/com/yaoyuan/jiscuss/entity/Tag.java
@@ -1,13 +1,13 @@
package com.yaoyuan.jiscuss.entity;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
import lombok.Data;
-import javax.persistence.Column;
-import javax.persistence.Entity;
-import javax.persistence.GeneratedValue;
-import javax.persistence.GenerationType;
-import javax.persistence.Id;
-import javax.persistence.Table;
import java.io.Serializable;
import java.util.Date;
diff --git a/src/main/java/com/yaoyuan/jiscuss/entity/User.java b/src/main/java/com/yaoyuan/jiscuss/entity/User.java
index 86914ed..3c072cf 100644
--- a/src/main/java/com/yaoyuan/jiscuss/entity/User.java
+++ b/src/main/java/com/yaoyuan/jiscuss/entity/User.java
@@ -1,14 +1,17 @@
package com.yaoyuan.jiscuss.entity;
-
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
import lombok.Data;
-import javax.persistence.Column;
-import javax.persistence.Entity;
-import javax.persistence.GeneratedValue;
-import javax.persistence.GenerationType;
-import javax.persistence.Id;
-import javax.persistence.Table;
import java.io.Serializable;
import java.util.Date;
@@ -22,15 +25,21 @@ public class User implements Serializable {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
+ @NotBlank
+ @Size(min = 3, max = 50)
@Column(name = "username")
private String username;
+ @Size(max = 50)
@Column(name = "realname")
private String realname;
+ @Email
+ @Size(max = 100)
@Column(name = "email")
private String email;
+ /** BCrypt-hashed password. Column length must be >= 68 (see V2 migration). */
@Column(name = "password")
private String password;
@@ -40,12 +49,14 @@ public class User implements Serializable {
@Column(name = "age")
private Integer age;
+ @Pattern(regexp = "^(男|女|其他)?$")
@Column(name = "gender")
private String gender;
@Column(name = "avatar")
private String avatar;
+ @Size(max = 20)
@Column(name = "phone")
private String phone;
@@ -63,5 +74,4 @@ public class User implements Serializable {
@Column(name = "level")
private Integer level;
-
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/yaoyuan/jiscuss/entity/custom/TagCustom.java b/src/main/java/com/yaoyuan/jiscuss/entity/custom/TagCustom.java
index 54ed72a..cc3340f 100644
--- a/src/main/java/com/yaoyuan/jiscuss/entity/custom/TagCustom.java
+++ b/src/main/java/com/yaoyuan/jiscuss/entity/custom/TagCustom.java
@@ -4,7 +4,7 @@ import lombok.Data;
import lombok.Getter;
import lombok.Setter;
-import javax.persistence.Column;
+import jakarta.persistence.Column;
/**
* @author yaoyuan2.chu
diff --git a/src/main/java/com/yaoyuan/jiscuss/exception/GlobalExceptionHandler.java b/src/main/java/com/yaoyuan/jiscuss/exception/GlobalExceptionHandler.java
new file mode 100644
index 0000000..601bc36
--- /dev/null
+++ b/src/main/java/com/yaoyuan/jiscuss/exception/GlobalExceptionHandler.java
@@ -0,0 +1,80 @@
+package com.yaoyuan.jiscuss.exception;
+
+import com.yaoyuan.jiscuss.response.ApiResponse;
+import com.yaoyuan.jiscuss.response.ResponseCode;
+import jakarta.validation.ConstraintViolationException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+import java.util.stream.Collectors;
+
+/**
+ * Global exception handler.
+ *
+ *
Catches {@link BaseException}, validation errors, and unexpected exceptions so they
+ * are returned as structured JSON instead of Spring's default error page.
+ */
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+ private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
+
+ /** Business logic exceptions (already typed with a ResponseCode). */
+ @ExceptionHandler(BaseException.class)
+ public ResponseEntity> handleBaseException(BaseException ex) {
+ log.warn("Business exception: code={}, msg={}", ex.getCode().getCode(), ex.getCode().getMsg());
+ return ResponseEntity
+ .status(HttpStatus.BAD_REQUEST)
+ .body(ApiResponse.fail(ex.getCode()));
+ }
+
+ /** @Valid / @Validated failures on @RequestBody. */
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ public ResponseEntity> handleValidationException(MethodArgumentNotValidException ex) {
+ String message = ex.getBindingResult().getFieldErrors().stream()
+ .map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
+ .collect(Collectors.joining("; "));
+ log.warn("Validation failed: {}", message);
+ return ResponseEntity
+ .status(HttpStatus.BAD_REQUEST)
+ .body(ApiResponse.error(message));
+ }
+
+ /** @Validated failures on @RequestParam / @PathVariable. */
+ @ExceptionHandler(ConstraintViolationException.class)
+ public ResponseEntity> handleConstraintViolation(ConstraintViolationException ex) {
+ String message = ex.getConstraintViolations().stream()
+ .map(cv -> cv.getPropertyPath() + ": " + cv.getMessage())
+ .collect(Collectors.joining("; "));
+ log.warn("Constraint violation: {}", message);
+ return ResponseEntity
+ .status(HttpStatus.BAD_REQUEST)
+ .body(ApiResponse.error(message));
+ }
+
+ /**
+ * Spring Security access-denied exceptions re-thrown by method security.
+ * Note: exceptions raised before method invocation are handled by {@code CustomAccessDeniedHandler}.
+ */
+ @ExceptionHandler(AccessDeniedException.class)
+ public ResponseEntity> handleAccessDenied(AccessDeniedException ex) {
+ return ResponseEntity
+ .status(HttpStatus.FORBIDDEN)
+ .body(ApiResponse.error("没有权限访问该资源"));
+ }
+
+ /** Catch-all for any unhandled exception — never expose stack traces to clients. */
+ @ExceptionHandler(Exception.class)
+ public ResponseEntity> handleGeneral(Exception ex) {
+ log.error("Unexpected error", ex);
+ return ResponseEntity
+ .status(HttpStatus.INTERNAL_SERVER_ERROR)
+ .body(ApiResponse.fail(ResponseCode.SERVICE_ERROR));
+ }
+}
diff --git a/src/main/java/com/yaoyuan/jiscuss/handler/CustomAccessDeniedHandler.java b/src/main/java/com/yaoyuan/jiscuss/handler/CustomAccessDeniedHandler.java
index 69fd38c..5dd7e62 100644
--- a/src/main/java/com/yaoyuan/jiscuss/handler/CustomAccessDeniedHandler.java
+++ b/src/main/java/com/yaoyuan/jiscuss/handler/CustomAccessDeniedHandler.java
@@ -8,10 +8,10 @@ import org.springframework.security.web.WebAttributes;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
-import javax.servlet.RequestDispatcher;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
diff --git a/src/main/java/com/yaoyuan/jiscuss/handler/RbacPermission.java b/src/main/java/com/yaoyuan/jiscuss/handler/RbacPermission.java
index 6bd5aa1..30414c5 100644
--- a/src/main/java/com/yaoyuan/jiscuss/handler/RbacPermission.java
+++ b/src/main/java/com/yaoyuan/jiscuss/handler/RbacPermission.java
@@ -1,37 +1,70 @@
package com.yaoyuan.jiscuss.handler;
+import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
-import javax.servlet.http.HttpServletRequest;
+import java.util.Collection;
/**
- * RBAC数据模型控制权限
+ * RBAC (Role-Based Access Control) permission evaluator.
*
- * @author charlie
+ * Rules:
+ *
+ * - Anonymous / unauthenticated: denied
+ * - ROLE_ADMIN: full access (all paths)
+ * - ROLE_USER: access to all non-admin paths
+ * - Admin-only paths ({@code /admin/**}, {@code /druid/**}, etc.) require ROLE_ADMIN
+ *
+ *
+ * Previously this method unconditionally returned {@code true}, giving every user
+ * unrestricted access regardless of role — a critical security vulnerability now fixed.
*/
@Component("rbacPermission")
public class RbacPermission {
- private AntPathMatcher antPathMatcher = new AntPathMatcher();
+ private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
+ /** URL patterns that require ROLE_ADMIN. */
+ private static final String[] ADMIN_ONLY_PATTERNS = {
+ "/admin/**",
+ "/druid/**",
+ "/actuator/**",
+ "/h2-console/**",
+ "/user_api/**",
+ "/other_api/**"
+ };
+
+ /**
+ * Evaluates whether the authenticated principal is permitted to access the requested URL.
+ *
+ * @param request the current HTTP request
+ * @param authentication the current authentication token
+ * @return {@code true} when access is granted
+ */
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
- Object principal = authentication.getPrincipal();
- boolean hasPermission = false;
+ if (authentication == null
+ || !authentication.isAuthenticated()
+ || "anonymousUser".equals(authentication.getPrincipal())) {
+ return false;
+ }
- hasPermission = true;
-// if (principal instanceof UserEntity) {
-// // 读取用户所拥有的权限菜单
-// List