feat(security): implement JWT-based authentication and authorization

- Configured JWT token validation filter in security chain
- Added user role mapping with new t_user_role table and UserRole entity
- Implemented custom authentication entry point and access denied handler
- Updated UserDetailService to load user roles from database
- Added @PreAuthorize annotation support for method-level security
- Refactored build scripts to use java-library plugin and proper dependency scope
- Enhanced SQL schema with user role table and improved table comments
- Added global exception handler for AccessDeniedException
- Introduced ResponseCodeEnum constants for unauthorized and forbidden access
- Integrated TokenAuthenticationFilter into Spring Security filter chain
This commit is contained in:
2025-11-29 15:19:35 +08:00
parent de52e2816c
commit 0a126eb520
17 changed files with 339 additions and 28 deletions

View File

@@ -0,0 +1,64 @@
package com.hanserwei.common.domain.dataobject;
import jakarta.persistence.*; // 使用 Jakarta Persistence API (JPA 3.0+)
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.CreationTimestamp;
import java.io.Serial;
import java.io.Serializable;
import java.time.Instant;
/**
* 用户角色表t_user_role 对应实体)
*/
@Setter
@Getter
@Entity
@Table(name = "t_user_role",
indexes = {
@Index(name = "idx_username", columnList = "username") // 映射数据库中的 idx_username 索引
})
public class UserRole implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID (BIG SERIAL PRIMARY KEY)
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
/**
* 用户名 (VARCHAR(60) NOT NULL)
*/
@Column(name = "username", length = 60, nullable = false)
private String username;
/**
* 角色名称 (VARCHAR(60) NOT NULL)
*/
@Column(name = "role_name", length = 60, nullable = false)
private String roleName;
/**
* 创建时间 (TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP)
*/
@CreationTimestamp
@Column(name = "create_time", nullable = false, updatable = false)
private Instant createTime;
// --- 构造函数 (Constructor) ---
public UserRole() {
}
public UserRole(String username, String roleName) {
this.username = username;
this.roleName = roleName;
}
}

View File

@@ -0,0 +1,11 @@
package com.hanserwei.common.domain.repository;
import com.hanserwei.common.domain.dataobject.UserRole;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface UserRoleRepository extends JpaRepository<UserRole, Long> {
List<UserRole> queryAllByUsername(String username);
}

View File

@@ -15,6 +15,8 @@ public enum ResponseCodeEnum implements BaseExceptionInterface {
// ----------- 业务异常状态码 -----------
LOGIN_FAIL("20000", "登录失败"),
USERNAME_OR_PWD_ERROR("20001", "用户名或密码错误"),
UNAUTHORIZED("20002", "无访问权限,请先登录!"),
FORBIDDEN("20004", "演示账号仅支持查询操作!")
;
// 异常码

View File

@@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.security.access.AccessDeniedException;
import java.util.Optional;
@ControllerAdvice
@@ -76,4 +77,11 @@ public class GlobalExceptionHandler {
return Response.fail(errorCode, errorMessage);
}
@ExceptionHandler({ AccessDeniedException.class })
public void throwAccessDeniedException(AccessDeniedException e) throws AccessDeniedException {
// 捕获到鉴权失败异常,主动抛出,交给 RestAccessDeniedHandler 去处理
log.info("============= 捕获到 AccessDeniedException");
throw e;
}
}