云悦智销项目04_员工部门懒加载的no Session的_一系列问题
云悦智销项目实战:懒加载问题终结篇与 Shiro 权限框架入门
本文是云悦智销项目系列第 04 篇,分为两大模块。上篇:彻底终结懒加载的 noSession/noSerializer/n-to-n 三大问题,给出企业级最佳实践。下篇:全面入门 Apache Shiro 轻量级权限框架——四大基石、核心组件、RBAC 模型、shiro.ini 配置、Hello World 完整案例(登录认证 + 角色/权限校验)。
目录
上篇:懒加载问题终结
1.1 noSession 问题
问题现象
// 访问 /employee/page 时出现:
// org.hibernate.LazyInitializationException:
// failed to lazily initialize a collection of role:
// cn.itsource.domain.Employee.department,
// could not initialize proxy - no Session
根本原因
时序图:noSession 是如何发生的
┌─ Request ─→ DispatcherServlet ─→ Controller ─→ Service ─→ Repository
│
├─ 开启事务
├─ 查询 Employee(department 是懒加载代理)
├─ 提交事务
└─ ★ Session 关闭!
← 返回 Page<Employee>
Controller ─→ Jackson 序列化 Employee
│
├─ 调用 Employee.getDepartment()
├─ 触发懒加载 → 需要访问数据库
└─ ★ Session 已关闭 → noSession 异常!
核心矛盾:JPA 的 Session 在 Service 层事务提交后关闭,但 JSON 序列化发生在上层的 Controller/View 层。两者的生命周期不匹配。
解决方案:OpenEntityManagerInViewFilter(OEIVF)
原理:将 JPA EntityManager 的生命周期从"事务结束"延长到"请求结束"。
<!-- web.xml 配置 -->
<!-- 解决 noSession 问题:延长 Session 到请求结束 -->
<filter>
<filter-name>openEntityManagerInViewFilter</filter-name>
<filter-class>
org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter
</filter-class>
</filter>
<filter-mapping>
<filter-name>openEntityManagerInViewFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
配置后时序:
┌─ Request ─→ OEIVF(开启 EM)─→ Controller ─→ Service ─→ Repository
│
├─ 开启事务
├─ 查询(department 代理)
├─ 提交事务
└─ Session 仍保持打开(OEIVF 持有)
← 返回 Page<Employee>
Controller ─→ Jackson 序列化
├─ getDepartment() → 触发懒加载
├─ Session 可用 → 正常加载 Department
└─ 序列化完成 → 返回 JSON
OEIVF → 关闭 EntityManager
└─ Response ─→ 客户端
注意事项
✅ 优点:
- 配置简单,一行 Filter 即可
- 对现有代码零侵入
- 视图层可以自由访问懒加载属性
⚠️ 缺点:
- Session 保持时间长(整个请求周期)
- 长事务 → 数据库连接占用量高
- 不适合高并发场景
🎯 适用场景:
- 企业管理系统(并发量低)
- 数据量中等(万级)
- 查询关联复杂
❌ 不适用场景:
- 高并发 API 服务
- 微服务网关
- 大数据量批处理
1.2 noSerializer 问题
问题现象
// 即使解决了 noSession,还可能出现:
// com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
// No serializer found for class
// org.hibernate.proxy.pojo.javassist.JavassistLazyInitializer
// and no properties discovered to create BeanSerializer
根本原因
Hibernate 为懒加载的 Department 创建了代理对象(CGLIB/Javassist)
Employee 对象的内存结构:
┌─────────────────────────────────┐
│ Employee │
│ - id: 1 │
│ - username: "张三" │
│ - department: │
│ ┌─────────────────────┐ │
│ │ Department Proxy │ │ ← Hibernate 代理对象
│ │ - id: 1 │ │
│ │ - name: "技术部" │ │
│ │ - handler │ │ ← 额外字段(Jackson 不认识!)
│ │ - hibernate... │ │ ← 额外字段(Jackson 不认识!)
│ │ - fieldHandler │ │ ← 额外字段(Jackson 不认识!)
│ └─────────────────────┘ │
└─────────────────────────────────┘
Jackson 默认配置下,遇到"额外字段"会报错!
解决方案一:@JsonIgnoreProperties 注解
// ===== 在每个关联字段上加注解 =====
// 缺点:每个关联对象都要加,非常繁琐
@Entity
@Table(name = "employee")
public class Employee extends BaseDomain {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id")
@JsonIgnoreProperties(value = {
"hibernateLazyInitializer",
"handler",
"fieldHandler"
})
private Department department;
// getter/setter...
}
解决方案二:CustomMapper 全局配置(推荐)
// ===== CustomMapper.java =====
package cn.itsource.common;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
/**
* 自定义 Jackson ObjectMapper
*
* 解决 Hibernate 懒加载代理对象的 JSON 序列化问题
* 全局生效,无需在每个实体类上加注解
*/
public class CustomMapper extends ObjectMapper {
public CustomMapper() {
// 1. 忽略 null 字段(不输出值为 null 的字段)
this.setSerializationInclusion(JsonInclude.Include.NON_NULL);
// 2. 允许空 Bean 序列化(解决 Hibernate 代理对象问题)
// ★ 这是最关键的配置!
// Hibernate 的懒加载代理对象可能在序列化时被认为是"空 Bean"
// 默认配置会抛出 InvalidDefinitionException
// 设为 false 后,空 Bean 被序列化为 {}
this.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
// 3. (可选)设置日期格式
// this.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
// 4. (可选)设置时区
// this.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
}
}
Spring MVC 注册 CustomMapper
<!-- applicationContext-mvc.xml -->
<!-- 配置自定义消息转换器,全局使用 CustomMapper -->
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.json.
MappingJackson2HttpMessageConverter">
<!-- 支持的内容类型 -->
<property name="supportedMediaTypes">
<list>
<value>application/json;charset=UTF-8</value>
<value>application/x-www-form-urlencoded;charset=UTF-8</value>
<!-- ★ 添加 text/html!否则 EasyUI 的 form submit 会报错 -->
<value>text/html;charset=UTF-8</value>
</list>
</property>
<!-- 使用自定义 ObjectMapper -->
<property name="objectMapper">
<bean class="cn.itsource.common.CustomMapper"/>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
注意
supportedMediaTypes中的text/html:EasyUI 的 form submit 期望返回text/html类型,虽然实际返回的是 JSON 字符串,但浏览器端的默认 Accept Header 会要求text/html。如果不加这个配置,浏览器会报下载文件或解析失败的错误。
1.3 n-to-n 关联数据丢失问题
问题场景
修改部门时出现的问题:
1. Employee 中 department 是懒加载代理对象
2. 前端传参只传了 department.id(没有其他字段)
3. Service 中 save(employee) 时
4. Hibernate 的持久化上下文认为:
- 当前 employee 关联了一个"不完整的" department
- 执行 UPDATE 时,可能级联更新 department 表
- department 的其他字段被覆盖为 null
5. → 数据丢失!
根本原因
持久化对象的 ID 不能被修改的!
当 employee.department 是一个代理对象时:
- 这个代理对象是 JPA 管理的
- 如果 save 时,代理对象的状态被 merge
- 但前端传回来的 department 只包含 id 字段
- 其他字段(name/phone/location 等)全部为 null
- JPA 执行 UPDATE 时,这些字段就被更新为 null
解决方案
// ===== 在 Controller 中,保存前清除无效的关联对象 =====
@RequestMapping("/save")
@ResponseBody
public JsonResult save(Employee employee) {
try {
// ★ 关键处理:清除无效的关联对象
if (employee.getDepartment() != null
&& employee.getDepartment().getId() == null) {
// 前端传了 department.id=""(空字符串)
// Spring MVC 自动创建了 Department 对象但 id=null
// 需要把这种无效关联清除,否则 JPA 会试图持久化空对象
employee.setDepartment(null);
}
// 也可以额外处理:只设置 ID 的关联对象
// 从数据库重新查询完整的 Department 对象
if (employee.getDepartment() != null
&& employee.getDepartment().getId() != null) {
Department dept = departmentRepository
.findById(employee.getDepartment().getId())
.orElse(null);
employee.setDepartment(dept);
}
employeeService.save(employee);
return new JsonResult();
} catch (Exception e) {
e.printStackTrace();
return new JsonResult(false, "保存失败:" + e.getMessage());
}
}
1.4 企业级最佳实践配置
web.xml 完整配置
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<display-name>云悦智销系统</display-name>
<!-- 1. 编码过滤器(保证中文不乱码)-->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>
org.springframework.web.filter.CharacterEncodingFilter
</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 2. 解决 noSession:OpenEntityManagerInViewFilter ★ -->
<filter>
<filter-name>openEntityManagerInViewFilter</filter-name>
<filter-class>
org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter
</filter-class>
</filter>
<filter-mapping>
<filter-name>openEntityManagerInViewFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 3. Spring MVC 前端控制器 -->
<servlet>
<servlet-name>springDispatcher</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springDispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
三个问题的解决方案总结
┌──────────────────────────────────────────────────────────────┐
│ 懒加载问题终结方案 │
├──────────────┬──────────────────────┬────────────────────────┤
│ 问题 │ 解决方案 │ 配置位置 │
├──────────────┼──────────────────────┼────────────────────────┤
│ noSession │ OpenEntityManager │ web.xml │
│ │ InViewFilter │ │
├──────────────┼──────────────────────┼────────────────────────┤
│ noSerializer │ CustomMapper │ applicationContext- │
│ │ + FAIL_ON_EMPTY_BEANS │ mvc.xml │
├──────────────┼──────────────────────┼────────────────────────┤
│ n-to-n 数据 │ Controller 中清除 │ EmployeeController │
│ 丢失 │ 无效关联对象 │ .save() │
└──────────────┴──────────────────────┴────────────────────────┘
下篇:Shiro 权限框架入门
2.1 Shiro 是什么
Apache Shiro 是一个 Java 安全框架,提供:
✅ 身份认证(Authentication)—— 登录验证
✅ 授权(Authorization)—— 权限控制
✅ 密码学(Cryptography)—— 加密解密
✅ 会话管理(Session Management)—— 会话管理
特点:
- 轻量级:简单易用,学习成本低
- 粗粒度:适合一般的企业应用权限控制
- 独立:不依赖 Spring 等容器
- 功能全面:一个框架解决所有安全需求
2.2 为什么用 Shiro —— 与 Spring Security 对比
┌──────────────────┬──────────────────────────┬──────────────────────────┐
│ 维度 │ Shiro │ Spring Security │
├──────────────────┼──────────────────────────┼──────────────────────────┤
│ 学习曲线 │ ⭐ 平缓(2-3 天上手) │ ⭐⭐⭐ 陡峭(1-2 周) │
│ 框架依赖 │ 完全不依赖 Spring │ 强依赖 Spring │
│ 配置复杂度 │ 简单(ini/xml/注解) │ 复杂(大量配置类) │
│ 功能粒度 │ 粗粒度(适合 80% 场景) │ 细粒度(精确到方法级) │
│ 会话管理 │ 独立管理 │ 依赖 HttpSession │
│ 密码学 │ 内置支持 │ 需额外集成 │
│ 社区活跃度 │ 活跃(Apache 顶级项目) │ 极活跃(Spring 生态) │
│ 适用场景 │ 企业管理系统 │ 金融/高安全场景 │
└──────────────────┴──────────────────────────┴──────────────────────────┘
🎯 结论:
- 云悦智销这类企业管理系统 → 推荐 Shiro(简单够用)
- 银行/支付系统 → 推荐 Spring Security(精细控制)
2.3 RBAC 权限模型
Shiro 的核心权限设计基于 RBAC(Role-Based Access Control,基于角色的访问控制)。
┌─────────────────────────────────────────────────────┐
│ RBAC 模型 │
│ │
│ ┌───────────┐ │
│ │ 用户 │ │
│ │ (User) │ │
│ └─────┬─────┘ │
│ │ 属于 │
│ ▼ │
│ ┌───────────┐ │
│ │ 角色 │ 多对多 │
│ │ (Role) │────────────────┐ │
│ └─────┬─────┘ │ │
│ │ 拥有 │ │
│ ▼ ▼ │
│ ┌───────────┐ ┌───────────┐ │
│ │ 权限 │ │ 资源 │ │
│ │(Permisson)│ │ (Resource) │ │
│ └───────────┘ └───────────┘ │
│ │
│ 关系:用户 n ── n 角色 ── n ── n 权限 │
│ │
│ 一句话:张三(用户)是管理员(角色) │
│ 可以访问员工管理(权限/资源) │
└─────────────────────────────────────────────────────┘
2.4 Shiro 的四大基石
┌─────────────────────────────────────────────────────────────┐
│ Shiro 的四大功能 │
├─────────────────┬───────────────────────────────────────────┤
│ 功能 │ 说明 │
├─────────────────┼───────────────────────────────────────────┤
│ Authentication │ 身份认证 — 你是谁? │
│(认证) │ login/logout/记住我 │
├─────────────────┼───────────────────────────────────────────┤
│ Authorization │ 授权 — 你能做什么? │
│(授权) │ hasRole/isPermitted/checkPermission │
├─────────────────┼───────────────────────────────────────────┤
│ Cryptography │ 密码学 — 保证数据安全 │
│(加密) │ MD5/SHA/AES/Base64 │
├─────────────────┼───────────────────────────────────────────┤
│ Session │ 会话管理 — 跟踪用户状态 │
│ Management │ 独立于 HttpSession,支持所有客户端 │
│(会话管理) │ 桌面应用/Web应用/移动端 │
└─────────────────┴───────────────────────────────────────────┘
2.5 Shiro 的核心对象(三大组件)
① Subject(主题/当前用户)
// ===== Subject =====
// 表示"当前操作的用户"(可能是人类用户,也可能是第三方服务)
// Shiro 的所有安全操作都通过 Subject 进行
Subject currentUser = SecurityUtils.getSubject();
// 常用方法:
currentUser.isAuthenticated(); // 是否已登录
currentUser.getPrincipal(); // 获取用户身份标识(通常是用户名)
currentUser.login(token); // 登录
currentUser.logout(); // 登出
currentUser.hasRole("admin"); // 是否有某个角色
currentUser.isPermitted("user:save"); // 是否有某个权限
② SecurityManager(安全管理器)
// ===== SecurityManager =====
// Shiro 的心脏!所有安全操作的实际执行者
// Subject 委托 SecurityManager 完成具体的安全检查
// 获取方式:
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
// SecurityManager 内部包含:
// - Authenticator:认证器
// - Authorizer:授权器
// - SessionManager:会话管理器
// - CacheManager:缓存管理器
// - Realm:数据源(最重要的组件!)
③ Realm(数据源/领域)
// ===== Realm =====
// Shiro 和应用程序安全数据之间的"桥梁"
// 相当于 DAO 层:从数据库获取用户/角色/权限数据
// Shiro 内置了多种 Realm:
// - IniRealm → 从 .ini 配置文件读取(开发测试用)
// - JdbcRealm → 从 JDBC 数据源读取
// - PropertiesRealm → 从 .properties 文件读取
// - 自定义 Realm → 继承 AuthorizingRealm(生产使用)
// Realm 的两个核心方法:
// doGetAuthenticationInfo() → 获取认证信息(用户密码等)
// doGetAuthorizationInfo() → 获取授权信息(角色/权限)
2.6 Shiro 架构图
┌──────────────────────────────────────────────────────┐
│ Shiro 架构 │
│ │
│ ┌───────────┐ │
│ │ Subject │ ← 应用程序直接交互的对象 │
│ │(当前用户) │ │
│ └─────┬─────┘ │
│ │ 委托 │
│ ▼ │
│ ┌──────────────────────────────────────────┐ │
│ │ SecurityManager │ │
│ │ (安全管理器) │ │
│ │ │ │
│ │ ┌────────────┐ ┌───────────┐ │ │
│ │ │Authenticator│ │ Authorizer│ │ │
│ │ │ 认证器 │ │ 授权器 │ │ │
│ │ └──────┬─────┘ └─────┬─────┘ │ │
│ │ │ │ │ │
│ │ ┌──────▼──────────────▼─────┐ │ │
│ │ │ Realm │ │ │
│ │ │ (数据源:ini/DB/LDAP) │ │ │
│ │ └───────────────────────────┘ │ │
│ │ │ │
│ │ ┌───────────┐ ┌─────────────┐ │ │
│ │ │ Session │ │ Cache │ │ │
│ │ │ Manager │ │ Manager │ │ │
│ │ └───────────┘ └─────────────┘ │ │
│ └──────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
2.7 认证与授权流程
认证流程(登录)
┌─ Subject.login(token) ──────────────────────────────────────┐
│ │
│ ① 应用程序调用 Subject.login(usernamePasswordToken) │
│ │
│ ② SecurityManager 收到认证请求 │
│ │
│ ③ SecurityManager 委托 Authenticator 执行认证 │
│ │
│ ④ Authenticator 调用 Realm.doGetAuthenticationInfo() │
│ ┌──────────────────────────────────────────────┐ │
│ │ Realm 从数据源获取用户信息: │ │
│ │ - 根据用户名查数据库 │ │
│ │ - 返回 AuthenticationInfo(包含密码/盐等) │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ⑤ Authenticator 比较密码: │
│ - 输入的密码 vs. 数据库中的密码(加密后比较) │
│ - 匹配 → 认证成功 │
│ - 不匹配 → 抛出 AuthenticationException │
│ │
│ ⑥ 认证成功 → 创建认证后的 Subject │
│ │
└─────────────────────────────────────────────────────────────┘
认证异常:
UnknownAccountException → 账户不存在
IncorrectCredentialsException → 密码错误
LockedAccountException → 账户锁定
ExcessiveAttemptsException → 尝试次数过多
授权流程(权限检查)
┌─ Subject.hasRole("admin") / Subject.isPermitted("emp:save") ─┐
│ │
│ ① 应用程序调用 Subject.hasRole("admin") │
│ │
│ ② SecurityManager 收到授权请求 │
│ │
│ ③ SecurityManager 委托 Authorizer 执行授权 │
│ │
│ ④ Authorizer 调用 Realm.doGetAuthorizationInfo() │
│ ┌───────────────────────────────────────────────┐ │
│ │ Realm 从数据源获取角色/权限信息: │ │
│ │ - 根据当前用户查角色和权限 │ │
│ │ - 返回 AuthorizationInfo │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ ⑤ Authorizer 判断用户是否拥有指定角色/权限 │
│ - 有 → 返回 true │
│ - 没有 → 返回 false(或抛出 AuthorizationException) │
│ │
└──────────────────────────────────────────────────────────────┘
2.8 Hello World 实战
2.8.1 创建 Maven 项目
<!-- pom.xml -->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.itsource</groupId>
<artifactId>shiro-hello</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<dependencies>
<!-- ★ Shiro 核心包 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<!-- 日志包(Shiro 依赖 commons-logging)-->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<!-- Junit 测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.9</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
2.8.2 配置 shiro.ini
创建 src/main/resources/shiro.ini:
# ===== shiro.ini =====
#
# Shiro 配置文件
# 包含用户、角色、权限信息
# 开发测试用,正式环境替换为数据库 Realm
# ============================================
# [users] — 用户定义
# 格式:用户名 = 密码, 角色1, 角色2, ...
# ============================================
[users]
root = 123456, admin
guest = guest, it
zhangsan = abc123, hr
lisi = 111111
# root: 用户名 123456: 密码 admin: 角色
# guest: 用户名 guest: 密码 it: 角色
# ============================================
# [roles] — 角色定义
# 格式:角色名 = 权限表达式1, 权限表达式2, ...
# ============================================
[roles]
admin = * # admin 拥有所有权限(* 通配符)
it = employee:* # it 角色拥有员工的所有权限
hr = employee:save # hr 角色只有员工新增权限
# 权限表达式说明:
# * → 所有权限
# employee:* → employee 资源的所有操作(save/update/delete/select)
# employee:save → employee 资源的 save 操作
# employee:save,update → employee 资源的 save 和 update 操作
2.8.3 Hello World 测试类
// ===== HelloShiro.java =====
package cn.itsource.shiro;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.junit.Test;
/**
* Shiro Hello World —— 第一个 Shiro 程序
*
* 功能演示:
* 1. 读取 shiro.ini 配置(IniRealm)
* 2. 登录认证(Authentication)
* 3. 角色检查(Authorization - Role)
* 4. 权限检查(Authorization - Permission)
*/
public class HelloShiro {
@Test
public void testHello() throws Exception {
// ==========================================
// 第一步:获取 SecurityManager
// ==========================================
//
// Factory 模式读取 shiro.ini 文件
// 返回一个 SecurityManager 实例
// shiro.ini 中包含了用户/角色/权限数据
// ini 文件内部自动创建了 IniRealm
//
// ★ 这句话背后做了 3 件事:
// 1. 读取 shiro.ini → 创建 IniRealm
// 2. 解析用户数据 → 存入 IniRealm
// 3. 创建 SecurityManager → 绑定 IniRealm
//
Factory<SecurityManager> factory =
new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
// ==========================================
// 第二步:将 SecurityManager 绑定到当前上下文
// ==========================================
//
// SecurityUtils.setSecurityManager() 将 SecurityManager
// 设为全局可用。之后在任何地方都可以通过
// SecurityUtils.getSubject() 获取当前用户对象
//
SecurityUtils.setSecurityManager(securityManager);
// ==========================================
// 第三步:获取当前用户(Subject)
// ==========================================
//
// Subject = 当前用户
// 未登录时 = 游客(anonymous)
// 登录后 = 认证用户
//
Subject currentUser = SecurityUtils.getSubject();
// 检查当前用户是否已登录
System.out.println("用户是否登录:" + currentUser.isAuthenticated());
// → false(还没有登录)
// ==========================================
// 第四步:登录认证
// ==========================================
//
// 准备用户名密码令牌
// UsernamePasswordToken = 用户输入的用户名和密码
//
UsernamePasswordToken token =
new UsernamePasswordToken("guest", "guest");
try {
// ★ 执行登录
// 这行代码会触发整个认证流程!
currentUser.login(token);
System.out.println("=== 登录成功!===");
System.out.println("用户是否登录:" + currentUser.isAuthenticated());
// → true
System.out.println("用户名:" + currentUser.getPrincipal());
// → guest
} catch (UnknownAccountException e) {
// 用户名不存在
System.out.println("登录失败:用户名不存在!");
} catch (IncorrectCredentialsException e) {
// 密码错误
System.out.println("登录失败:密码错误!");
} catch (LockedAccountException e) {
// 账户锁定
System.out.println("登录失败:账户已锁定!");
} catch (AuthenticationException e) {
// 其他认证异常
System.out.println("登录失败:" + e.getMessage());
}
// ==========================================
// 第五步:授权检查(登录后)
// ==========================================
// ★ 角色检查
// hasRole():返回 boolean(推荐,不会抛异常)
// checkRole():无返回值,不符合时抛异常
System.out.println("\n=== 角色检查 ===");
System.out.println("是否有 admin 角色:" + currentUser.hasRole("admin"));
// → false(guest 用户只有 it 角色)
System.out.println("是否有 it 角色:" + currentUser.hasRole("it"));
// → true
// ★ 权限检查
// isPermitted():返回 boolean
System.out.println("\n=== 权限检查 ===");
System.out.println("是否拥有 employee:* 权限:"
+ currentUser.isPermitted("employee:*"));
// → true(it 角色拥有 employee:* 权限)
System.out.println("是否拥有 employee:save 权限:"
+ currentUser.isPermitted("employee:save"));
// → true(employee:* 包含 employee:save)
System.out.println("是否拥有 department:save 权限:"
+ currentUser.isPermitted("department:save"));
// → false(it 角色只有 employee 的权限)
// ==========================================
// 第六步:登出
// ==========================================
currentUser.logout();
System.out.println("\n=== 已登出 ===");
System.out.println("用户是否登录:" + currentUser.isAuthenticated());
// → false
}
}
2.8.4 测试多种账号
// ===== 不同用户的权限对比测试 =====
@Test
public void testUsers() {
Factory<SecurityManager> factory =
new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
// 测试用户:root(管理员)
testUser("root", "123456");
// 测试用户:guest(IT 部门)
testUser("guest", "guest");
// 测试用户:zhangsan(HR 部门)
testUser("zhangsan", "abc123");
}
private void testUser(String username, String password) {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token =
new UsernamePasswordToken(username, password);
subject.login(token);
System.out.println("\n========== 用户:" + username + " ==========");
System.out.println("角色 admin: " + subject.hasRole("admin"));
System.out.println("角色 it: " + subject.hasRole("it"));
System.out.println("角色 hr: " + subject.hasRole("hr"));
System.out.println("权限 employee:*: "
+ subject.isPermitted("employee:*"));
System.out.println("权限 employee:save: "
+ subject.isPermitted("employee:save"));
System.out.println("权限 employee:delete: "
+ subject.isPermitted("employee:delete"));
System.out.println("权限 department:*: "
+ subject.isPermitted("department:*"));
subject.logout();
}
// ===== 预期输出 =====
// ========== 用户:root ==========
// 角色 admin: true
// 角色 it: false
// 角色 hr: false
// 权限 employee:*: true (admin = *)
// 权限 employee:save: true (admin = *)
// 权限 employee:delete: true
// 权限 department:*: true
//
// ========== 用户:guest ==========
// 角色 admin: false
// 角色 it: true
// 角色 hr: false
// 权限 employee:*: true (it = employee:*)
// 权限 employee:save: true
// 权限 employee:delete: true
// 权限 department:*: false (it 只有 employee 权限)
//
// ========== 用户:zhangsan ==========
// 角色 admin: false
// 角色 it: false
// 角色 hr: true
// 权限 employee:*: false (hr = employee:save)
// 权限 employee:save: true ✓
// 权限 employee:delete: false ✗
// 权限 department:*: false
2.9 Shiro ini 文件配置详解
完整 ini 配置结构
# ===== shiro.ini 完整配置模板 =====
# ============================================
# [main] — 配置 Shiro 组件(Realm、过滤器等)
# 可以自定义 Realm、CacheManager 等
# ============================================
[main]
# 配置自定义 Realm(默认使用 IniRealm)
# myRealm = com.example.MyRealm
# 配置缓存管理器(减少数据库查询)
# cacheManager = org.apache.shiro.cache.ehcache.EhCacheManager
# 配置密码匹配器
# credentialsMatcher = org.apache.shiro.authc.credential.HashedCredentialsMatcher
# credentialsMatcher.hashAlgorithmName = MD5
# credentialsMatcher.hashIterations = 2
# ============================================
# [users] — 用户定义
# 格式:用户名 = 密码 [, 角色1] [, 角色2] ...
# ============================================
[users]
# root 用户,密码 123456,角色 admin
root = 123456, admin
# guest 用户,密码 guest,角色 it
guest = guest, it
# zhangsan 用户,密码 abc123,角色 hr
zhangsan = abc123, hr
# 支持一个用户多个角色(逗号分隔)
# lisi = 111111, admin, hr
# ============================================
# [roles] — 角色定义
# 格式:角色名 = 权限表达式1, 权限表达式2, ...
# ============================================
[roles]
# admin: 所有权限
admin = *
# it: 员工模块的所有操作
it = employee:*
# hr: 只有员工新增权限
hr = employee:save
# 多个权限用逗号分隔
# developer = employee:*, department:select
# ============================================
# [urls] — Web 应用的 URL 过滤器配置(Web 应用专用)
# 格式:URL = 过滤器
# ============================================
# [urls]
# /login = anon # 登录页面匿名可访问
# /logout = logout # 登出
# /static/** = anon # 静态资源匿名可访问
# /** = authc # 其他所有 URL 需要登录
2.10 权限表达式
Shiro 的权限表达式使用冒号分隔的层级结构:
# ===== 权限表达式格式 =====
# 资源:操作:实例
#
# 层级 1:资源(Resource)— 要控制的对象
# 层级 2:操作(Action)— 能做什么
# 层级 3:实例(Instance)— 特定的数据对象(可选)
# ===== 权限表达式示例 =====
employee:* # 员工模块的所有操作
employee:save # 员工新增
employee:update # 员工修改
employee:delete # 员工删除
employee:select # 员工查询
department:* # 部门模块的所有操作
department:save # 部门新增
department:select # 部门查询
# 通配符 * 表示所有
# employee:* 等价于 employee:save,update,delete,select
# 权限比较规则:
# employee:* 包含 employee:save ✓
# employee:* 包含 employee:delete ✓
# employee:* 包含 employee:* ✓
# employee:save 不包含 employee:* ✗
# * 包含 employee:* ✓
# * 包含 department:* ✓
# 角色中的权限定义:
# admin = * → 管理员可以做任何事情
# it = employee:* → IT 部门可以管理员工
# hr = employee:save → HR 只能新增员工
# dev = employee:*,dept:select → 开发者可以管理员工并查看部门
总结
上篇总结:懒加载问题
┌──────────────────┬──────────────────────┬──────────────────────┐
│ 问题 │ 原因 │ 解决方案 │
├──────────────────┼──────────────────────┼──────────────────────┤
│ noSession │ Session 在 Service │ OpenEntityManager │
│ │ 层关闭 │ InViewFilter │
├──────────────────┼──────────────────────┼──────────────────────┤
│ noSerializer │ Hibernate 代理对象 │ CustomMapper │
│ │ Jackson 不认识 │ FAIL_ON_EMPTY_BEANS │
├──────────────────┼──────────────────────┼──────────────────────┤
│ n-to-n 数据丢失 │ 关联对象只传了 ID │ Controller 中 │
│ │ 其他字段为 null │ 处理关联对象逻辑 │
└──────────────────┴──────────────────────┴──────────────────────┘
下篇总结:Shiro 权限框架
Shiro 学习路线:
⚫ 第一步:理解 RBAC(用户 → 角色 → 权限)
⚫ 第二步:熟悉四大基石(认证/授权/加密/会话)
⚫ 第三步:掌握三大组件(Subject/SecurityManager/Realm)
⚫ 第四步:动手 Hello World(IniRealm + 认证 + 授权)
⚫ 第五步:集成到 Spring/Web 项目(下一篇)
核心概念一句话:
Subject 当前用户 → SecurityManager 总调度 → Realm 数据源
Shiro 的核心优势:
✅ 轻量级 — 不依赖 Spring
✅ 简单易用 — 学习曲线平缓
✅ 功能完整 — 一个框架搞定所有安全需求
✅ 适合企业应用 — 80% 的权限场景够用
系列文章进度
云悦智销项目 12 篇系列:
✅ 01 - Spring Data JPA 基础 + 框架搭建
✅ 02 - Base 抽取 + EasyUI 分页
✅ 03 - CRUD 完整实现 + 数据丢失问题
✅ 04 - 懒加载问题终结 + Shiro 权限框架入门(本篇)
🔲 05 - Shiro 集成 Spring 与 Web 应用
🔲 06-12 - 后续功能模块...
一句话总结
懒加载问题,通过 OEIVF + CustomMapper 双管齐下可以彻底解决。从本篇开始,项目正式引入 Shiro 权限框架——这是企业级应用的标配功能。理解了 RBAC 模型、三大核心组件、认证/授权流程,后续的集成开发就水到渠成了。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)