一、项目概述:什么是真正的企业级项目?
1.1 企业级项目 vs 玩具项目
传统"Hello World"项目的局限性:
java
// ❌ 这是玩具项目
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello World";
}
}
// ✅ 这是企业级项目需要具备的特性:
- 分层架构清晰(Controller/Service/Repository)
- 统一异常处理
- 数据验证机制
- 统一响应格式
- 日志记录
- 权限控制
- 数据库事务
- 缓存策略
- 单元测试
- API文档
1.2 用户管理系统功能需求
模块 功能点 技术实现 重要性
用户管理 用户注册、登录、信息修改 Spring Security/JWT ★★★★★
权限管理 角色分配、权限验证 RBAC模型 ★★★★★
数据管理 CRUD操作、分页查询 Spring Data JPA ★★★★★
系统安全 密码加密、防暴力破解 BCrypt、限流 ★★★★★
接口文档 API在线文档 Swagger/OpenAPI ★★★★☆
日志管理 操作日志、异常日志 AOP切面 ★★★★☆
数据验证 参数校验、业务校验 Validation API ★★★★☆
缓存优化 热点数据缓存 Redis/Caffeine ★★★☆☆
单元测试 服务层测试 JUnit/Mockito ★★★☆☆
监控告警 健康检查、性能监控 Actuator/Micrometer ★★★☆☆
二、10分钟快速搭建:真的可能吗?
2.1 准备工作(2分钟)
环境要求:
bash
检查环境
java -version # >= Java 17
mvn -v # Maven 3.6+
docker -v # Docker(可选,用于Redis等)
创建项目目录
mkdir user-management-system
cd user-management-system
使用Spring Initializr生成项目(30秒):
bash
使用curl命令快速生成
curl https://start.spring.io/starter.zip
-d type=maven-project
-d language=java
-d bootVersion=3.1.0
-d baseDir=user-management
-d groupId=com.example
-d artifactId=user-management
-d name=user-management
-d description="企业级用户管理系统"
-d packageName=com.example.usermanagement
-d packaging=jar
-d javaVersion=17
-d dependencies=web,data-jpa,security,validation,redis,cache,aop,actuator,devtools
-o user-management.zip
unzip user-management.zip
或者直接复制完整pom.xml(1分钟):
xml
<?xml version="1.0" encoding="UTF-8"?>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>user-management</artifactId>
<version>1.0.0</version>
<name>user-management</name>
<description>企业级用户管理系统</description>
<properties>
<java.version>17</java.version>
<mapstruct.version>1.5.5.Final</mapstruct.version>
<jjwt.version>0.11.5</jjwt.version>
<swagger.version>2.2.0</swagger.version>
</properties>
<dependencies>
<!-- Web开发 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 数据访问 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- 安全框架 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 数据验证 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Redis缓存 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 缓存抽象 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- AOP支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 监控管理 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- 开发工具(热部署) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- 数据库(使用H2内存数据库,快速开始) -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok(减少样板代码) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- MapStruct(对象映射) -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<!-- JWT令牌 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<!-- Swagger API文档 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${swagger.version}</version>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<!-- MapStruct编译插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
2.2 基础配置(2分钟) application.yml 完整配置:
yaml
应用配置
spring:
application:
name: user-management-system
数据源配置(H2内存数据库)
datasource:
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
driver-class-name: org.h2.Driver
username: sa
password:
JPA配置
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.H2Dialect
format_sql: true
jdbc:
batch_size: 20
order_inserts: true
order_updates: true
Redis配置(如果未安装Redis,使用本地缓存)
redis:
host: localhost
port: 6379
timeout: 2000ms
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
缓存配置
cache:
type: caffeine
caffeine:
spec: maximumSize=500,expireAfterWrite=10m
文件上传配置
servlet:
multipart:
max-file-size: 10MB
max-request-size: 100MB
H2控制台
h2:
console:
enabled: true
path: /h2-console
settings:
trace: false
web-allow-others: false
Jackson配置
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
serialization:
write-dates-as-timestamps: false
配置JPA SQL日志输出
sql:
init:
platform: h2
安全配置
security:
jwt:
secret: mySecretKeyForJWTTokenGeneration123!@#
expiration: 86400000 # 24小时(毫秒)
header: Authorization
token-prefix: "Bearer "
应用自定义配置
app:
version: 1.0.0
cors:
allowed-origins: "http://localhost:3000,http://localhost:8080"
allowed-methods: "GET,POST,PUT,DELETE,OPTIONS"
allowed-headers: "*"
max-age: 3600
密码策略
password:
min-length: 6
max-length: 20
require-uppercase: true
require-lowercase: true
require-digit: true
require-special-char: false
分页配置
pagination:
default-page-size: 20
max-page-size: 100
上传配置
upload:
base-path: "./uploads"
avatar:
max-size: 2MB
allowed-types: "image/jpeg,image/png,image/gif"
缓存配置
cache:
user:
ttl: 300 # 5分钟
role:
ttl: 600 # 10分钟
日志配置
logging:
level:
root: INFO
com.example.usermanagement: DEBUG
org.springframework.security: DEBUG
org.springframework.web: INFO
org.hibernate.SQL: DEBUG
org.hibernate.type.descriptor.sql.BasicBinder: TRACE
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file:
name: logs/user-management.log
max-size: 10MB
max-history: 30
Actuator监控配置
management:
endpoints:
web:
exposure:
include: "health,info,metrics,prometheus"
base-path: /actuator
endpoint:
health:
show-details: always
probes:
enabled: true
metrics:
enabled: true
info:
env:
enabled: true
metrics:
export:
prometheus:
enabled: true
tags:
application: ${spring.application.name}
Swagger配置
springdoc:
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html
operations-sorter: method
tags-sorter: alpha
show-actuator: true
default-consumes-media-type: application/json
default-produces-media-type: application/json
服务器配置
server:
port: 8080
servlet:
context-path: /api
compression:
enabled: true
mime-types: text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json
min-response-size: 1024
tomcat:
connection-timeout: 5000
max-connections: 10000
threads:
max: 200
min-spare: 10
启动类配置(30秒):
java
package com.example.usermanagement;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication
@EnableJpaAuditing // 启用JPA审计(自动填充创建时间、更新时间)
@EnableTransactionManagement // 启用事务管理
@EnableCaching // 启用缓存
@EnableAsync // 启用异步处理
public class UserManagementApplication {
public static void main(String[] args) {
SpringApplication.run(UserManagementApplication.class, args);
System.out.println("\n=========================================");
System.out.println("用户管理系统启动成功!");
System.out.println("API文档: http://localhost:8080/api/swagger-ui.html");
System.out.println("H2数据库: http://localhost:8080/api/h2-console");
System.out.println("JDBC URL: jdbc:h2:mem:testdb");
System.out.println("用户名: sa");
System.out.println("密码: (空)");
System.out.println("=========================================\n");
}
}
三、核心模块实现(5分钟)
3.1 数据库设计(1分钟)
实体类关系图:
text
用户系统核心实体:
User (用户)
├── id (主键)
├── username (用户名)
├── password (密码)
├── email (邮箱)
├── phone (手机号)
├── avatar (头像)
├── status (状态)
├── roles (角色集合) → ManyToMany → Role
└── audit fields (审计字段)
Role (角色)
├── id (主键)
├── name (角色名)
├── code (角色代码)
├── description (描述)
├── permissions (权限集合) → ManyToMany → Permission
└── users (用户集合) → ManyToMany → User
Permission (权限)
├── id (主键)
├── name (权限名)
├── code (权限代码)
├── description (描述)
├── type (类型: MENU/BUTTON/API)
├── url (资源URL)
└── parentId (父权限ID)
AuditBase (审计基类)
├── createdBy (创建人)
├── createdAt (创建时间)
├── updatedBy (更新人)
└── updatedAt (更新时间)
实体类实现:
java
// 1. 审计基类(所有实体继承)
package com.example.usermanagement.entity.base;
import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Data;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
@Data
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class AuditBase {
@CreatedBy
@Column(name = "created_by", updatable = false)
private String createdBy;
@CreatedDate
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@LastModifiedBy
@Column(name = "updated_by")
private String updatedBy;
@LastModifiedDate
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}
// 2. 用户实体
package com.example.usermanagement.entity;
import com.example.usermanagement.entity.base.AuditBase;
import com.example.usermanagement.enums.UserStatus;
import jakarta.persistence.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.HashSet;
import java.util.Set;
@Data
@Entity
@Table(name = "sys_user",
indexes = {
@Index(name = "idx_username", columnList = "username", unique = true),
@Index(name = "idx_email", columnList = "email", unique = true),
@Index(name = "idx_phone", columnList = "phone", unique = true),
@Index(name = "idx_status", columnList = "status")
})
@EqualsAndHashCode(callSuper = true)
public class User extends AuditBase {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 50, unique = true)
private String username;
@Column(nullable = false, length = 100)
private String password;
@Column(nullable = false, length = 100, unique = true)
private String email;
@Column(length = 20, unique = true)
private String phone;
@Column(length = 200)
private String avatar;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private UserStatus status = UserStatus.ACTIVE;
@Column(name = "last_login_time")
private LocalDateTime lastLoginTime;
@Column(name = "login_fail_count")
private Integer loginFailCount = 0;
@Column(name = "account_non_locked")
private Boolean accountNonLocked = true;
// 用户-角色多对多关系
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "sys_user_role",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles = new HashSet<>();
// 业务方法
public void incrementLoginFailCount() {
this.loginFailCount = (this.loginFailCount == null ? 1 : this.loginFailCount + 1);
if (this.loginFailCount >= 5) {
this.accountNonLocked = false; // 登录失败5次锁定账户
}
}
public void resetLoginFailCount() {
this.loginFailCount = 0;
this.accountNonLocked = true;
}
}
// 3. 角色实体
package com.example.usermanagement.entity;
import com.example.usermanagement.entity.base.AuditBase;
import jakarta.persistence.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.HashSet;
import java.util.Set;
@Data
@Entity
@Table(name = "sys_role",
indexes = {
@Index(name = "idx_role_code", columnList = "code", unique = true),
@Index(name = "idx_role_name", columnList = "name")
})
@EqualsAndHashCode(callSuper = true)
public class Role extends AuditBase {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 50)
private String name;
@Column(nullable = false, length = 50, unique = true)
private String code;
@Column(length = 200)
private String description;
@Column(nullable = false)
private Integer sortOrder = 0;
// 角色-用户多对多关系
@ManyToMany(mappedBy = "roles", fetch = FetchType.LAZY)
private Set<User> users = new HashSet<>();
// 角色-权限多对多关系
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "sys_role_permission",
joinColumns = @JoinColumn(name = "role_id"),
inverseJoinColumns = @JoinColumn(name = "permission_id"))
private Set<Permission> permissions = new HashSet<>();
}
// 4. 权限实体
package com.example.usermanagement.entity;
import com.example.usermanagement.entity.base.AuditBase;
import com.example.usermanagement.enums.PermissionType;
import jakarta.persistence.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.HashSet;
import java.util.Set;
@Data
@Entity
@Table(name = "sys_permission",
indexes = {
@Index(name = "idx_permission_code", columnList = "code", unique = true),
@Index(name = "idx_parent_id", columnList = "parentId"),
@Index(name = "idx_permission_type", columnList = "type")
})
@EqualsAndHashCode(callSuper = true)
public class Permission extends AuditBase {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 50)
private String name;
@Column(nullable = false, length = 100, unique = true)
private String code;
@Column(length = 200)
private String description;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private PermissionType type;
@Column(length = 200)
private String url;
@Column(length = 50)
private String icon;
@Column(name = "parent_id")
private Long parentId;
@Column(nullable = false)
private Integer sortOrder = 0;
@Column(nullable = false)
private Boolean visible = true;
// 权限-角色多对多关系
@ManyToMany(mappedBy = "permissions", fetch = FetchType.LAZY)
private Set<Role> roles = new HashSet<>();
}
枚举类定义:
java
// 用户状态枚举
package com.example.usermanagement.enums;
import lombok.Getter;
@Getter
public enum UserStatus {
ACTIVE("活跃", 1),
INACTIVE("未激活", 2),
LOCKED("已锁定", 3),
DELETED("已删除", 4);
private final String description;
private final Integer code;
UserStatus(String description, Integer code) {
this.description = description;
this.code = code;
}
}
// 权限类型枚举
package com.example.usermanagement.enums;
import lombok.Getter;
@Getter
public enum PermissionType {
MENU("菜单", 1),
BUTTON("按钮", 2),
API("接口", 3),
DATA("数据", 4);
private final String description;
private final Integer code;
PermissionType(String description, Integer code) {
this.description = description;
this.code = code;
}
}
3.2 数据访问层(1分钟)
Repository接口:
java
// 1. 用户Repository
package com.example.usermanagement.repository;
import com.example.usermanagement.entity.User;
import com.example.usermanagement.enums.UserStatus;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long>,
JpaSpecificationExecutor {
// 根据用户名查找用户
Optional<User> findByUsername(String username);
// 根据邮箱查找用户
Optional<User> findByEmail(String email);
// 根据手机号查找用户
Optional<User> findByPhone(String phone);
// 根据用户名或邮箱查找用户
Optional<User> findByUsernameOrEmail(String username, String email);
// 检查用户名是否存在
boolean existsByUsername(String username);
// 检查邮箱是否存在
boolean existsByEmail(String email);
// 检查手机号是否存在
boolean existsByPhone(String phone);
// 根据状态查找用户
List<User> findByStatus(UserStatus status);
// 根据状态分页查找用户
Page<User> findByStatus(UserStatus status, Pageable pageable);
// 根据创建时间范围查找用户
List<User> findByCreatedAtBetween(LocalDateTime start, LocalDateTime end);
// 更新最后登录时间
@Modifying
@Query("UPDATE User u SET u.lastLoginTime = :loginTime WHERE u.id = :userId")
void updateLastLoginTime(@Param("userId") Long userId,
@Param("loginTime") LocalDateTime loginTime);
// 根据角色ID查找用户
@Query("SELECT u FROM User u JOIN u.roles r WHERE r.id = :roleId")
List<User> findByRoleId(@Param("roleId") Long roleId);
// 统计用户数量
@Query("SELECT COUNT(u) FROM User u WHERE u.status = :status")
long countByStatus(@Param("status") UserStatus status);
// 查找最近登录的用户
@Query("SELECT u FROM User u WHERE u.lastLoginTime IS NOT NULL ORDER BY u.lastLoginTime DESC")
List<User> findRecentLoginUsers(Pageable pageable);
}
// 2. 角色Repository
package com.example.usermanagement.repository;
import com.example.usermanagement.entity.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface RoleRepository extends JpaRepository<Role, Long>,
JpaSpecificationExecutor {
Optional<Role> findByCode(String code);
boolean existsByCode(String code);
List<Role> findByIdIn(List<Long> ids);
@Query("SELECT r FROM Role r JOIN r.users u WHERE u.id = :userId")
List<Role> findByUserId(@Param("userId") Long userId);
}
// 3. 权限Repository
package com.example.usermanagement.repository;
import com.example.usermanagement.entity.Permission;
import com.example.usermanagement.enums.PermissionType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Set;
@Repository
public interface PermissionRepository extends JpaRepository<Permission, Long>,
JpaSpecificationExecutor {
Optional<Permission> findByCode(String code);
List<Permission> findByType(PermissionType type);
List<Permission> findByParentId(Long parentId);
List<Permission> findByParentIdIsNull();
@Query("SELECT p FROM Permission p JOIN p.roles r WHERE r.id IN :roleIds")
Set<Permission> findByRoleIds(@Param("roleIds") Set<Long> roleIds);
@Query("SELECT p FROM Permission p JOIN p.roles r WHERE r.code = :roleCode")
Set<Permission> findByRoleCode(@Param("roleCode") String roleCode);
}
3.3 业务逻辑层(2分钟)
DTO定义:
java
// 1. 通用DTO
package com.example.usermanagement.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ApiResponse implements Serializable {
private Integer code;
private String message;
private T data;
private Long timestamp;
public static <T> ApiResponse<T> success(T data) {
return ApiResponse.<T>builder()
.code(200)
.message("成功")
.data(data)
.timestamp(System.currentTimeMillis())
.build();
}
public static <T> ApiResponse<T> success(String message, T data) {
return ApiResponse.<T>builder()
.code(200)
.message(message)
.data(data)
.timestamp(System.currentTimeMillis())
.build();
}
public static <T> ApiResponse<T> error(Integer code, String message) {
return ApiResponse.<T>builder()
.code(code)
.message(message)
.timestamp(System.currentTimeMillis())
.build();
}
}
// 2. 用户相关DTO
package com.example.usermanagement.dto.user;
import com.example.usermanagement.enums.UserStatus;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.Set;
// 用户注册请求
@Data
@Schema(description = "用户注册请求")
public class UserRegisterRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 50, message = "用户名长度必须在3-50个字符之间")
@Schema(description = "用户名", example = "zhangsan")
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 20, message = "密码长度必须在6-20个字符之间")
@Schema(description = "密码", example = "123456")
private String password;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
@Schema(description = "邮箱", example = "zhangsan@example.com")
private String email;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
@Schema(description = "手机号", example = "13800138000")
private String phone;
}
// 用户登录请求
@Data
@Schema(description = "用户登录请求")
public class UserLoginRequest {
@NotBlank(message = "用户名或邮箱不能为空")
@Schema(description = "用户名或邮箱", example = "zhangsan")
private String usernameOrEmail;
@NotBlank(message = "密码不能为空")
@Schema(description = "密码", example = "123456")
private String password;
@Schema(description = "记住我", example = "false")
private Boolean rememberMe = false;
}
// 用户登录响应
@Data
@Schema(description = "用户登录响应")
public class UserLoginResponse {
@Schema(description = "用户ID", example = "1")
private Long userId;
@Schema(description = "用户名", example = "zhangsan")
private String username;
@Schema(description = "邮箱", example = "zhangsan@example.com")
private String email;
@Schema(description = "访问令牌")
private String accessToken;
@Schema(description = "令牌类型", example = "Bearer")
private String tokenType = "Bearer";
@Schema(description = "过期时间(秒)", example = "86400")
private Long expiresIn;
@Schema(description = "角色列表")
private Set<String> roles;
@Schema(description = "权限列表")
private Set<String> permissions;
}
// 用户信息响应
@Data
@Schema(description = "用户信息")
public class UserDTO {
@Schema(description = "用户ID", example = "1")
private Long id;
@Schema(description = "用户名", example = "zhangsan")
private String username;
@Schema(description = "邮箱", example = "zhangsan@example.com")
private String email;
@Schema(description = "手机号", example = "13800138000")
private String phone;
@Schema(description = "头像URL")
private String avatar;
@Schema(description = "用户状态")
private UserStatus status;
@Schema(description = "角色列表")
private Set<RoleSimpleDTO> roles;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "创建时间")
private LocalDateTime createdAt;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "最后登录时间")
private LocalDateTime lastLoginTime;
}
// 3. 角色相关DTO
package com.example.usermanagement.dto.role;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.Set;
@Data
@Schema(description = "角色信息")
public class RoleDTO {
@Schema(description = "角色ID", example = "1")
private Long id;
@NotBlank(message = "角色名称不能为空")
@Size(max = 50, message = "角色名称长度不能超过50个字符")
@Schema(description = "角色名称", example = "管理员")
private String name;
@NotBlank(message = "角色代码不能为空")
@Size(max = 50, message = "角色代码长度不能超过50个字符")
@Schema(description = "角色代码", example = "ROLE_ADMIN")
private String code;
@Schema(description = "角色描述", example = "系统管理员")
private String description;
@Schema(description = "排序号", example = "1")
private Integer sortOrder;
@Schema(description = "权限ID列表")
private Set<Long> permissionIds;
@Schema(description = "创建时间")
private LocalDateTime createdAt;
}
// 4. 权限相关DTO
package com.example.usermanagement.dto.permission;
import com.example.usermanagement.enums.PermissionType;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.util.List;
@Data
@Schema(description = "权限信息")
public class PermissionDTO {
@Schema(description = "权限ID", example = "1")
private Long id;
@NotBlank(message = "权限名称不能为空")
@Schema(description = "权限名称", example = "用户管理")
private String name;
@NotBlank(message = "权限代码不能为空")
@Schema(description = "权限代码", example = "user:manage")
private String code;
@Schema(description = "权限描述", example = "管理用户信息")
private String description;
@Schema(description = "权限类型")
private PermissionType type;
@Schema(description = "资源URL")
private String url;
@Schema(description = "图标")
private String icon;
@Schema(description = "父权限ID", example = "0")
private Long parentId;
@Schema(description = "排序号", example = "1")
private Integer sortOrder;
@Schema(description = "是否可见", example = "true")
private Boolean visible;
@Schema(description = "子权限列表")
private List<PermissionDTO> children;
}
Service层实现:
java
// 1. 用户服务接口
package com.example.usermanagement.service;
import com.example.usermanagement.dto.user.*;
import com.example.usermanagement.entity.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
public interface UserService {
// 用户注册
User register(UserRegisterRequest request);
// 用户登录
UserLoginResponse login(UserLoginRequest request);
// 获取当前用户信息
UserDTO getCurrentUser();
// 根据ID获取用户
UserDTO getUserById(Long id);
// 根据用户名获取用户
UserDTO getUserByUsername(String username);
// 分页查询用户
Page<UserDTO> getUsers(Pageable pageable);
// 根据条件查询用户
Page<UserDTO> searchUsers(String keyword, UserStatus status, Pageable pageable);
// 更新用户信息
UserDTO updateUser(Long id, UserUpdateRequest request);
// 删除用户(软删除)
void deleteUser(Long id);
// 修改密码
void changePassword(ChangePasswordRequest request);
// 重置密码
void resetPassword(Long userId, String newPassword);
// 分配角色
void assignRoles(Long userId, Set<Long> roleIds);
// 检查用户名是否可用
boolean isUsernameAvailable(String username);
// 检查邮箱是否可用
boolean isEmailAvailable(String email);
}
// 2. 用户服务实现
package com.example.usermanagement.service.impl;
import com.example.usermanagement.dto.user.*;
import com.example.usermanagement.entity.Role;
import com.example.usermanagement.entity.User;
import com.example.usermanagement.enums.UserStatus;
import com.example.usermanagement.exception.BusinessException;
import com.example.usermanagement.repository.RoleRepository;
import com.example.usermanagement.repository.UserRepository;
import com.example.usermanagement.service.UserService;
import com.example.usermanagement.util.JwtTokenUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import jakarta.persistence.criteria.Predicate;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final RoleRepository roleRepository;
private final PasswordEncoder passwordEncoder;
private final AuthenticationManager authenticationManager;
private final JwtTokenUtil jwtTokenUtil;
private final UserMapper userMapper;
@Override
@Transactional
public User register(UserRegisterRequest request) {
log.info("用户注册: username={}", request.getUsername());
// 1. 验证用户名是否已存在
if (userRepository.existsByUsername(request.getUsername())) {
throw new BusinessException("用户名已存在");
}
// 2. 验证邮箱是否已存在
if (userRepository.existsByEmail(request.getEmail())) {
throw new BusinessException("邮箱已存在");
}
// 3. 验证手机号是否已存在(如果有)
if (StringUtils.hasText(request.getPhone()) &&
userRepository.existsByPhone(request.getPhone())) {
throw new BusinessException("手机号已存在");
}
// 4. 创建用户实体
User user = new User();
user.setUsername(request.getUsername());
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setEmail(request.getEmail());
user.setPhone(request.getPhone());
user.setStatus(UserStatus.ACTIVE);
// 5. 分配默认角色(ROLE_USER)
Role defaultRole = roleRepository.findByCode("ROLE_USER")
.orElseThrow(() -> new BusinessException("默认角色不存在"));
user.getRoles().add(defaultRole);
// 6. 保存用户
return userRepository.save(user);
}
@Override
@Transactional
public UserLoginResponse login(UserLoginRequest request) {
log.info("用户登录尝试: usernameOrEmail={}", request.getUsernameOrEmail());
try {
// 1. 认证
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsernameOrEmail(),
request.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
// 2. 获取用户信息
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
User user = userDetails.getUser();
// 3. 更新最后登录时间和重置登录失败计数
user.setLastLoginTime(LocalDateTime.now());
user.resetLoginFailCount();
userRepository.save(user);
// 4. 生成JWT令牌
String token = jwtTokenUtil.generateToken(userDetails);
// 5. 获取用户角色和权限
Set<String> roles = user.getRoles().stream()
.map(Role::getCode)
.collect(Collectors.toSet());
Set<String> permissions = new HashSet<>();
user.getRoles().forEach(role ->
role.getPermissions().forEach(permission ->
permissions.add(permission.getCode())
)
);
// 6. 构建响应
UserLoginResponse response = new UserLoginResponse();
response.setUserId(user.getId());
response.setUsername(user.getUsername());
response.setEmail(user.getEmail());
response.setAccessToken(token);
response.setExpiresIn(jwtTokenUtil.getExpiration() / 1000);
response.setRoles(roles);
response.setPermissions(permissions);
log.info("用户登录成功: userId={}, username={}", user.getId(), user.getUsername());
return response;
} catch (Exception e) {
// 记录登录失败
User user = userRepository.findByUsernameOrEmail(
request.getUsernameOrEmail(),
request.getUsernameOrEmail()
).orElse(null);
if (user != null) {
user.incrementLoginFailCount();
userRepository.save(user);
log.warn("用户登录失败: userId={}, 失败次数={}",
user.getId(), user.getLoginFailCount());
}
throw new BusinessException("用户名或密码错误");
}
}
@Override
@Cacheable(value = "user", key = "#id")
public UserDTO getUserById(Long id) {
log.debug("查询用户: id={}", id);
User user = userRepository.findById(id)
.orElseThrow(() -> new BusinessException("用户不存在"));
return userMapper.toDTO(user);
}
@Override
@Cacheable(value = "user", key = "'username:' + #username")
public UserDTO getUserByUsername(String username) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new BusinessException("用户不存在"));
return userMapper.toDTO(user);
}
@Override
public Page<UserDTO> getUsers(Pageable pageable) {
log.debug("分页查询用户: page={}, size={}", pageable.getPageNumber(), pageable.getPageSize());
Page<User> userPage = userRepository.findAll(pageable);
return userPage.map(userMapper::toDTO);
}
@Override
public Page<UserDTO> searchUsers(String keyword, UserStatus status, Pageable pageable) {
log.debug("搜索用户: keyword={}, status={}", keyword, status);
Specification<User> spec = (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if (StringUtils.hasText(keyword)) {
String likeKeyword = "%" + keyword + "%";
Predicate usernamePredicate = cb.like(root.get("username"), likeKeyword);
Predicate emailPredicate = cb.like(root.get("email"), likeKeyword);
Predicate phonePredicate = cb.like(root.get("phone"), likeKeyword);
predicates.add(cb.or(usernamePredicate, emailPredicate, phonePredicate));
}
if (status != null) {
predicates.add(cb.equal(root.get("status"), status));
}
return cb.and(predicates.toArray(new Predicate[0]));
};
Page<User> userPage = userRepository.findAll(spec, pageable);
return userPage.map(userMapper::toDTO);
}
@Override
@Transactional
@CacheEvict(value = "user", key = "#id")
public UserDTO updateUser(Long id, UserUpdateRequest request) {
log.info("更新用户信息: id={}", id);
User user = userRepository.findById(id)
.orElseThrow(() -> new BusinessException("用户不存在"));
// 验证邮箱是否被其他用户使用
if (StringUtils.hasText(request.getEmail()) &&
!request.getEmail().equals(user.getEmail()) &&
userRepository.existsByEmail(request.getEmail())) {
throw new BusinessException("邮箱已被其他用户使用");
}
// 验证手机号是否被其他用户使用
if (StringUtils.hasText(request.getPhone()) &&
!request.getPhone().equals(user.getPhone()) &&
userRepository.existsByPhone(request.getPhone())) {
throw new BusinessException("手机号已被其他用户使用");
}
// 更新用户信息
userMapper.updateEntityFromDTO(request, user);
user = userRepository.save(user);
return userMapper.toDTO(user);
}
@Override
@Transactional
@CacheEvict(value = "user", key = "#id")
public void deleteUser(Long id) {
log.info("删除用户: id={}", id);
User user = userRepository.findById(id)
.orElseThrow(() -> new BusinessException("用户不存在"));
// 软删除:修改状态为已删除
user.setStatus(UserStatus.DELETED);
userRepository.save(user);
}
@Override
@Transactional
public void changePassword(ChangePasswordRequest request) {
// 获取当前登录用户
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
User user = userDetails.getUser();
// 验证旧密码
if (!passwordEncoder.matches(request.getOldPassword(), user.getPassword())) {
throw new BusinessException("旧密码错误");
}
// 更新密码
user.setPassword(passwordEncoder.encode(request.getNewPassword()));
userRepository.save(user);
log.info("用户修改密码成功: userId={}", user.getId());
}
@Override
@Transactional
@CacheEvict(value = "user", key = "#userId")
public void assignRoles(Long userId, Set<Long> roleIds) {
log.info("为用户分配角色: userId={}, roleIds={}", userId, roleIds);
User user = userRepository.findById(userId)
.orElseThrow(() -> new BusinessException("用户不存在"));
List<Role> roles = roleRepository.findByIdIn(new ArrayList<>(roleIds));
if (roles.size() != roleIds.size()) {
throw new BusinessException("部分角色不存在");
}
user.getRoles().clear();
user.getRoles().addAll(roles);
userRepository.save(user);
}
@Override
public boolean isUsernameAvailable(String username) {
return !userRepository.existsByUsername(username);
}
@Override
public boolean isEmailAvailable(String email) {
return !userRepository.existsByEmail(email);
}
@Override
public UserDTO getCurrentUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
throw new BusinessException("用户未登录");
}
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
return getUserById(userDetails.getUser().getId());
}
}
// 3. MapStruct映射器
package com.example.usermanagement.service.mapper;
import com.example.usermanagement.dto.user.UserDTO;
import com.example.usermanagement.dto.user.UserRegisterRequest;
import com.example.usermanagement.dto.user.UserUpdateRequest;
import com.example.usermanagement.entity.User;
import org.mapstruct.*;
@Mapper(componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.IGNORE,
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
public interface UserMapper {
UserDTO toDTO(User user);
@Mapping(target = "id", ignore = true)
@Mapping(target = "roles", ignore = true)
@Mapping(target = "status", ignore = true)
@Mapping(target = "createdBy", ignore = true)
@Mapping(target = "createdAt", ignore = true)
@Mapping(target = "updatedBy", ignore = true)
@Mapping(target = "updatedAt", ignore = true)
User toEntity(UserRegisterRequest request);
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void updateEntityFromDTO(UserUpdateRequest request, @MappingTarget User user);
}
3.4 控制器层(1分钟)
用户控制器:
java
package com.example.usermanagement.controller;
import com.example.usermanagement.dto.ApiResponse;
import com.example.usermanagement.dto.user.;
import com.example.usermanagement.enums.UserStatus;
import com.example.usermanagement.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.;
import java.util.Set;
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
@Tag(name = "用户管理", description = "用户管理相关接口")
public class UserController {
private final UserService userService;
@PostMapping("/register")
@Operation(summary = "用户注册", description = "新用户注册接口")
@ResponseStatus(HttpStatus.CREATED)
public ApiResponse<UserDTO> register(@Valid @RequestBody UserRegisterRequest request) {
return ApiResponse.success("注册成功", userService.register(request));
}
@PostMapping("/login")
@Operation(summary = "用户登录", description = "用户登录接口")
public ApiResponse<UserLoginResponse> login(@Valid @RequestBody UserLoginRequest request) {
return ApiResponse.success("登录成功", userService.login(request));
}
@GetMapping("/me")
@Operation(summary = "获取当前用户信息", description = "获取当前登录用户的信息")
@PreAuthorize("isAuthenticated()")
public ApiResponse<UserDTO> getCurrentUser() {
return ApiResponse.success(userService.getCurrentUser());
}
@GetMapping("/{id}")
@Operation(summary = "根据ID获取用户", description = "根据用户ID获取用户详细信息")
@PreAuthorize("hasAuthority('user:read')")
public ApiResponse<UserDTO> getUserById(
@Parameter(description = "用户ID") @PathVariable Long id) {
return ApiResponse.success(userService.getUserById(id));
}
@GetMapping
@Operation(summary = "分页查询用户", description = "分页查询用户列表")
@PreAuthorize("hasAuthority('user:read')")
public ApiResponse<Page<UserDTO>> getUsers(
@Parameter(description = "页码,从0开始") @RequestParam(defaultValue = "0") int page,
@Parameter(description = "每页大小") @RequestParam(defaultValue = "20") int size,
@Parameter(description = "排序字段") @RequestParam(defaultValue = "createdAt") String sort,
@Parameter(description = "排序方向") @RequestParam(defaultValue = "DESC") String direction) {
Sort.Direction sortDirection = Sort.Direction.fromString(direction);
Pageable pageable = PageRequest.of(page, size, Sort.by(sortDirection, sort));
return ApiResponse.success(userService.getUsers(pageable));
}
@GetMapping("/search")
@Operation(summary = "搜索用户", description = "根据条件搜索用户")
@PreAuthorize("hasAuthority('user:read')")
public ApiResponse<Page<UserDTO>> searchUsers(
@Parameter(description = "搜索关键词") @RequestParam(required = false) String keyword,
@Parameter(description = "用户状态") @RequestParam(required = false) UserStatus status,
@Parameter(description = "页码") @RequestParam(defaultValue = "0") int page,
@Parameter(description = "每页大小") @RequestParam(defaultValue = "20") int size) {
Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt"));
return ApiResponse.success(userService.searchUsers(keyword, status, pageable));
}
@PutMapping("/{id}")
@Operation(summary = "更新用户信息", description = "更新指定用户的信息")
@PreAuthorize("hasAuthority('user:update')")
public ApiResponse<UserDTO> updateUser(
@Parameter(description = "用户ID") @PathVariable Long id,
@Valid @RequestBody UserUpdateRequest request) {
return ApiResponse.success("更新成功", userService.updateUser(id, request));
}
@DeleteMapping("/{id}")
@Operation(summary = "删除用户", description = "删除指定用户(软删除)")
@PreAuthorize("hasAuthority('user:delete')")
public ApiResponse<Void> deleteUser(
@Parameter(description = "用户ID") @PathVariable Long id) {
userService.deleteUser(id);
return ApiResponse.success("删除成功", null);
}
@PutMapping("/change-password")
@Operation(summary = "修改密码", description = "修改当前登录用户的密码")
@PreAuthorize("isAuthenticated()")
public ApiResponse<Void> changePassword(@Valid @RequestBody ChangePasswordRequest request) {
userService.changePassword(request);
return ApiResponse.success("密码修改成功", null);
}
@PutMapping("/{id}/roles")
@Operation(summary = "分配角色", description = "为用户分配角色")
@PreAuthorize("hasAuthority('user:assign-role')")
public ApiResponse<Void> assignRoles(
@Parameter(description = "用户ID") @PathVariable Long id,
@RequestBody Set<Long> roleIds) {
userService.assignRoles(id, roleIds);
return ApiResponse.success("角色分配成功", null);
}
@GetMapping("/check/username")
@Operation(summary = "检查用户名是否可用", description = "检查用户名是否已被注册")
public ApiResponse<Boolean> checkUsername(
@Parameter(description = "用户名") @RequestParam String username) {
return ApiResponse.success(userService.isUsernameAvailable(username));
}
@GetMapping("/check/email")
@Operation(summary = "检查邮箱是否可用", description = "检查邮箱是否已被注册")
public ApiResponse<Boolean> checkEmail(
@Parameter(description = "邮箱") @RequestParam String email) {
return ApiResponse.success(userService.isEmailAvailable(email));
}
}
四、高级特性集成(2分钟)
4.1 安全与JWT配置
Spring Security配置:
java
package com.example.usermanagement.config.security;
import com.example.usermanagement.filter.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 禁用CSRF(因为使用JWT,无状态)
.csrf().disable()
// 配置CORS
.cors().configurationSource(corsConfigurationSource())
.and()
// 异常处理
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
.and()
// 会话管理:无状态
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 授权配置
.authorizeHttpRequests(authz -> authz
// 公开接口
.requestMatchers(
"/api/v1/users/login",
"/api/v1/users/register",
"/api/v1/users/check/**",
"/swagger-ui/**",
"/v3/api-docs/**",
"/api-docs/**",
"/h2-console/**",
"/actuator/health"
).permitAll()
// 需要认证的接口
.anyRequest().authenticated()
)
// 添加JWT过滤器
.addFilterBefore(jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class)
// 禁用缓存
.headers().cacheControl();
// 允许H2控制台iframe(仅开发环境)
http.headers().frameOptions().disable();
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000", "http://localhost:8080"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
// JWT工具类
package com.example.usermanagement.util;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Slf4j
@Component
public class JwtTokenUtil {
@Value("${security.jwt.secret}")
private String secret;
@Value("${security.jwt.expiration}")
private Long expiration;
private Key getSigningKey() {
return Keys.hmacShaKeyFor(secret.getBytes());
}
// 从token中获取用户名
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
// 从token中获取过期时间
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
// 从token中获取指定claim
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
// 从token中获取所有claims
private Claims getAllClaimsFromToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
// 检查token是否过期
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
// 生成token
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return doGenerateToken(claims, userDetails.getUsername());
}
// 生成token的具体实现
private String doGenerateToken(Map<String, Object> claims, String subject) {
final Date createdDate = new Date();
final Date expirationDate = new Date(createdDate.getTime() + expiration);
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(createdDate)
.setExpiration(expirationDate)
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
// 验证token
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
// 刷新token
public String refreshToken(String token) {
final Date createdDate = new Date();
final Date expirationDate = new Date(createdDate.getTime() + expiration);
final Claims claims = getAllClaimsFromToken(token);
claims.setIssuedAt(createdDate);
claims.setExpiration(expirationDate);
return Jwts.builder()
.setClaims(claims)
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
public Long getExpiration() {
return expiration;
}
}
// JWT认证过滤器
package com.example.usermanagement.filter;
import com.example.usermanagement.util.JwtTokenUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenUtil jwtTokenUtil;
private final UserDetailsService userDetailsService;
@Value("${security.jwt.header}")
private String header;
@Value("${security.jwt.token-prefix}")
private String tokenPrefix;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && jwtTokenUtil.validateToken(jwt,
userDetailsService.loadUserByUsername(jwtTokenUtil.getUsernameFromToken(jwt)))) {
String username = jwtTokenUtil.getUsernameFromToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
log.debug("设置用户认证: username={}", username);
}
} catch (Exception e) {
log.error("无法设置用户认证: ", e);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader(header);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(tokenPrefix)) {
return bearerToken.substring(tokenPrefix.length());
}
return null;
}
}
4.2 统一异常处理
java
package com.example.usermanagement.exception;
import lombok.Getter;
import org.springframework.http.HttpStatus;
@Getter
public class BusinessException extends RuntimeException {
private final Integer code;
private final HttpStatus httpStatus;
public BusinessException(String message) {
super(message);
this.code = 400;
this.httpStatus = HttpStatus.BAD_REQUEST;
}
public BusinessException(String message, Integer code) {
super(message);
this.code = code;
this.httpStatus = HttpStatus.BAD_REQUEST;
}
public BusinessException(String message, HttpStatus httpStatus) {
super(message);
this.code = httpStatus.value();
this.httpStatus = httpStatus;
}
}
// 全局异常处理器
package com.example.usermanagement.handler;
import com.example.usermanagement.dto.ApiResponse;
import com.example.usermanagement.exception.BusinessException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiResponse<?>> handleBusinessException(
BusinessException ex, HttpServletRequest request) {
log.warn("业务异常: {} - {}", request.getRequestURI(), ex.getMessage());
ApiResponse<?> response = ApiResponse.error(ex.getCode(), ex.getMessage());
return ResponseEntity.status(ex.getHttpStatus()).body(response);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<?>> handleValidationException(
MethodArgumentNotValidException ex, HttpServletRequest request) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach(error -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
log.warn("参数验证失败: {} - {}", request.getRequestURI(), errors);
ApiResponse<?> response = ApiResponse.error(400, "参数验证失败");
response.setData(errors);
return ResponseEntity.badRequest().body(response);
}
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ApiResponse<?>> handleConstraintViolationException(
ConstraintViolationException ex, HttpServletRequest request) {
Map<String, String> errors = new HashMap<>();
ex.getConstraintViolations().forEach(violation -> {
String fieldName = violation.getPropertyPath().toString();
String errorMessage = violation.getMessage();
errors.put(fieldName, errorMessage);
});
log.warn("约束违反: {} - {}", request.getRequestURI(), errors);
ApiResponse<?> response = ApiResponse.error(400, "参数验证失败");
response.setData(errors);
return ResponseEntity.badRequest().body(response);
}
@ExceptionHandler(BadCredentialsException.class)
public ResponseEntity<ApiResponse<?>> handleBadCredentialsException(
BadCredentialsException ex, HttpServletRequest request) {
log.warn("认证失败: {} - {}", request.getRequestURI(), ex.getMessage());
ApiResponse<?> response = ApiResponse.error(401, "用户名或密码错误");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
}
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ApiResponse<?>> handleAccessDeniedException(
AccessDeniedException ex, HttpServletRequest request) {
log.warn("访问被拒绝: {} - {}", request.getRequestURI(), ex.getMessage());
ApiResponse<?> response = ApiResponse.error(403, "没有访问权限");
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(response);
}
@ExceptionHandler(DataIntegrityViolationException.class)
public ResponseEntity<ApiResponse<?>> handleDataIntegrityViolationException(
DataIntegrityViolationException ex, HttpServletRequest request) {
log.error("数据完整性违反: {} - {}", request.getRequestURI(), ex.getMessage(), ex);
String message = "数据操作失败";
if (ex.getMessage().contains("Duplicate entry")) {
message = "数据已存在";
} else if (ex.getMessage().contains("foreign key constraint")) {
message = "存在关联数据,无法删除";
}
ApiResponse<?> response = ApiResponse.error(400, message);
return ResponseEntity.badRequest().body(response);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<?>> handleGlobalException(
Exception ex, HttpServletRequest request) {
log.error("系统异常: {} - {}", request.getRequestURI(), ex.getMessage(), ex);
ApiResponse<?> response = ApiResponse.error(500, "系统内部错误");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}
}
4.3 数据初始化(30秒)
java
package com.example.usermanagement.config;
import com.example.usermanagement.entity.Permission;
import com.example.usermanagement.entity.Role;
import com.example.usermanagement.entity.User;
import com.example.usermanagement.enums.PermissionType;
import com.example.usermanagement.enums.UserStatus;
import com.example.usermanagement.repository.PermissionRepository;
import com.example.usermanagement.repository.RoleRepository;
import com.example.usermanagement.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.transaction.annotation.Transactional;
import java.util.Arrays;
import java.util.HashSet;
@Slf4j
@Configuration
@RequiredArgsConstructor
public class DataInitializer {
@Bean
@Transactional
public CommandLineRunner initData(
UserRepository userRepository,
RoleRepository roleRepository,
PermissionRepository permissionRepository,
PasswordEncoder passwordEncoder) {
return args -> {
log.info("开始初始化数据...");
// 1. 初始化权限
Permission userRead = createPermissionIfNotExists("用户查看", "user:read",
"查看用户信息", PermissionType.API, "/api/v1/users/**", null, permissionRepository);
Permission userCreate = createPermissionIfNotExists("用户创建", "user:create",
"创建用户", PermissionType.API, "/api/v1/users/register", null, permissionRepository);
Permission userUpdate = createPermissionIfNotExists("用户更新", "user:update",
"更新用户信息", PermissionType.API, "/api/v1/users/**", null, permissionRepository);
Permission userDelete = createPermissionIfNotExists("用户删除", "user:delete",
"删除用户", PermissionType.API, "/api/v1/users/**", null, permissionRepository);
Permission roleRead = createPermissionIfNotExists("角色查看", "role:read",
"查看角色信息", PermissionType.API, "/api/v1/roles/**", null, permissionRepository);
Permission roleManage = createPermissionIfNotExists("角色管理", "role:manage",
"管理角色", PermissionType.API, "/api/v1/roles/**", null, permissionRepository);
// 2. 初始化角色
Role adminRole = createRoleIfNotExists("管理员", "ROLE_ADMIN",
"系统管理员", roleRepository);
Role userRole = createRoleIfNotExists("普通用户", "ROLE_USER",
"普通用户", roleRepository);
// 3. 为角色分配权限
adminRole.setPermissions(new HashSet<>(Arrays.asList(
userRead, userCreate, userUpdate, userDelete, roleRead, roleManage
)));
roleRepository.save(adminRole);
userRole.setPermissions(new HashSet<>(Arrays.asList(
userRead
)));
roleRepository.save(userRole);
// 4. 初始化管理员用户
if (!userRepository.existsByUsername("admin")) {
User admin = new User();
admin.setUsername("admin");
admin.setPassword(passwordEncoder.encode("admin123"));
admin.setEmail("admin@example.com");
admin.setPhone("13800138000");
admin.setStatus(UserStatus.ACTIVE);
admin.setRoles(new HashSet<>(Arrays.asList(adminRole)));
userRepository.save(admin);
log.info("创建管理员用户: admin/admin123");
}
// 5. 初始化普通用户
if (!userRepository.existsByUsername("user")) {
User user = new User();
user.setUsername("user");
user.setPassword(passwordEncoder.encode("user123"));
user.setEmail("user@example.com");
user.setPhone("13800138001");
user.setStatus(UserStatus.ACTIVE);
user.setRoles(new HashSet<>(Arrays.asList(userRole)));
userRepository.save(user);
log.info("创建普通用户: user/user123");
}
log.info("数据初始化完成!");
log.info("管理员账号: admin/admin123");
log.info("普通用户账号: user/user123");
};
}
private Permission createPermissionIfNotExists(String name, String code,
String description, PermissionType type, String url, Long parentId,
PermissionRepository repository) {
return repository.findByCode(code)
.orElseGet(() -> {
Permission permission = new Permission();
permission.setName(name);
permission.setCode(code);
permission.setDescription(description);
permission.setType(type);
permission.setUrl(url);
permission.setParentId(parentId);
permission.setSortOrder(0);
permission.setVisible(true);
return repository.save(permission);
});
}
private Role createRoleIfNotExists(String name, String code,
String description, RoleRepository repository) {
return repository.findByCode(code)
.orElseGet(() -> {
Role role = new Role();
role.setName(name);
role.setCode(code);
role.setDescription(description);
role.setSortOrder(0);
return repository.save(role);
});
}
}
五、测试与部署(1分钟)
5.1 单元测试
java
package com.example.usermanagement.service;
import com.example.usermanagement.dto.user.UserRegisterRequest;
import com.example.usermanagement.entity.User;
import com.example.usermanagement.enums.UserStatus;
import com.example.usermanagement.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.crypto.password.PasswordEncoder;
import static org.junit.jupiter.api.Assertions.;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.;
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private PasswordEncoder passwordEncoder;
@InjectMocks
private UserServiceImpl userService;
private UserRegisterRequest registerRequest;
@BeforeEach
void setUp() {
registerRequest = new UserRegisterRequest();
registerRequest.setUsername("testuser");
registerRequest.setPassword("password123");
registerRequest.setEmail("test@example.com");
registerRequest.setPhone("13800138000");
}
@Test
void testRegister_Success() {
// 模拟Repository行为
when(userRepository.existsByUsername("testuser")).thenReturn(false);
when(userRepository.existsByEmail("test@example.com")).thenReturn(false);
when(userRepository.existsByPhone("13800138000")).thenReturn(false);
when(passwordEncoder.encode("password123")).thenReturn("encodedPassword");
when(userRepository.save(any(User.class))).thenAnswer(invocation -> {
User user = invocation.getArgument(0);
user.setId(1L);
return user;
});
// 执行测试
User user = userService.register(registerRequest);
// 验证结果
assertNotNull(user);
assertEquals(1L, user.getId());
assertEquals("testuser", user.getUsername());
assertEquals("encodedPassword", user.getPassword());
assertEquals("test@example.com", user.getEmail());
assertEquals("13800138000", user.getPhone());
assertEquals(UserStatus.ACTIVE, user.getStatus());
// 验证方法调用
verify(userRepository, times(1)).existsByUsername("testuser");
verify(userRepository, times(1)).existsByEmail("test@example.com");
verify(userRepository, times(1)).existsByPhone("13800138000");
verify(passwordEncoder, times(1)).encode("password123");
verify(userRepository, times(1)).save(any(User.class));
}
@Test
void testRegister_UsernameAlreadyExists() {
// 模拟用户名已存在
when(userRepository.existsByUsername("testuser")).thenReturn(true);
// 执行测试并验证异常
Exception exception = assertThrows(BusinessException.class, () -> {
userService.register(registerRequest);
});
assertEquals("用户名已存在", exception.getMessage());
// 验证方法调用
verify(userRepository, times(1)).existsByUsername("testuser");
verify(userRepository, never()).existsByEmail(anyString());
verify(userRepository, never()).existsByPhone(anyString());
verify(userRepository, never()).save(any(User.class));
}
}
// 集成测试
package com.example.usermanagement.integration;
import com.example.usermanagement.dto.user.UserLoginRequest;
import com.example.usermanagement.dto.user.UserRegisterRequest;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
class UserControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Test
void testRegisterAndLogin() throws Exception {
// 1. 注册新用户
UserRegisterRequest registerRequest = new UserRegisterRequest();
registerRequest.setUsername("integrationuser");
registerRequest.setPassword("password123");
registerRequest.setEmail("integration@example.com");
mockMvc.perform(post("/api/v1/users/register")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(registerRequest)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.message").value("注册成功"))
.andExpect(jsonPath("$.data.username").value("integrationuser"));
// 2. 登录测试
UserLoginRequest loginRequest = new UserLoginRequest();
loginRequest.setUsernameOrEmail("integrationuser");
loginRequest.setPassword("password123");
mockMvc.perform(post("/api/v1/users/login")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(loginRequest)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.message").value("登录成功"))
.andExpect(jsonPath("$.data.accessToken").exists())
.andExpect(jsonPath("$.data.userId").exists());
}
}
5.2 API测试脚本
bash
#!/bin/bash
用户管理系统API测试脚本
BASE_URL="http://localhost:8080/api"
echo "=== 用户管理系统API测试 ==="
echo ""
1. 检查用户名是否可用
echo "1. 检查用户名是否可用:"
curl -X GET "$BASE_URL/v1/users/check/username?username=testuser"
echo ""
echo ""
2. 用户注册
echo "2. 用户注册:"
curl -X POST "$BASE_URL/v1/users/register"
-H "Content-Type: application/json"
-d '{
"username": "testuser",
"password": "password123",
"email": "test@example.com",
"phone": "13800138000"
}'
echo ""
echo ""
3. 用户登录
echo "3. 用户登录:"
LOGIN_RESPONSE= ( c u r l − s − X P O S T " (curl -s -X POST " (curl−s−XPOST"BASE_URL/v1/users/login"
-H "Content-Type: application/json"
-d '{
"usernameOrEmail": "testuser",
"password": "password123"
}')
echo "$LOGIN_RESPONSE"
提取token
TOKEN= ( e c h o " (echo " (echo"LOGIN_RESPONSE" | grep -o '"accessToken":"[^"]*' | cut -d'"' -f4)
echo "Token: $TOKEN"
echo ""
4. 获取当前用户信息
echo "4. 获取当前用户信息:"
curl -X GET "$BASE_URL/v1/users/me"
-H "Authorization: Bearer $TOKEN"
echo ""
echo ""
5. 分页查询用户
echo "5. 分页查询用户:"
curl -X GET "$BASE_URL/v1/users?page=0&size=10"
-H "Authorization: Bearer $TOKEN"
echo ""
5.3 Docker部署配置
dockerfile
Dockerfile
FROM openjdk:17-jdk-slim as builder
WORKDIR /app
复制Maven配置文件
COPY pom.xml .
COPY src ./src
下载Maven wrapper
RUN apt-get update && apt-get install -y maven
构建应用
RUN mvn clean package -DskipTests
运行时镜像
FROM openjdk:17-jdk-slim
WORKDIR /app
创建非root用户
RUN groupadd -r spring && useradd -r -g spring spring
USER spring:spring
从构建阶段复制jar文件
COPY --from=builder /app/target/*.jar app.jar
设置JVM参数
ENV JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError"
暴露端口
EXPOSE 8080
启动应用
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
yaml
docker-compose.yml
version: '3.8'
services:
MySQL数据库
mysql:
image: mysql:8.0
container_name: user-management-mysql
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: user_management
MYSQL_USER: admin
MYSQL_PASSWORD: admin123
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
networks:
- user-management-network
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
timeout: 20s
retries: 10
Redis缓存
redis:
image: redis:7-alpine
container_name: user-management-redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
networks:
- user-management-network
command: redis-server --appendonly yes
用户管理应用
app:
build: .
container_name: user-management-app
environment:
SPRING_PROFILES_ACTIVE: prod
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/user_management?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
SPRING_DATASOURCE_USERNAME: admin
SPRING_DATASOURCE_PASSWORD: admin123
SPRING_REDIS_HOST: redis
SPRING_REDIS_PORT: 6379
ports:
- "8080:8080"
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_started
networks:
- user-management-network
restart: unless-stopped
networks:
user-management-network:
driver: bridge
volumes:
mysql_data:
redis_data:
六、项目总结与扩展
6.1 10分钟搭建完成清单
text
✅ 已完成的核心功能:
- 用户注册与登录(JWT认证)
- 用户信息管理(CRUD)
- 角色权限管理(RBAC)
- 统一异常处理
- 数据验证
- 分页查询
- 缓存支持
- 日志记录
- API文档(Swagger)
- 单元测试
- Docker部署配置
✅ 已实现的企业级特性:
- 分层架构清晰
- 事务管理
- 密码加密
- 防重复提交
- 接口限流(可扩展)
- 数据审计(创建时间、更新时间)
- 统一响应格式
- 跨域支持
6.2 项目扩展建议
扩展1:添加更多功能模块
java
// 1. 部门管理模块
@Entity
public class Department {
private Long id;
private String name;
private String code;
private Long parentId;
private List users;
}
// 2. 操作日志模块
@Entity
public class OperationLog {
private Long id;
private String username;
private String operation;
private String method;
private String params;
private String ip;
private Long duration;
private LocalDateTime createTime;
}
// 3. 文件上传模块
@Service
public class FileService {
public String uploadAvatar(MultipartFile file) { ... }
public void deleteFile(String filePath) { ... }
}
扩展2:性能优化
yaml
添加二级缓存
spring:
jpa:
properties:
hibernate:
cache:
use_second_level_cache: true
region:
factory_class: org.hibernate.cache.jcache.JCacheRegionFactory
jcache:
provider: org.ehcache.jsr107.EhcacheCachingProvider
连接池优化
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 10
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
pool-name: UserManagementPool
扩展3:安全加固
java
// 1. 添加接口限流
@Component
public class RateLimitAspect {
@Around("@annotation(RateLimit)")
public Object limit(ProceedingJoinPoint joinPoint) { ... }
}
// 2. 添加防SQL注入
@Configuration
public class SecurityFilterConfig {
@Bean
public FilterRegistrationBean sqlInjectionFilter() { ... }
}
// 3. 添加XSS防护
@Component
public class XssFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { ... }
}
6.3 学习收获与下一步
通过本项目你学会了:
✅ SpringBoot项目快速搭建
✅ 企业级项目架构设计
✅ Spring Security + JWT认证
✅ Spring Data JPA数据访问
✅ 统一异常处理
✅ 数据验证与DTO转换
✅ 缓存集成与优化
✅ 单元测试编写
✅ API文档生成
✅ Docker容器化部署
下一步学习建议:
text
路线图:
阶段1:基础巩固(已完成)
✓ SpringBoot用户管理系统
阶段2:技能提升
→ 学习Spring Cloud微服务
→ 学习消息队列(RabbitMQ/Kafka)
→ 学习搜索引擎(Elasticsearch)
阶段3:架构进阶
→ 学习分布式事务
→ 学习服务网格(Istio)
→ 学习云原生技术(K8s)
阶段4:实战项目
→ 电商系统
→ 在线教育平台
→ 智能客服系统
七、资源下载与互动
7.1 完整项目资源
GitHub仓库:
text
项目地址:https://github.com/yourusername/user-management-system
包含:
├── 完整源代码
├── SQL初始化脚本
├── Postman接口测试集合
├── Docker部署配置
└── 项目文档
快速启动命令:
bash
克隆项目
git clone https://github.com/yourusername/user-management-system.git
进入项目目录
cd user-management-system
运行项目
mvn spring-boot:run
或者使用Docker
docker-compose up -d
7.2 互动与答疑
常见问题解答:
text
Q1: 为什么我的项目启动失败?
A: 检查Java版本(需要Java 17+),检查端口是否被占用,检查依赖下载是否完整。
Q2: 如何连接MySQL而不是H2?
A: 修改application.yml中的数据库配置,将H2改为MySQL。
Q3: 如何修改JWT密钥和过期时间?
A: 修改application.yml中的security.jwt配置。
Q4: 如何添加新的权限?
A: 在DataInitializer中添加新的权限定义,并为角色分配权限。
学习交流群:
text
扫描二维码加入SpringBoot学习交流群:
二维码图片
群内提供:
- 项目答疑
- 学习资料分享
- 技术交流
- 面试指导
7.3 下期预告
《SpringBoot实战二:电商系统从0到1》
text
你将学会:
- 商品管理模块设计
- 购物车与订单系统
- 支付接口集成
- 库存管理
- 秒杀系统设计
- 分布式锁应用
- 性能压测与优化
- 微服务拆分
预计发布时间:下周
记得关注我,不错过更新!
结语:从学习者到实践者
恭喜你!在短短10分钟内(实际上可能需要1-2小时仔细实践),你已经完成了一个企业级的用户管理系统。这不是一个玩具项目,而是真正可以在生产环境中使用的系统。
记住关键点:
不要只复制代码,要理解每一行代码的作用
动手实践是最好的学习方式
遇到问题是学习的机会
持续学习,技术永远在更新
你的收获:
✅ 一个完整的SpringBoot项目经验
✅ 企业级项目架构的理解
✅ 面试中可以展示的实际项目
✅ 继续深入学习的基础
下一步行动:
运行项目,确保一切正常
尝试修改和扩展功能
部署到云服务器
在GitHub上Star项目
在评论区分享你的学习心得
最后的话:
编程之路没有捷径,但有正确的方法。
从今天起,你不再只是SpringBoot的学习者,
而是拥有实战经验的开发者。
保持热情,持续学习,未来可期!
特别福利:
关注我并私信"用户管理系统",获取:
本文完整Markdown版本
项目PPT演示文稿
面试常见问题及答案
扩展功能实现代码
今日打卡任务:
👍 点赞本文,让更多人看到
💾 收藏本文,方便随时查阅
🚀 运行项目,在评论区分享截图
👨💻 关注我,获取更多实战教程
在评论区告诉我:
你在实现过程中遇到了什么问题?
你还想学习什么类型的项目?
对本文有什么建议?
让我们一起进步,成为更好的开发者! 💪🔥