Apache Shiro 构建并运行示例应用指南

Apache Shiro 2.2.1 构建并运行示例应用指南:从零创建 Maven 工程,给出完整 pom.xml、shiro.ini 与三个示例源码,覆盖登录成功与会话读写、密码错误与账号不存在两类失败场景、角色与权限校验(hasRole、isPermitted、checkPermission),附编译运行命令、真实回显与逐行结果解读,全程实测。

Apache ShiroApache Shiro2026/10/12

Apache Shiro 构建并运行示例应用指南

目标:从零构建并运行一组基于 Apache Shiro 2.2.1 的最小认证授权示例(共 3 个:登录成功与会话读写、登录失败异常处理、角色与权限校验)。给出完整 pom.xml、shiro.ini、示例 Java 源码、编译与运行命令及真实回显。所有命令与输出均在 Ubuntu 22.04.5 + OpenJDK 17 环境实测通过。

一、环境信息

  • 操作系统:Ubuntu 22.04.5 LTS
  • Java:OpenJDK 17.0.20.1
  • 构建工具:Apache Maven 3.6.3
  • Shiro 版本:2.2.1(依赖 shiro-core,传递引入 lang/cache/crypto/config/event 等模块)
  • 日志实现:slf4j-simple 2.0.16 + jcl-over-slf4j 2.0.16
  • 示例工程目录:/root/shiro-demo/
  • 演示账号:root、admin、alice(密码统一 q1w2e3r4)

二、前置条件

  • 已安装 JDK 17 与 Maven(安装与环境变量验证见《Apache Shiro 安装指南》):

    java -version
    mvn -version
    
  • 服务器可访问 Maven Central(首次构建需下载依赖)。

  • 示例进程堆内存建议限制在 256–512MB(见「九、注意事项」)。

三、示例工程结构

标准 Maven 布局,一个 pom、两个 ini 配置与 5 个 Java 类:

./pom.xml
./src/main/java/demo/AuthzDemo.java
./src/main/java/demo/ConfigDemo.java
./src/main/java/demo/ConfigDemo2.java
./src/main/java/demo/LoginDemo.java
./src/main/java/demo/LoginFailureDemo.java
./src/main/resources/shiro.ini
./src/main/resources/shiro-tuned.ini

(以上为 find . -type f | sort 实测回显。)

文件作用
LoginDemo.java示例 1:root 登录成功 + Session 读写(本文第六章)
LoginFailureDemo.java示例 2:密码错误/账号不存在/登录成功对照(本文第七章)
AuthzDemo.java示例 3:hasRole / isPermitted / checkPermission 校验(本文第八章)
ConfigDemo.java、ConfigDemo2.java、shiro-tuned.ini默认组件打印与 [main] 覆盖验证,详见《Apache Shiro 配置参数详解》
shiro.ini账号与角色数据(本文第五章)

创建工程骨架:

mkdir -p /root/shiro-demo/src/main/java/demo /root/shiro-demo/src/main/resources
cd /root/shiro-demo

四、完整 pom.xml

pom 引入 shiro-core 2.2.1,排除其传递的 log4j2 绑定,改用 slf4j-simple,并桥接 jcl-over-slf4j:

<?xml version="1.0" encoding="UTF-8"?>
<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>demo</groupId>
    <artifactId>shiro-demo</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>2.2.1</version>
            <exclusions>
                <!-- shiro-core 2.x 依赖树中的 log4j2 绑定,改用轻量 slf4j-simple 输出日志 -->
                <exclusion>
                    <groupId>org.apache.logging.log4j</groupId>
                    <artifactId>log4j-slf4j2-impl</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>2.0.16</version>
        </dependency>
        <!-- shiro-config-ogdl 依赖 commons-beanutils,1.11.0 不再传递 commons-logging,
             桥接 jcl-over-slf4j 补齐 org.apache.commons.logging.LogFactory -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>2.0.16</version>
        </dependency>
    </dependencies>
</project>

