如果你对Apache Shiro还不太熟悉,这篇文章将向你展示如何开发一个由Apache Shiro保护的非常简单的应用程序。在此过程中,我们将讨论Shiro的核心概念,以帮助你熟悉Shiro的设计和API。
在阅读该文前,建议先了解Shiro的架构和安全相关术语,以便更好的理解代码。
本文代码参考Apache Shiro源码(版本2.0.0)中的 samples/quickstart/src/main/java/Quickstart.java 文件,可以根据需要下载。
环境要求
- JDK 11+(查看版本:java -version)
- Maven 3.6.3+(查看版本:mvn --version)
实战
项目结构
基础配置
pom文件
pom.xml
<?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 https://maven.apache.org/maven-v4_0_0.xsd">
<groupId>org.apache.shiro.samples</groupId>
<version>4.0.0</version>
<modelVersion>4.0.0</modelVersion>
<artifactId>quickstart</artifactId>
<name>Apache Shiro :: Samples :: Quick Start</name>
<packaging>jar</packaging>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>1.11</source>
<target>1.11</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>2.0.0</version>
</dependency>
<!-- configure logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>2.0.12</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>2.22.1</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.22.1</version>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
日志配置
log4j2.xml
<Configuration name="ConfigTest" status="ERROR" monitorInterval="5">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Logger name="org.springframework" level="warn" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
<Logger name="org.apache" level="warn" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
<Logger name="net.sf.ehcache" level="warn" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
<Logger name="org.apache.shiro.util.ThreadContext" level="warn" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
使用shiro
在应用程序中使用Shiro,首先要理解的是,Shiro中几乎所有的内容都与核心组件SecurityManager
相关。需要注意,它与java.lang.SecurityManager
不是同一个东西。
正如我们在架构中所说,Shiro的SecurityManager
是应用程序Shiro环境的核心,并且每个应用程序必须要有一个SecurityManager
。因此,我们首先要做的就是设置SecurityManager
实例。
初始化SecurityManager
尽管我们可以直接在Java源代码中实例化SecurityManager
类,但Shiro中SecurityManager
的实现包含很多配置选项和内部组件,这使得这样做变得繁琐。而使用基于文本的格式来配置SecurityManager
会更加容易,也更加灵活。
为此,Shiro通过基于文本的INI
配置文件提供了一种默认的“通用解决方案”。如今,人们已经厌倦了使用庞大的XML文件,而INI
格式易读、易用,并且几乎不需要依赖。
由于Shiro中SecurityManager
的实现和所有支持组件都是与 JavaBeans 兼容的,因此Shiro几乎可以使用任何配置文件格式进行配置,如XML
、YAML
、JSON
、Groovy Builder
标记语言等。INI
只是 Shiro 的“通用解决方案”格式,任何环境下,其他选项均不可用的情况下,可使用该格式进行配置。
在本项目中,我们使用shiro.ini
文件来对SecurityManager
进行配置。
shiro.ini
# -----------------------------------------------------------------------------
# 用户及其分配的角色
#
# 每行都符合 org.apache.shiro.realm.text.TextConfigurationRealm#setUserDefinitions 的 JavaDoc 中定义的格式。
#
# username = password, role1, role2, ..., roleN
# -----------------------------------------------------------------------------
[users]
# 用户 = 'root' 密码 = 'secret' 角色 = {'admin'}
root = secret, admin
# 用户 = 'guest' 密码 = 'guest' 角色 = {'guest'}
guest = guest, guest
# 用户 = 'presidentskroob' 密码 = '12345' 角色 = {'president'}
presidentskroob = 12345, president
# 用户 = 'darkhelmet' 密码 = 'ludicrousspeed' 角色 = {'darklord', 'schwartz'}
darkhelmet = ludicrousspeed, darklord, schwartz
# 用户 = 'lonestarr' 密码 = 'vespa' 角色 = {'goodguy', 'schwartz'}
lonestarr = vespa, goodguy, schwartz
# -----------------------------------------------------------------------------
# 角色及其分配的权限
#
# 每行都符合 org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions 的 JavaDoc 中定义的格式。
#
# roleName = perm1, perm2, ..., permN
# -----------------------------------------------------------------------------
[roles]
# 角色 'admin' 拥有所有权限,用通配符 '*' 表示
admin = *
# 角色 'schwartz' 拥有名称前缀为 'lightsaber: '的所有权限
schwartz = lightsaber:*
# “goodguy”角色可以“drive”(操作)车牌为“eagle5”(实例特定id)的winnebago(类型)
goodguy = winnebago:drive:eagle5
编写安全代码
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.env.BasicIniEnvironment;
import org.apache.shiro.ini.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.lang.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Quickstart {
private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);
public static void main(String[] args) {
/**
* 1、实例化SecurityManager
* classpath: 资源指示符的前缀,它告诉Shiro从哪里加载ini文件(也支持其他前缀,如‘url:’和 ‘file:’)。
* */
SecurityManager securityManager = new BasicIniEnvironment("classpath:shiro.ini").getSecurityManager();
/**
* 2、全局设置SecurityManager(非必须)
* 这个简单的快速入门示例,将SecurityManager作为JVM单例进行访问。
* 大多数应用程序不会这样做,而是依赖于它们的容器配置或web.xml(Web应用程序)。
* */
SecurityUtils.setSecurityManager(securityManager);
/**
* 3、获取当前用户
* 在保护我们的应用程序时,可能我们最常问自己的问题是“当前用户是谁?”或“当前用户是否允许执行该操作?”,因此,对我们来说,在应用程序中考虑安全的最自然的方式就是基于当前用户。
* Shiro的API基本上使用其Subject来表示“当前用户”。
* */
Subject currentUser = SecurityUtils.getSubject();
/**
* 4、获取Session
* Session是Shiro特有的实例,它提供了您所熟悉的常规HttpSession的大部分功能,但有一些额外的优点和一个很大的不同:它不需要HTTP环境!
* */
Session session = currentUser.getSession();
session.setAttribute("someKey", "aValue");
String value = (String) session.getAttribute("someKey");
if (value.equals("aValue")) {
log.info("获取到正确的值! [" + value + "]");
}
/**
* 5、登录
* 上面的Subject实例代表了当前用户,但谁是当前用户呢?他们是匿名的,直到他们至少登录一次为止。
* */
if (!currentUser.isAuthenticated()) {
//可以以GUI的方式收集用户身份和凭证,如HTML的用户名/密码表单、X509证书、OpenID等。这里使用用户名/密码的示例,因为这是最常见的方式。
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
//支持‘记住我’
token.setRememberMe(true);
try {
//如果登录尝试失败,你可以捕获各种具体的异常,这些异常会告诉你到底发生了什么,并允许你据此进行处理和反应。
//但是安全最佳实践是向用户提供通用的登录失败消息,因为你不会希望帮助试图入侵你系统的攻击者。
currentUser.login(token);
} catch (UnknownAccountException uae) {
log.info("用户名不存在 " + token.getPrincipal());
} catch (IncorrectCredentialsException ice) {
log.info("用户 " + token.getPrincipal() + " 的密码错误!");
} catch (LockedAccountException lae) {
log.info("用户名 " + token.getPrincipal() + " 被锁定. 请联系你的管理员解锁。");
}
catch (AuthenticationException ae) {
//意外情况? error?
}
}
/**
* 6、登录后,用户角色、权限等操作
* */
log.info("用户 [" + currentUser.getPrincipal() + "] 成功登录.");
if (currentUser.hasRole("schwartz")) {
log.info("你的角色是 Schwartz!");
} else {
log.info("你不是 schwartz 角色.");
}
if (currentUser.isPermitted("lightsaber:wield")) {
log.info("你有 lightsaber:wield 的权限");
} else {
log.info("你没有 lightsaber:wield 的权限");
}
//实例级权限:
if (currentUser.isPermitted("winnebago:drive:eagle5")) {
log.info("你允许‘drive’车牌号为(id)'eagle5'的winnebago。这是钥匙——玩得开心点!");
} else {
log.info("你未被允许驾驶车牌号为(id)'eagle5'的房车!");
}
/**
* 7、退出
* */
currentUser.logout();
System.exit(0);
}
}
输出
20:18:08.105 [main] INFO Quickstart - 获取到正确的值! [aValue]
20:18:08.111 [main] INFO Quickstart - 用户 [lonestarr] 成功登录.
20:18:08.111 [main] INFO Quickstart - 你的角色是 Schwartz!
20:18:08.112 [main] INFO Quickstart - 你有 lightsaber:wield 的权限
20:18:08.112 [main] INFO Quickstart - 你允许‘drive’车牌号为(id)'eagle5'的winnebago。这是钥匙——玩得开心点!
总结
希望这篇文章能帮助你理解如何在基本应用程序中使用Shiro,以及Shiro的主要设计概念,即Subject
和SecurityManager
。