云悦智销项目实战:懒加载问题终结篇与 Shiro 权限框架入门

本文是云悦智销项目系列第 04 篇,分为两大模块。上篇:彻底终结懒加载的 noSession/noSerializer/n-to-n 三大问题,给出企业级最佳实践。下篇:全面入门 Apache Shiro 轻量级权限框架——四大基石、核心组件、RBAC 模型、shiro.ini 配置、Hello World 完整案例(登录认证 + 角色/权限校验)。


目录

  1. 上篇:懒加载问题终结
  2. 下篇:Shiro 权限框架入门
  3. 总结

上篇:懒加载问题终结

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 模型、三大核心组件、认证/授权流程,后续的集成开发就水到渠成了。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