⚠️ 注意:jcl-over-slf4j 不可省略。shiro-core 2.2.1 依赖树中的 commons-beanutils 1.11.0 不再传递 commons-logging,缺失时运行直接报 NoClassDefFoundError: org/apache/commons/logging/LogFactory(排障过程见《Apache Shiro 安装指南》常见问题)。

五、shiro.ini 账号与角色

创建 /root/shiro-demo/src/main/resources/shiro.ini:

[main]
# IniSecurityManagerFactory 解析本文件时,依据 [users]/[roles] 隐式构建 IniRealm 并挂到 SecurityManager

[users]
root = q1w2e3r4, admin
admin = q1w2e3r4, user, deployer
alice = q1w2e3r4, user

[roles]
admin = *
user = app:read, app:write
deployer = app:deploy
  • [users] 段:每行「账号 = 密码, 角色1, 角色2」。root 挂 admin 角色;admin 挂 user 与 deployer;alice 仅 user。
  • [roles] 段:每行「角色 = 权限列表」。admin 用通配符 * 表示全部权限;user 仅 app:read 与 app:write;deployer 仅 app:deploy。

六、示例 1:登录成功与 Session(LoginDemo)

6.1 示例源码

创建 /root/shiro-demo/src/main/java/demo/LoginDemo.java:

package demo;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.ini.IniSecurityManagerFactory;
import org.apache.shiro.lang.util.Factory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 示例 1:加载 shiro.ini,root 账号登录成功,并演示 Session 基本读写。
 */
public class LoginDemo {

    private static final Logger log = LoggerFactory.getLogger(LoginDemo.class);

    public static void main(String[] args) {
        // 1. 从 classpath:shiro.ini 构建 SecurityManager
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        SecurityManager securityManager = factory.getInstance();
        SecurityUtils.setSecurityManager(securityManager);

        // 2. 获取当前 Subject 并登录
        Subject currentUser = SecurityUtils.getSubject();
        log.info("before login, isAuthenticated = {}", currentUser.isAuthenticated());

        UsernamePasswordToken token = new UsernamePasswordToken("root", "q1w2e3r4");
        currentUser.login(token);

        log.info("after login, isAuthenticated = {}", currentUser.isAuthenticated());
        log.info("principal = {}", currentUser.getPrincipal());
        log.info("hasRole(admin) = {}", currentUser.hasRole("admin"));

        // 3. Session 读写
        Session session = currentUser.getSession();
        session.setAttribute("loginUser", currentUser.getPrincipal());
        log.info("session attribute loginUser = {}", session.getAttribute("loginUser"));
        log.info("sessionId = {}, host = {}", session.getId(), session.getHost());

        // 4. 退出登录
        currentUser.logout();
        log.info("after logout, isAuthenticated = {}", currentUser.isAuthenticated());
    }
}

6.2 编译与运行

cd /root/shiro-demo
mvn compile
mvn exec:java -Dexec.mainClass=demo.LoginDemo

编译实测回显(末段):

[INFO] Compiling 5 source files to /root/shiro-demo/target/classes
[WARNING] /root/shiro-demo/src/main/java/demo/LoginFailureDemo.java: Some input files use or override a deprecated API.
[WARNING] /root/shiro-demo/src/main/java/demo/LoginFailureDemo.java: Recompile with -Xlint:deprecation for details.
[INFO] BUILD SUCCESS
[INFO] Total time:  1.340 s
[INFO] Finished at: 2026-10-12T12:11:48Z

⚠️ 注意:deprecated 警告来自 IniSecurityManagerFactory(2.x 标记过时),不影响运行。

运行实测回显(应用段与构建结果):

