作为一个前端,想要系统性的学习搭建一个基于RBAC权限的后台管理系统,于是就有了这个系列的文章,系列文章旨在记录学习一个后台管理系统的过程,非构建一个可直接部署于生产环境的商业级应用,配套前端工程见Github
创建一个SptingBoot项目
首先没有选择单体项目,直接选择多模块项目入手,多模块还不是微服务。
创建项目很简单,参考文章IntelliJ IDEA 构建 Spring Boot 多模块 Maven 项目的方法比较,
- 目录机构如下
Demo-end/ <!-- 项目根目录 -->
├── .gitignore <!-- Git忽略文件配置 -->
├── demo-api/ <!-- 业务API接口模块 -->
├── demo-common/ <!-- 通用工具模块 -->
├── demo-framework/ <!-- 框架模块 -->
├── demo-generator/ <!-- 代码生成器模块 -->
├── demo-system/ <!-- 系统管理模块(用户管理,角色管理等) -->
├── doc/ <!-- 文档目录 -->
└── pom.xml <!-- Maven项目配置文件 -->
- 在
demo-api模块新建应用启动类
/**
* 后台管理系统启动类
*/
@SpringBootApplication
public class RbacAdminApplication {
public static void main(String[] args) {
SpringApplication.run(RbacAdminApplication.class, args);
}
}
- 在
demo-api模块新建controller测试类,成功启动项目后浏览器访问hello接口返回数据,项目创建成功
/**
* 测试控制器
*/
@RestController
public class HelloWorldController {
/**
* 测试接口
* @return 测试数据
*/
@GetMapping("/hello")
public Map<String, Object> hello() {
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("message", "Hello World!");
result.put("data", "RBAC后台管理系统测试成功");
return result;
}
}
进行一些简单初始化
项目已经创建完成,在正式进入开发前先做一些基础工作:logback配置,日志切面配置、Lombok测试、Hibernate Validator测试、SpringDoc配置、添加统一响应和用异常处理机制
配置日志
可参考相关文章:Java应用日志管理:日志库、日志门面及其在SpringBoot中的整合策略
- 配置logback日志,并测试配置成功
需求:手动添加的日志和自动记录的接口的日志可以存储到文件中,区分操作系统
- 添加
logbck-spring.yml配置appendar和不同环境下的日志输出规则。
logback配置文件使用条件表达式需要添加 janino依赖
<dependency>
<groupId>org.codehaus.janino</groupId>
<artifactId>janino</artifactId>
<version>${janino.version}</version>
</dependency>
<?xml version="1.0" encoding="UTF-8"?>
<!-- Logback配置文件 - 用于控制日志的输出行为、格式和存储方式 -->
<configuration>
<!-- 从Spring配置文件中读取基础包名配置 -->
<springProperty scope="context" name="basePackage" source="app.base-package"/>
<!-- 直接读取应用名称 -->
<springProperty scope="context" name="appName" source="spring.application.name" defaultValue="rbac-admin"/>
<!-- 根据操作系统类型选择日志路径 -->
<if condition='property("os.name").toLowerCase().contains("windows")'>
<then>
<property name="log.path" value="D:\${appName}\logs"/>
</then>
<else>
<property name="log.path" value="/var/log/${appName}"/>
</else>
</if>
<!-- 日志输出格式 - 定义日志的显示格式,包含以下元素:
%d{yyyy-MM-dd HH:mm:ss.SSS} - 日期时间,精确到毫秒
[%thread] - 输出日志的线程名
%-5level - 日志级别,用5个字符右对齐(如:INFO、ERROR)
%logger{20} - 输出日志的logger名,最大20个字符
[%method,%line] - 输出日志的方法名和行号
%msg%n - 日志消息和换行符
-->
<property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
<!-- 控制台输出 - 将日志输出到控制台,主要用于开发调试阶段查看实时日志 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${log.pattern}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 系统日志输出 - 将INFO级别的日志输出到文件,用于记录系统的正常运行信息 -->
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/info.log</file>
<!-- 循环政策:基于时间创建日志文件 - 每天创建一个新的日志文件,避免单个文件过大 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/info.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
<charset>UTF-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 - 此appender只接收INFO级别的日志 -->
<level>INFO</level>
<!-- 匹配时的操作:接收(记录)- 当日志级别等于INFO时,接收并记录日志 -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录)- 当日志级别不是INFO时(如DEBUG、WARN、ERROR等),拒绝记录 -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 系统错误日志输出 - 将ERROR级别的日志单独输出到错误日志文件,便于快速定位系统错误 -->
<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/error.log</file>
<!-- 循环政策:基于时间创建日志文件 - 每天创建一个新的日志文件,避免单个文件过大 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/error.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
<charset>UTF-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 - 此appender只接收ERROR级别的日志 -->
<level>ERROR</level>
<!-- 匹配时的操作:接收(记录)- 当日志级别等于ERROR时,接收并记录日志 -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录)- 当日志级别不是ERROR时(如DEBUG、INFO、WARN等),拒绝记录 -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 异步输出 - 通过异步方式处理日志,提高系统性能,减少日志记录对主业务的影响 -->
<appender name="async_info" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丢失日志,默认的,如果队列的80%已满,则会丢弃TRACE、DEBUG、INFO级别的日志 -->
<!-- 设置为0表示不丢弃任何日志,即使队列已满也会阻塞而不是丢弃日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 更改默认的队列深度,该值会影响性能,默认值为256 -->
<!-- 队列越大,可以缓存的日志越多,但会占用更多内存 -->
<queueSize>512</queueSize>
<!-- 添加附加的appender,最多只能添加一个 -->
<!-- 此处引用file_info appender,表示异步处理INFO级别的日志 -->
<appender-ref ref="file_info"/>
</appender>
<!-- 异步错误日志输出 - 单独处理ERROR级别的日志,确保错误日志不会丢失 -->
<appender name="async_error" class="ch.qos.logback.classic.AsyncAppender">
<!-- 设置为0表示即使队列满了也不丢弃错误日志,确保所有错误都被记录 -->
<discardingThreshold>0</discardingThreshold>
<!-- 队列大小设置为512,提供足够的缓冲空间处理突发的错误日志 -->
<queueSize>512</queueSize>
<!-- 引用file_error appender,表示异步处理ERROR级别的日志 -->
<appender-ref ref="file_error"/>
</appender>
<!-- 开发环境 - 开发阶段使用的日志配置,提供更详细的日志信息辅助开发调试 -->
<springProfile name="dev">
<!-- root是默认的日志配置,level="info"表示默认记录INFO及以上级别(INFO、WARN、ERROR)的日志 -->
<root level="info">
<!-- 开发环境下同时输出到控制台和文件,方便开发调试 -->
<appender-ref ref="console" />
<appender-ref ref="async_info" />
<appender-ref ref="async_error" />
</root>
<!-- 项目自身的包设置为debug级别,可以看到更详细的调试信息 -->
<logger name="${basePackage}" level="debug" />
</springProfile>
<!-- 测试环境 - 用于测试阶段的日志配置,保留必要的信息用于问题分析 -->
<springProfile name="test">
<!-- 测试环境下默认记录INFO及以上级别的日志 -->
<root level="info">
<!-- 测试环境同时输出到控制台和文件,便于测试人员查看日志 -->
<appender-ref ref="console" />
<appender-ref ref="async_info" />
<appender-ref ref="async_error" />
</root>
<!-- 项目自身的包设置为info级别,只记录必要的信息,不记录debug信息 -->
<logger name="${basePackage}" level="info" />
</springProfile>
<!-- 生产环境 - 用于线上运行的日志配置,只记录重要信息,减少日志量,提高性能 -->
<springProfile name="prod">
<!-- 生产环境下默认记录INFO及以上级别的日志 -->
<root level="info">
<!-- 生产环境同样配置了控制台输出,但实际部署时通常只关注文件日志 -->
<appender-ref ref="console" />
<appender-ref ref="async_info" />
<appender-ref ref="async_error" />
</root>
<!-- 项目自身的包设置为warn级别,只记录警告和错误,减少日志数量,提高系统性能 -->
<!-- 这意味着只有WARN和ERROR级别的日志会被记录,而INFO和DEBUG级别的日志会被忽略 -->
<logger name="${basePackage}" level="warn" />
</springProfile>
</configuration>
- 添加测试类。项目启动成功后会初始测试类里的测试日志,同时也会输出日志到文件
/**
* Logback日志框架测试类
* 用于验证日志配置是否生效
* 这个测试类已经配置为Spring组件(@Component)
* 当SpringBoot应用启动时会自动执行run方法
* 只需要确保:
* 1. 项目中包含logback相关依赖(spring-boot-starter-logging)
* 2. resources目录下配置logback-spring.xml或application.properties中配置日志级别
* 3. 直接运行SpringBoot主应用类即可看到不同级别的日志输出
* 4. 默认情况下TRACE和DEBUG级别不会显示,需要在配置文件中调整日志级别
*/
@Component
public class LogbackTest implements CommandLineRunner {
private static final Logger logger = LoggerFactory.getLogger(LogbackTest.class);
@Override
public void run(String... args) {
// 测试不同级别的日志
logger.trace("这是 TRACE 级别的日志");
logger.debug("这是 DEBUG 级别的日志");
logger.info("这是 INFO 级别的日志");
logger.warn("这是 WARN 级别的日志");
logger.error("这是 ERROR 级别的日志");
// // 测试带异常的日志
// try {
// // 模拟一个异常
// int result = 1 / 0;
// } catch (Exception e) {
// logger.error("发生异常:", e);
// }
logger.info("日志框架配置测试完成!");
}
}
- 添加日志切面,记录请求日志
这一步要用到spring-boot-starter-aop关于aop的知识点就先不介绍了。重新启动项目,访问测试接口查看控制台和文件日志
/**
* 操作日志记录处理切面
* 用于记录每个接口的请求参数、响应结果、执行时间等信息
*/
@Aspect
@Component
@Order(1)
@Slf4j
public class LogAspect {
/**
* 配置切入点 - 所有controller包下的所有方法
*/
@Pointcut("execution(* me.parade.controller..*.*(..))")
public void logPointCut() {
}
/**
* 环绕通知,记录请求的各项信息
*
* @param joinPoint 切点
* @return 原方法返回值
* @throws Throwable 执行原方法时可能抛出的异常
*/
@Around("logPointCut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("========== 日志切面开始 ==========");
long startTime = System.currentTimeMillis();
// 获取请求相关信息
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes != null ? attributes.getRequest() : null;
// 获取方法签名
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 记录请求开始日志
if (request != null) {
log.info("========== 请求开始 ==========");
log.info("请求URL: {} {}", request.getMethod(), request.getRequestURI());
log.info("请求方法: {}.{}", signature.getDeclaringTypeName(), method.getName());
log.info("请求IP: {}", getIpAddress(request));
log.info("请求参数: {}", getRequestParams(joinPoint));
}
// 执行原方法
Object result;
try {
result = joinPoint.proceed();
// 记录正常响应日志
long executionTime = System.currentTimeMillis() - startTime;
log.info("响应结果: {}", result);
log.info("执行时间: {}ms", executionTime);
log.info("========== 请求结束 ==========\n");
} catch (Throwable e) {
// 记录异常日志
long executionTime = System.currentTimeMillis() - startTime;
log.error("请求异常: {}", e.getMessage());
log.error("执行时间: {}ms", executionTime);
log.error("========== 请求异常结束 ==========\n");
throw e;
}
return result;
}
/**
* 获取请求参数字符串
*
* @param joinPoint 切点
* @return 请求参数字符串
*/
private String getRequestParams(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
if (args == null || args.length == 0) {
return "无参数";
}
// 过滤掉HttpServletRequest、HttpServletResponse、MultipartFile等特殊参数
List<Object> filteredArgs = Arrays.stream(args)
.filter(arg -> !(arg instanceof HttpServletRequest ||
arg instanceof HttpServletResponse ||
arg instanceof MultipartFile))
.collect(Collectors.toList());
return filteredArgs.toString();
}
/**
* 获取请求IP地址
*
* @param request HTTP请求
* @return IP地址
*/
private String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
Lombok测试
- 添加依赖
<!-- lombok 依赖,仅编译期生效,生产包不包含 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
- 添加实体
@Data
public class LombokDemo {
private Long id;
private String name;
}
- 添加测试类
public class LombokDemoTest {
@Test
void testLombokGetterSetter(){
LombokDemo demo = new LombokDemo();
demo.setId(1L);
demo.setName("测试");
assertEquals(1L, demo.getId());
assertEquals("测试", demo.getName());
}
}
Hibernate Validator测试
- 添加测试
Bean,加上验证注解
@Data
public class UserCreateParam {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 20, message = "密码长度必须在6-20位之间")
private String password;
}
- 添加单测类验证Hibernate Validator校验效果
访问接口参数为空会报400错误,但是没有具体的报错信息。这个时候就
需要使用使用全局异常处理机制
/**
* 创建用户接口
* @param param 用户创建参数
* @return 创建结果
*/
@Operation(summary = "创建用户", description = "创建一个新用户,需要提供用户名等基本信息")
@PostMapping("/user/create")
public String createUser(@Parameter(description = "用户创建参数") @RequestBody @Valid UserCreateParam param) {
logger.info("用户创建请求: {}", param.getUsername());
// 正常情况返回成功
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("message", "成功");
result.put("data", "添加成功");
return result;
}
添加统一响应和用异常处理机制
有统一的响应格式后,前端处理起来会很方便,大部分的格式都是code,msg,data,此外为了避免使用magic number,把结果状态码定义常量
public interface ResultCode {
Integer SUCCESS = 200;
Integer ERROR = 500;
Integer VALIDATE_FAILED = 400;
Integer UNAUTHORIZED = 401;
Integer FORBIDDEN = 403;
Integer NOT_FOUND = 404;
}
/**
* 统一响应结果类
*/
@Data
public class Result<T> {
private Integer code;
private String message;
private T data;
public static <T> Result<T> success() {
return success(null);
}
public static <T> Result<T> success(T data) {
return success("操作成功", data);
}
public static <T> Result<T> success(String message, T data) {
Result<T> result = new Result<>();
result.setCode(ResultCode.SUCCESS);
result.setMessage(message);
result.setData(data);
return result;
}
public static <T> Result<T> error() {
return error(ResultCode.ERROR, "操作失败");
}
public static <T> Result<T> error(String message) {
return error(500, message);
}
public static <T> Result<T> error(Integer code, String message) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
return result;
}
}
统一异常需要@RestControllerAdvice结合@ExceptionHandler轻松搞定
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 处理参数校验异常(@RequestBody参数校验)
* 这里使用ResponseEntity二次封装是为了规范Http的状态码,毕竟Result的code只是自己定义的code,不是HttpStatus
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Result<Void>> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
String message = getBindingResultErrorMessage(bindingResult);
logger.warn("参数校验失败: {}", message);
Result<Void> response = Result.error(ResultCode.VALIDATE_FAILED, message);
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}
//....省略其他异常处理
}
优化controler响应结果:每个controller里的每个请求都用Result二次封装返回结果,有时候也挺麻烦的,可以考虑使用@RestControllerAdvice自动对结果处理
/**
* 统一响应注解
* <p>
* 标记需要被统一响应拦截器处理的控制器方法
* 可以应用在类或方法上,应用在类上表示该类的所有方法都需要统一响应处理
* </p>
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ResponseResult {
}
/**
* 统一响应拦截器
* <p>
* 用于自动将控制器返回值包装成统一的响应格式,避免每个控制器方法都手动包装
* </p>
*/
@RestControllerAdvice(basePackages = {"me.parade"})
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// 如果返回值已经是Result类型,则不需要处理
if (returnType.getParameterType().equals(Result.class)) {
return false;
}
// 检查类或方法是否标记了@ResponseResult注解
boolean hasClassAnnotation = returnType.getContainingClass().isAnnotationPresent(ResponseResult.class);
boolean hasMethodAnnotation = returnType.getMethod() != null &&
returnType.getMethod().isAnnotationPresent(ResponseResult.class);
return hasClassAnnotation || hasMethodAnnotation;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
// 如果返回值为null,则返回成功响应
if (body == null) {
return Result.success();
}
// 如果返回值已经是Result类型,则直接返回
if (body instanceof Result) {
return body;
}
// 特殊处理String类型返回值
// 因为StringHttpMessageConverter会直接将返回值转换为字符串,而不是JSON
if (returnType.getParameterType().equals(String.class)) {
try {
// 设置Content-Type为application/json
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
// 将Result对象转换为JSON字符串
return objectMapper.writeValueAsString(Result.success(body));
} catch (JsonProcessingException e) {
throw new RuntimeException("将对象转换为JSON字符串时发生错误", e);
}
}
// 将返回值包装成统一的Result格式
return Result.success(body);
}
}
测试封装效果,完成每一小步之后,都要测试
集成Mybatis-plus
创建表和实体
参照ruoyi等开源框架先创建本地数据库,主要表有user,role,menu,dept,user_role,role_menu
创建实体类(可使用BaseEntity)来管理一些共性字段create_user,update_time等
添加依赖和配置文件
<!-- 数据库相关依赖版本 -->
<mysql.version>8.0.28</mysql.version>
<druid.version>1.2.16</druid.version>
<mybatis-plus.version>3.5.11</mybatis-plus.version>
<mybatis-plus-generator.version>3.5.11</mybatis-plus-generator.version>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- Druid连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<!-- MyBatis-Plus
<type>pom</type>表示这是一个 POM 类型的依赖(不是 jar 包),只用于依赖管理。
<scope>import</scope>只有在 <dependencyManagement> 里才允许使用 import,表示“导入”另一个 POM 的依赖管理配置。
这样配置后,mybatis-plus-bom 中声明的所有依赖(如 mybatis-plus-core、mybatis-plus-boot-starter 等)都可以在你的子模块中直接引用,无需再单独指定版本号,统一由 BOM 管理。
-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-bom</artifactId>
<version>${mybatis-plus.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
数据库连接配置文件参考ruoyi等开源库😀,考虑区分开发环境和生产环境。重新启动项目,
MybatisPlus配置文件
@Configuration
@MapperScan("me.parade.**.mapper")
public class MyBatisPlusConfig {
private final Environment environment;
//构造函数注入,取代@Autowired
public MyBatisPlusConfig(Environment environment) {
this.environment = environment;
}
/**
* 配置MybatisPlus拦截器
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
/**
* 自动填充处理器
*/
@Bean
public MetaObjectHandler metaObjectHandler() {
return new MetaObjectHandler() {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.class);
this.strictInsertFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
// TODO: 当实现了用户认证后,替换为实际的用户ID
// this.strictInsertFill(metaObject, "createBy", () -> 1L, Long.class);
// this.strictInsertFill(metaObject, "updateBy", () -> 1L, Long.class);
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
// TODO: 当实现了用户认证后,替换为实际的用户ID
// this.strictUpdateFill(metaObject, "updateBy", () -> 1L, Long.class);
}
};
}
}
测试集成效果,如果没报错证明数据库连接成功
集成SpringSecurity和jjwt
首先什么事jwt和jjwt,参考文章jwt和jjwt解读以及应用
添加依赖
<!-- Spring Security依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT相关依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<scope>runtime</scope>
</dependency>
准备相关工具和服务
要实现完整的认证和授权流程,在正式进入开发之前,要准备一些工具,
- jwt工具类(主要负责生成token和解析token),
- Secuity配置
- 配置放行请求(无需登录即可访问的公开接口)
- 未认证异常处理器
- 无权限访问异常处理器
- csrf
- session
- 跨域
- jwt验证和解析过滤器
- 认证管理器
- 等
至于jwt工具类和SecurityConfig和jwt过滤器的实现可以参考ruioyi或者项目源码,我也是让AI生成的,关在在于过程
有了基础设置后,就可以开始开发认证接口了
开发认证和授权
认证
public LoginResponse login(String username, String password) {
try {
// 进行身份验证
//1. new UsernamePasswordAuthenticationToken(username, password)创建一个“未认证”的 Authentication 对象包含principal:用户名,credentials:密码,authorities:null示未认证
//
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(username, password)
);
// 认证成功后,将认证信息存入SecurityContext
//设置 SecurityContext,是为了让本次请求线程能获取到认证用户信息,便于后续业务处理。
//典型场景:登录后立即返回用户详情、记录登录日志、触发审计等。
SecurityContextHolder.getContext().setAuthentication(authentication);
// 生成JWT令牌
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String accessToken = jwtTokenUtil.generateAccessToken(userDetails);
String refreshToken = jwtTokenUtil.generateRefreshToken(userDetails);
// 构建并返回登录响应
return LoginResponse.builder()
.token(accessToken)
.refreshToken(refreshToken)
.tokenType(jwtConfig.getTokenPrefix().trim())
.expiresIn(jwtConfig.getAccessTokenExpiration())
.build();
}catch (Exception e){
throw new BusinessException(e.getMessage());
}
}
授权
首先要保证接口没有被放行,接口会经过jwt过滤器,在过滤器里会根据jwt信息解析token的有效性和合法性,没有问题的话会在把用户信息(包括权限信息)一起存到上下文中
在接口使用@PreAuthorize或者自定义注解进行权限校验即可
写在最后
关于登录
可以先写一个测试类,产生一个加密密码,然后复制到数据库,前端就可以使用用户名和密码进行登录了
@SpringBootTest
public class PasswordEncoderTest {
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Test
public void generatePassword() {
String rawPassword = "123456";
String encodedPassword = passwordEncoder.encode(rawPassword);
System.out.println("Encoded password: " + encodedPassword);
// 验证密码是否匹配
boolean matches = passwordEncoder.matches(rawPassword, encodedPassword);
System.out.println("Password matches: " + matches);
}
}
至于业务接口的编写(菜单管理,角色分配,菜单分配等)就不涉及到大量的框架学习,基本是MybatisPlus和java的语法的熟练运用
虽然文档中没有涉及到大量代码,但是源码中有大量的文档和详细的注释,按照这个过程,经历过古法手打自己完成的项目,相信才会记忆深刻,与君共勉