diff --git a/exam-api/src/main/java/com/bc/exam/ExamApplication.java b/exam-api/src/main/java/com/bc/exam/ExamApplication.java index cd73a46..baceea1 100644 --- a/exam-api/src/main/java/com/bc/exam/ExamApplication.java +++ b/exam-api/src/main/java/com/bc/exam/ExamApplication.java @@ -15,7 +15,9 @@ import java.net.UnknownHostException; import java.util.List; /** - * 启动类 + * @Description 描述:应用启动类,Spring Boot主入口及环境初始化 + * @Author A贾宇婷034244310 + * @Date 20260615 */ @Log4j2 @SpringBootApplication diff --git a/exam-api/src/main/java/com/bc/exam/ability/shiro/ShiroRealm.java b/exam-api/src/main/java/com/bc/exam/ability/shiro/ShiroRealm.java index 4d53b78..3d862d2 100644 --- a/exam-api/src/main/java/com/bc/exam/ability/shiro/ShiroRealm.java +++ b/exam-api/src/main/java/com/bc/exam/ability/shiro/ShiroRealm.java @@ -24,14 +24,27 @@ import java.util.List; /** + * Shiro自定义Realm实现 + *
+ * 继承AuthorizingRealm,实现两个核心职责: + * 1. 认证(Authentication):通过doGetAuthenticationInfo校验用户JWT Token的合法性 + * 2. 授权(Authorization):通过doGetAuthorizationInfo加载用户的角色和权限信息 + *
+ * 当JwtFilter调用Subject.login()时,Shiro框架会自动调用本类中的认证和授权方法。 + *
+ * Shiro框架通过此方法判断是否将Token交给当前Realm处理。 + * 仅当Token为JwtToken类型时,才会调用本Realm的认证/授权方法。 + *
+ * 当请求中使用了@RequiresRoles、@RequiresPermissions等Shiro注解时, + * Shiro框架会自动调用此方法获取用户的授权信息,用于后续的权限判断。 + *
+ * 当JwtFilter调用Subject.login(jwtToken)时,Shiro框架会回调此方法。 + * 方法内部提取Token中的用户信息并校验Token有效性, + * 返回的SimpleAuthenticationInfo中principal为用户对象,credentials为原始Token字符串。 + *
+ * Token校验流程分为三步: + * 1. 从Token中解析用户名(JWT Payload部分) + * 2. 根据用户名从数据库中查询用户信息 + * 3. 验证Token的签名和过期时间 + * 任一步骤失败都会抛出AuthenticationException异常。 + *
+ * 当用户信息发生变更(如角色调整、权限修改)时,可调用此方法 + * 清除该用户在Shiro中的缓存,使下次请求时重新执行认证和授权逻辑。 + *
+ * 作为Shiro过滤链中的核心认证组件,拦截所有需要鉴权的HTTP请求。 + * 工作流程:从请求头中提取JWT Token -> 封装为JwtToken对象 -> 提交给ShiroRealm进行认证。 + * 继承BasicHttpAuthenticationFilter以融入Shiro的过滤器链机制。 + *
+ * Shiro在处理每个请求时会调用此方法来决定是否放行。 + * 内部调用executeLogin完成JWT认证,认证成功则放行,失败则返回统一错误响应。 + *
+ * 从HTTP请求头中提取JWT Token,封装为Shiro可识别的JwtToken对象, + * 然后提交给Shiro的Subject进行登录。Shiro会将JwtToken传递给ShiroRealm的 + * doGetAuthenticationInfo方法完成实际的身份校验。 + *
+ * 拦截所有Controller方法的返回结果,自动将带有@Dict注解的字段从字典编码翻译为可读文本。 + * 翻译后的文本以"字段名_dictText"的形式追加到返回的JSON对象中。 + * 同时处理Date类型字段的格式化输出。 + *
+ * 支持的数据结构: + * - 单个对象:直接遍历字段进行翻译 + * - 列表(List):遍历每个元素进行翻译 + * - 分页结果(IPage):遍历分页记录进行翻译 + * - 嵌套List字段:递归处理子列表中的字典翻译 + *
+ * 注意:当前@Component注解被注释,如需启用字典翻译功能需取消注释或通过其他方式注册为Spring Bean。 + *
+ * 切点表达式匹配com.bc.exam包下所有Controller类的public方法, + * 在方法执行完成后对返回结果进行字典翻译处理。 + *
+ * 调用前必须确保SysDictService已正确配置并注入。 + *
+ * 仅对ApiRest类型的返回值进行字典翻译处理,其他类型直接透传。 + *
+ * 根据data字段的实际类型分三种情况处理: + * 1. 分页对象(IPage):遍历分页记录逐条翻译 + * 2. 列表对象(List):遍历列表元素逐条翻译 + * 3. 单个对象:直接进行字段翻译 + * 基本数据类型和null值不做处理。 + *
+ * 处理流程: + * 1. 将对象序列化为JSON再解析为JSONObject,便于动态添加翻译字段 + * 2. 遍历对象所有字段,按类型分别处理: + * - List类型字段:递归调用processList处理嵌套列表 + * - 带@Dict注解的字段:查询字典服务翻译编码为可读文本,结果以"字段名_dictText"存储 + * - Date类型字段:按@JsonFormat注解指定的格式或默认格式(yyyy-MM-dd HH:mm:ss)格式化 + *
+ * 通过反射获取List的泛型参数确定元素实际类型, + * 然后将每个列表元素反序列化为实际类型后递归调用parseObject进行字典翻译。 + * 基本数据类型的列表(如List<String>)无需处理直接返回。 + *
+ * 通过SysDictService查询指定字典表中,匹配code字段值为key的记录, + * 返回其text字段的值作为翻译结果。 + *
+ * 基本类型包括:8种包装类型(Integer/Byte/Long/Double/Float/Character/Short/Boolean)、 + * String类型以及Number类型。这些类型不包含@Dict注解字段,无需进行字典翻译处理。 + *
+ * 继承自MyBatis-Plus的PaginationInterceptor,在提供分页能力的同时扩展了自定义查询条件注入功能。 + * 核心作用:拦截所有SELECT查询,将SQL中的{{userId}}占位符自动替换为当前登录用户的ID, + * 实现数据权限的自动过滤,无需在业务代码中手动拼接用户ID条件。 + *
+ * 工作原理: + * 1. 通过@Intercepts注解拦截StatementHandler的prepare方法(SQL准备阶段) + * 2. 获取原始SQL,检查是否包含{{userId}}占位符 + * 3. 若包含占位符,通过Shiro获取当前登录用户ID并进行替换 + * 4. 将替换后的SQL回写到BoundSql中,再交由父类PaginationInterceptor处理分页逻辑 + *
+ * 处理流程: + * 1. 通过MetaObject反射获取MappedStatement和BoundSql + * 2. 仅对SELECT类型的SQL进行处理 + * 3. 检查SQL中是否包含{{userId}}占位符,不包含则跳过 + * 4. 替换占位符后回写SQL,再交由父类处理分页 + *
+ * 使用JSqlParser解析SQL语句,在处理完用户ID替换后重新生成SQL字符串。 + * 解析失败时返回原始SQL,确保不影响正常查询执行。 + *
+ * 拦截MyBatis的所有INSERT和UPDATE操作,自动为实体对象中的时间字段赋值: + * - INSERT操作:同时自动填充createTime(创建时间)和updateTime(更新时间) + * - UPDATE操作:仅自动填充updateTime(更新时间) + *
+ * 该拦截器通过反射机制扫描实体对象的所有字段(包括父类字段), + * 当字段名匹配"createTime"或"updateTime"时自动赋值当前时间戳, + * 避免在业务代码中手动设置时间字段,减少重复代码。 + *
+ * 使用方式:实体类中声明createTime和updateTime字段即可自动生效, + * 字段类型需为java.sql.Timestamp或兼容类型。 + *
+ * 处理逻辑: + * 1. 获取SQL操作类型(INSERT或UPDATE)和操作的实体对象 + * 2. 通过反射获取实体所有字段(包括父类继承的字段) + * 3. 遍历字段,匹配createTime和updateTime并按规则赋值当前时间戳 + *
+ * 仅对Executor类型的目标对象进行代理包装,其他类型直接返回原对象。 + *
+ * 通过注册CorsFilter过滤器,统一处理所有HTTP请求的跨域问题。 + * 将过滤器优先级设置为最高(HIGHEST_PRECEDENCE),确保跨域处理在其他过滤器之前执行, + * 避免预检请求(OPTIONS)被拦截导致跨域失败。 + *
+ * 配置允许所有来源、所有请求头、所有请求方法的跨域访问, + * 并将过滤器优先级设为最高,保证跨域请求能被正确处理。 + *
+ * 负责配置MyBatis的核心组件: + * 1. 通过@MapperScan注解自动扫描指定包路径下的Mapper接口,避免逐个注册 + * 2. 注册查询拦截器(QueryInterceptor),用于自动填充通用查询条件(如用户ID过滤) + * 3. 注册更新拦截器(UpdateInterceptor),用于自动填充createTime和updateTime字段 + *
注意:拦截器的注册顺序很重要,QueryInterceptor继承自分页拦截器,需要优先注册以保证分页逻辑正常执行。
+ * 该拦截器继承自MyBatis-Plus的分页拦截器(PaginationInterceptor), + * 同时扩展了自定义的查询条件自动填充功能(如将SQL中的{{userId}}占位符替换为当前登录用户ID)。 + * limit设置为-1表示默认不限制查询条数,由业务层自行控制分页。 + *
+ * 拦截所有INSERT和UPDATE操作,自动为实体对象的createTime和updateTime字段赋值。 + * INSERT时同时设置createTime和updateTime,UPDATE时仅设置updateTime。 + *
+ * Filter Chain定义说明: * 1、一个URL可以配置多个Filter,使用逗号分隔 * 2、当设置多个过滤器时,全部验证通过,才视为通过 - * 3、部分过滤器可指定参数,如perms,roles + * 3、部分过滤器可指定参数,如perms、roles + *
+ * 配置思路:先定义不需要认证的白名单路径(anon), + * 再对剩余所有路径(/**)统一使用自定义的JwtFilter进行JWT认证。 + * LinkedHashMap保证路径匹配按配置顺序进行。 + *
+ * 核心配置: + * 1. 绑定自定义的ShiroRealm(负责认证和授权逻辑) + * 2. 禁用Shiro内置的Session管理(因为本项目使用无状态的JWT Token认证, + * 不依赖服务端的Session来维持用户登录状态) + *
+ * 使用CGLIB代理(proxyTargetClass=true)而非JDK动态代理, + * 以确保对没有实现接口的类也能正确代理。 + * 设置AdvisorBeanNamePrefix为"_no_advisor"是为了排除不需要被自动代理的Advisor, + * 避免与Shiro的注解拦截器产生冲突。 + *
+ * 负责管理Shiro相关Bean的生命周期回调(如初始化init和销毁destroy方法), + * 确保Shiro的SecurityManager等核心组件能够正确地初始化和释放资源。 + *
+ * 使Shiro的安全注解(如@RequiresRoles、@RequiresPermissions、@RequiresAuthentication等) + * 能够生效。该Advisor会拦截带有安全注解的方法,在方法执行前进行权限校验。 + *
+ * 通过Spring AOP切面机制,自动为Service层的业务方法添加事务管理, + * 无需在每个方法上手动添加@Transactional注解。 + * 根据方法名前缀自动匹配事务规则: + * - 增删改操作(insert/add/create/up/set/remove/delete等)使用读写事务 + * - 查询操作(query/select/get/find等)使用只读事务,数据库可对只读操作进行性能优化 + *
+ * 匹配com.bc.exam.modules包下所有子模块的service.impl包中所有类的所有方法, + * 即所有Service实现类的方法都会被事务拦截。 + *
+ * 根据方法名前缀定义两类事务规则: + * 1. 写操作(CUD):使用PROPAGATION_REQUIRED传播行为 + ISOLATION_READ_COMMITTED隔离级别 + 遇到Exception即回滚 + * 2. 读操作(R):使用PROPAGATION_SUPPORTS传播行为 + 只读模式,数据库可针对只读做优化 + *
+ * 将事务拦截器(txAdvice)绑定到Service实现类的切点上, + * 使得符合切点表达式的方法在执行时自动被事务管理。 + *
+ * 所有Controller接口的返回值都使用此类进行统一包装,确保前后端交互的数据格式一致。 + * 响应结构包含三个核心字段: + * - code: 业务状态码,0表示成功,1表示失败 + * - msg: 响应消息描述 + * - data: 响应数据体,泛型T支持任意业务数据类型 + *
+ * 使用示例: + *
+ * // 成功响应 + * return new ApiRest<>(); // 配合BaseController的success()方法使用 + * // 异常响应 + * return new ApiRest<>(serviceException); // 从异常构造错误响应 + *
0 = 操作成功,1 = 操作失败
将业务异常的code和msg映射到响应对象中
将API错误枚举的code和msg映射到响应对象中
+ * 所有业务Controller的公共基类,提供统一的响应构造方法, + * 封装success(成功)和failure(失败)两种响应模式及其多种重载形式, + * 使子类Controller只需调用简洁的方法即可返回规范化的ApiRest响应。 + *
+ * 响应状态约定: + * - code=0 表示操作成功 + * - code=1 表示操作失败 + *
用于不需要返回数据的操作,如删除、状态更新等
将ServiceException中的错误码和错误信息映射到统一响应格式
+ * 使用@RestControllerAdvice注解实现全局统一的异常捕获和处理, + * 确保所有Controller层抛出的异常都能被统一拦截并返回规范化的JSON响应。 + * 避免在每个Controller中重复编写try-catch代码,实现异常处理的集中化管理。 + *
+ * 处理机制: + * - @RestControllerAdvice = @ControllerAdvice + @ResponseBody, + * 异常处理结果自动序列化为JSON格式返回给前端 + * - @ExceptionHandler指定要捕获的异常类型及其处理方法 + * - @InitBinder和@ModelAttribute提供数据绑定和模型属性的全局预处理能力 + *
+ * 应用到所有@RequestMapping注解方法,在其执行之前初始化数据绑定器。 + * 可用于注册自定义的属性编辑器(PropertyEditor)或类型转换器(Converter), + * 当前为空实现,预留扩展点。 + *
+ * 在每个@RequestMapping方法执行前调用,可向Model中添加公共属性, + * 使得所有Controller方法都能通过@ModelAttribute获取到这些值。 + * 当前为空实现,预留扩展点。 + *
+ * 当Service层抛出ServiceException时,自动将异常中的错误码和错误信息 + * 封装为ApiRest统一响应格式返回给前端,HTTP状态码固定为200(OK), + * 通过ApiRest中的code字段区分业务成功/失败状态。 + *
查询当前用户的借阅/归还记录,支持按借还状态过滤,以及按关联书籍的ISBN/书名/作者/类型模糊搜索。 + * 查询使用自定义SQL(baseMapper.paging),涉及借还表(a)与书籍表(b)的关联查询。
支持多条件组合查询:按ID精确查询、按书籍类型筛选,以及按ISBN/书名/作者模糊搜索,按状态过滤
+ * 提供与当前登录用户相关的静态工具方法,封装Shiro安全框架的用户信息获取逻辑。 + * 所有方法均为静态方法,可在任意位置直接调用,无需注入。 + *
+ * 核心功能:从Shiro的SecurityContext中获取当前登录用户的ID, + * 支持两种模式:抛异常模式(默认)和静默模式(返回null)。 + *
+ * 从Shiro安全框架的Subject中获取当前登录用户的Principal信息, + * 提取用户ID返回。支持通过throwable参数控制未登录时的行为: + * - throwable=true: 未登录时抛出ServiceException(错误码ApiError.ERROR_10010002) + * - throwable=false: 未登录时静默返回null + *
+ * 便捷方法,等同于getUserId(true)。 + * 适用于必须登录才能访问的接口,未登录时直接抛出异常中断请求。 + *
查询系统中所有角色的分页列表,当前无条件过滤(全量分页)
根据用户ID查询用户角色关联表,提取所有角色ID列表,用于登录时返回角色信息和权限判断
登录流程:1.根据用户名查询用户 -> 2.校验账号状态 -> 3.验证密码 -> 4.生成Token -> 5.加载字典数据
用于前端携带Token访问接口时,验证Token有效性并刷新会话信息
仅清除当前Shiro会话,不做数据库层面的Token失效处理(JWT无状态特性)
仅当请求中包含新密码时才执行修改操作,使用当前登录用户ID定位记录
包含用户基本信息和角色关联的保存,整个操作在事务中执行。 + * 新增时自动生成雪花ID,修改密码时对密码加盐加密。
注册流程:1.检查用户名唯一性 -> 2.创建用户并加密密码 -> 3.默认分配"student"角色 -> 4.自动登录返回Token
适用于第三方快捷登录场景:先检查用户名是否已存在,已存在则直接登录,不存在则走注册流程
核心方法:为登录/注册/Token刷新场景统一生成JWT Token,并查询该用户的所有角色标识
通过模糊匹配roleIds字段中包含"ca"的记录,筛选出教师/教务类用户