[demo.LoginDemo.main()] INFO demo.LoginDemo - before login, isAuthenticated = false
[demo.LoginDemo.main()] INFO org.apache.shiro.session.mgt.AbstractValidatingSessionManager - Enabling session validation scheduler...
[demo.LoginDemo.main()] INFO demo.LoginDemo - after login, isAuthenticated = true
[demo.LoginDemo.main()] INFO demo.LoginDemo - principal = root
[demo.LoginDemo.main()] INFO demo.LoginDemo - hasRole(admin) = true
[demo.LoginDemo.main()] INFO demo.LoginDemo - session attribute loginUser = root
[demo.LoginDemo.main()] INFO demo.LoginDemo - sessionId = 901cd5c2-2d05-48f2-904d-21e0dcce612e, host = null
[demo.LoginDemo.main()] INFO demo.LoginDemo - after logout, isAuthenticated = false
[WARNING] thread Thread[SessionValidationThread-1,5,demo.LoginDemo] was interrupted but is still alive after waiting at least 15000msecs
[WARNING] thread Thread[SessionValidationThread-1,5,demo.LoginDemo] will linger despite being asked to die via interruption
[WARNING] NOTE: 1 thread(s) did not finish despite being asked to via interruption. This is not a problem with exec:java, it is a problem with the running code. Although not serious, it should be remedied.
[INFO] BUILD SUCCESS
[INFO] Total time:  15.639 s

6.3 结果解读

  • before login, isAuthenticated = false → after login, isAuthenticated = true:UsernamePasswordToken 携带 root/q1w2e3r4,IniRealm 依据 shiro.ini [users] 段完成认证。
  • principal = root:登录主体即账号名。
  • hasRole(admin) = true:root 在 [users] 段被授予 admin 角色。
  • session attribute loginUser = root:Session 属性写入后可读回;sessionId 为随机 UUID,每次运行不同。
  • after logout, isAuthenticated = false:logout() 清理会话与身份。
  • 末尾 3 行 WARNING:exec:java 方式下 logout() 后会话校验线程(SessionValidationThread-1)未即时退出的残留提示,不影响功能;用 java -cp 直接运行无此现象。

七、示例 2:登录失败场景(LoginFailureDemo)

7.1 示例源码

创建 /root/shiro-demo/src/main/java/demo/LoginFailureDemo.java:

package demo;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.ini.IniSecurityManagerFactory;
import org.apache.shiro.lang.util.Factory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;

/**
 * 示例 2:三种登录场景 —— 密码错误、账号不存在、凭据正确(对照)。
 */
public class LoginFailureDemo {

    public static void main(String[] args) {
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        SecurityUtils.setSecurityManager(factory.getInstance());

        attempt("root", "wrong-password");   // 用户存在,密码错误
        attempt("nobody", "q1w2e3r4");       // 用户不存在
        attempt("alice", "q1w2e3r4");        // 凭据正确(对照组)
    }

    private static void attempt(String username, String password) {
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(new UsernamePasswordToken(username, password));
            System.out.println("LOGIN OK: " + username + ", isAuthenticated = " + subject.isAuthenticated());
            subject.logout();
        } catch (UnknownAccountException e) {
            System.out.println("FAILURE: " + username + " -> UnknownAccountException: " + e.getMessage());
        } catch (IncorrectCredentialsException e) {
            System.out.println("FAILURE: " + username + " -> IncorrectCredentialsException: " + e.getMessage());
        } catch (AuthenticationException e) {
            System.out.println("FAILURE: " + username + " -> " + e.getClass().getName() + ": " + e.getMessage());
        }
    }
}

7.2 运行与回显

方式一(exec:java):

cd /root/shiro-demo && mvn exec:java -Dexec.mainClass=demo.LoginFailureDemo

实测回显(应用段):

FAILURE: root -> IncorrectCredentialsException: Submitted credentials for token [org.apache.shiro.authc.UsernamePasswordToken - root, rememberMe=false] did not match the expected credentials.
FAILURE: nobody -> UnknownAccountException: Realm [org.apache.shiro.realm.text.IniRealm@19091ea8] was unable to find account data for the submitted AuthenticationToken [org.apache.shiro.authc.UsernamePasswordToken - nobody, rememberMe=false].
[demo.LoginFailureDemo.main()] INFO org.apache.shiro.session.mgt.AbstractValidatingSessionManager - Enabling session validation scheduler...
LOGIN OK: alice, isAuthenticated = true

方式二(java -cp 直接运行,先由 Maven 生成依赖 classpath 清单):

cd /root/shiro-demo
mvn -q dependency:build-classpath -Dmdep.outputFile=/tmp/cp.txt
java -cp "target/classes:$(cat /tmp/cp.txt)" demo.LoginFailureDemo

实测回显(slf4j-simple 的线程名为 [main]):

FAILURE: root -> IncorrectCredentialsException: Submitted credentials for token [org.apache.shiro.authc.UsernamePasswordToken - root, rememberMe=false] did not match the expected credentials.
FAILURE: nobody -> UnknownAccountException: Realm [org.apache.shiro.realm.text.IniRealm@7f9a81e8] was unable to find account data for the submitted AuthenticationToken [org.apache.shiro.authc.UsernamePasswordToken - nobody, rememberMe=false].
[main] INFO org.apache.shiro.session.mgt.AbstractValidatingSessionManager - Enabling session validation scheduler...
LOGIN OK: alice, isAuthenticated = true

💡 说明:IniRealm@ 后的 hashcode 每次运行随机变化,属正常现象。

7.3 结果解读

  • root + wrong-password:账号存在但密码不匹配 → 抛 IncorrectCredentialsException,异常消息指出提交凭据与期望凭据不一致。
  • nobody + q1w2e3r4:shiro.ini [users] 段无此账号 → 抛 UnknownAccountException,异常消息指出 IniRealm 无法找到账号数据。
  • alice + q1w2e3r4:凭据正确(对照组)→ LOGIN OK: alice, isAuthenticated = true。
  • 三类场景都在 AuthenticationException 体系之内,示例按具体子类分别捕获后打印,程序不中断。

八、示例 3:角色与权限校验(AuthzDemo)

8.1 示例源码

创建 /root/shiro-demo/src/main/java/demo/AuthzDemo.java:

package demo;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.ini.IniSecurityManagerFactory;
import org.apache.shiro.lang.util.Factory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;

/**
 * 示例 3:hasRole / isPermitted / checkPermission 授权校验,alice 与 root 两个账号对照。
 */
public class AuthzDemo {

    public static void main(String[] args) {
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        SecurityUtils.setSecurityManager(factory.getInstance());

        check("alice");   // 角色:user
        check("root");    // 角色:admin(通配符授权)
    }

    private static void check(String username) {
        Subject subject = SecurityUtils.getSubject();
        subject.login(new UsernamePasswordToken(username, "q1w2e3r4"));

        System.out.println("=== " + username + " ===");
        System.out.println("hasRole(user) = " + subject.hasRole("user")
                + ", hasRole(deployer) = " + subject.hasRole("deployer")
                + ", hasRole(admin) = " + subject.hasRole("admin"));
        System.out.println("isPermitted(app:read) = " + subject.isPermitted("app:read"));
        System.out.println("isPermitted(app:write) = " + subject.isPermitted("app:write"));
        System.out.println("isPermitted(app:deploy) = " + subject.isPermitted("app:deploy"));
        System.out.println("isPermitted(app:delete) = " + subject.isPermitted("app:delete"));
        System.out.println("isPermittedAll(app:read, app:write) = " + subject.isPermittedAll("app:read", "app:write"));

        // checkPermission 校验失败时抛 UnauthorizedException
        try {
            subject.checkPermission("app:delete");
        } catch (UnauthorizedException e) {
            System.out.println("checkPermission(app:delete) -> UnauthorizedException: " + e.getMessage());
        }
        subject.logout();
    }
}

8.2 运行与回显

cd /root/shiro-demo && mvn exec:java -Dexec.mainClass=demo.AuthzDemo

实测回显(应用段与构建结果):

[demo.AuthzDemo.main()] INFO org.apache.shiro.session.mgt.AbstractValidatingSessionManager - Enabling session validation scheduler...
=== alice ===
hasRole(user) = true, hasRole(deployer) = false, hasRole(admin) = false
isPermitted(app:read) = true
isPermitted(app:write) = true
isPermitted(app:deploy) = false
isPermitted(app:delete) = false
isPermittedAll(app:read, app:write) = true
checkPermission(app:delete) -> UnauthorizedException: Subject does not have permission [app:delete]
=== root ===
hasRole(user) = false, hasRole(deployer) = false, hasRole(admin) = true
isPermitted(app:read) = true
isPermitted(app:write) = true
isPermitted(app:deploy) = true
isPermitted(app:delete) = true
isPermittedAll(app:read, app:write) = true
[INFO] BUILD SUCCESS
[INFO] Total time:  16.022 s

8.3 结果解读

  • alice(user 角色):具备 app:read、app:write;不具备 app:deploy、app:delete;isPermittedAll 对两项同时判定为 true;checkPermission(app:delete) 校验失败抛 UnauthorizedException(消息为 Subject does not have permission [app:delete])。
  • root(admin 角色):[roles] 段中 admin = *(通配符),因此四个权限点全部为 true;hasRole 仅 admin 为 true。
  • hasRole 与 isPermitted 的区别:hasRole 判定角色名,isPermitted 判定权限串(支持 资源:操作 分层写法);check 系列方法(checkRole/checkPermission)在无权限时抛异常,is/has 系列返回布尔值。

九、注意事项

⚠️ 注意:

  1. 三个示例共用一个 SecurityManager 时,每个场景应使用独立 Subject 并在结束后 logout(),避免身份状态串扰(示例均按此约定编写)。

  2. 示例进程堆内存建议限制在 256–512MB,通过 MAVEN_OPTS 注入后运行:

    export MAVEN_OPTS="-Xms256m -Xmx512m"
    mvn exec:java -Dexec.mainClass=demo.LoginDemo
    

    运行期间另开终端用 jps -v 验证 Maven 进程已带堆参数(实测回显):

    7021 Launcher -Xms256m -Xmx512m -Dclassworlds.conf=/usr/share/maven/bin/m2.conf -Dmaven.home=/usr/share/maven -Dlibrary.jansi.path=/usr/share/maven/lib/jansi-native -Dmaven.multiModuleProjectDirectory=/root/shiro-demo
    7076 Jps -Dapplication.home=/usr/lib/jvm/java-17-openjdk-amd64 -Xms8m -Djdk.module.main=jdk.jcmd
    
  3. sessionId、IniRealm@hashcode 等随机值每次运行不同,比对回显时忽略即可。

  4. shiro.ini 中的账号与口令 q1w2e3r4 仅为演示凭据,生产环境必须更换并使用加密存储。

十、常见问题

Q1:运行报 NoClassDefFoundError: org/apache/commons/logging/LogFactory?

pom 缺少 jcl-over-slf4j。按第四章补齐依赖后重新 mvn compile,完整排障过程见《Apache Shiro 安装指南》常见问题。

Q2:mvn exec:java 结束后出现 SessionValidationThread 残留 WARNING?

exec:java 方式下 logout() 后会话校验线程不会立即退出导致,程序功能正常;改用 java -cp 直接运行无此现象(见 7.2 方式二)。

Q3:alice 为什么 isPermitted(app:deploy) = false?

shiro.ini [roles] 段中 user 角色仅定义了 app:read, app:write。给 alice 增加 deployer 角色或给 user 角色追加 app:deploy 权限后,重新运行即可变为 true。

Q4:checkPermission 与 isPermitted 该用哪个?

业务上「只判断有无权限再分支」用 isPermitted(返回布尔);「必须具备否则拒绝执行」用 checkPermission(失败抛 UnauthorizedException,语义更强,便于统一异常处理)。

十一、参考链接

  • Apache Shiro 官网:https://shiro.apache.org/
  • 10 分钟教程:https://shiro.apache.org/10-minute-tutorial.html
  • Java 认证指南:https://shiro.apache.org/java-authentication-guide.html
  • Java 授权指南:https://shiro.apache.org/java-authorization-guide.html
  • Realm 文档:https://shiro.apache.org/realm.html