如果你刚刚接触Apache Shiro,这个简短的教程将会给你展示如何快速的建立起一个被Apache Shiro保护的初始应用程序。本文中,我们将一直讨论Shiro的核心要点,以便帮助你尽快熟悉Shiro的设计理念与API。
生成应用
在这个简单的例子中,我们将会创建一个非常简单的命令行应用来运行和快速退出,这样你可以快速了解Shiro的API。
Apache Shiro从设计出来的那天起,就是为了能够支持任何应用程序,从最简单的命令行应用到大型的分布式web应用程序。尽管我们在这里只是创建了一个简单的应用程序,但是也同样适用于任何一个你所知的应用。 |
---|
这个教程最低需要java1.6及以上的环境和Apache Maven依赖管理工具,当然你也可以获取Shiro的jar包来直接在你的应用程序中使用。
同样请确保你使用的Maven版本在2.2.1及以上,你可以在命令提示符下键入命令 **mvn --version
**来查看当前版本。
root:~/shiro-tutorial$ mvn --version
Apache Maven 2.2.1 (r801777; 2009-08-06 12:16:01-0700)
Java version: 1.6.0_24
Java home: /System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home
Default locale: en_US, platform encoding: MacRoman
OS name: "mac os x" version: "10.6.7" arch: "x86_64" Family: "mac"
接下来,我们使用最原始的方式创建一个应用程序(当然读者可以直接使用IDEA来创建),先生成一个目录,比如 shiro-tutorial ,并且在目录下生成一个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 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.apache.shiro.tutorials</groupId>
<artifactId>shiro-tutorial</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>First Apache Shiro Application</name>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>1.6</source>
<target>1.6</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
<!-- This plugin is only to test run our little application. It is not
needed in most Shiro-enabled applications: -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.1</version>
<executions>
<execution>
<goals>
<goal>java</goal>
</goals>
</execution>
</executions>
<configuration>
<classpathScope>test</classpathScope>
<mainClass>Tutorial</mainClass>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.1</version>
</dependency>
<!-- Shiro uses SLF4J for logging. We'll use the 'simple' binding
in this example app. See http://www.slf4j.org for more info. -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.21</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.7.21</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
接下来创建一个有main方法的java类,同样在当前目录下生成子文件目录 src/main/java
和java文件 Tutorial.java
:
src/main/java/Tutorial.java
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.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Tutorial {
private static final transient Logger log = LoggerFactory.getLogger(Tutorial.class);
public static void main(String[] args) {
log.info("My First Apache Shiro Application");
System.exit(0);
}
}
测试启动
在当前目录下(shiro-tutorial)输入一下命令提示符(当然你也可以在idea中直接指向main方法):
mvn compile exec:java
你将会看到应用的启动与结束,命令句界面会展示如下内容:
root:~/projects/shiro-tutorial$ mvn compile exec:java
... a bunch of Maven output ...
1 [Tutorial.main()] INFO Tutorial - My First Apache Shiro Application
root:~/projects/shiro-tutorial\$
添加Shiro
在应用程序中启用Shiro时需要注意的是,Shiro中的几乎所有内容都与 SecurityManager
的中心/核心组件相关 - 它与java.lang.SecurityManager不同。
我们将在下个章节来详细了解Shiro的设计理念,本章节知道 SecurityManager
是应用程序中使用的Shiro环境的核心并且每个应用程序必须存在一个 SecurityManager
就足够了。因此,我们在Tutorial应用程序中必须做的第一件事是设置 SecurityManager
实例。
虽然我们可以直接实例化SecurityManager类,但是Shiro的SecurityManager实现有着太多的配置选项和内部组件,这使得我们如果想要采用硬编码的方式来实现会非常困难, 相反,使用灵活的基于文本的配置方式来配置 SecurityManager
会更容易得多。 人们现在已经厌倦了使用庞大的XML文件,INI易于阅读,易于使用,并且需要很少的依赖性。
所以我们将在这个应用程序中使用INI文件来配置Shiro的 SecurityManager
。首先在 pom.xml
同级下创建一个 src/main/resources
目录 ,在其目录下创建一个shiro.ini文件并输入如下内容:
# =============================================================================
# Tutorial INI configuration
#
# Usernames/passwords are based on the classic Mel Brooks' film "Spaceballs" :)
# =============================================================================
# -----------------------------------------------------------------------------
# 用户名及对应的角色
# username = password, role1, role2, ..., roleN
# -----------------------------------------------------------------------------
[users]
root = secret, admin
guest = guest, guest
presidentskroob = 12345, president
darkhelmet = ludicrousspeed, darklord, schwartz
lonestarr = vespa, goodguy, schwartz
# -----------------------------------------------------------------------------
# 角色名及对应的权限
# roleName = perm1, perm2, ..., permN
# -----------------------------------------------------------------------------
[roles]
admin = *
schwartz = lightsaber:*
goodguy = winnebago:drive:eagle5
以上内容所示,这个配置设置了一组静态的基础用户账号,足以满足我们的第一个应用程序。在后面的章节中,我们会使用更复杂的用户数据源,如关系数据库,LDAP和ActiveDirectory等。
现在我们已经定义了INI文件,接下来我们在应用程序中创建一个 src/main/resources
实例,在main方法中作如下改动:
public static void main(String[] args) {
log.info("My First Apache Shiro Application");
//1.创建一个INI安全管理工厂的实例
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//2.获取SecurityManager 实例
SecurityManager securityManager = factory.getInstance();
//3.在上下文语义环境中添加SecurityManager 实例
SecurityUtils.setSecurityManager(securityManager);
System.exit(0);
}
再次执行 mvn compile exec:java
命令(或执行main方法),能够看到一切执行顺利。
下面具体介绍一下我们的修改:
- 我们使用Shiro的
IniSecurityManagerFactory
来实现获取root跟目录下的shiro.ini文件配置。该实现体现了Shiro对设计模式中工厂模式的支持。classpath:
前缀指明了资源的位置(其他前缀,例如url:
和file:
同样支持) factory.getInstance()
方法是用来解析INI文件并且返回一个对应配置的SecurityManager
实例。- 在这个简单的示例中,我们将
SecurityManager
设置为可通过JVM访问的静态(内存)单例。需要注意的是,虽然再上面这个例子中可以使用,但是如果您在单个JVM中拥有多个使用Shiro的应用程序将无法进行此操作。更复杂的应用程序环境通常会将SecurityManager
放在特定于应用程序的内存中(例如在Web应用程序的ServletContext
或Spring,Guice或JBoss DI容器实例中)。
使用Shiro
现在我们的 SecurityManager
已经准备就绪,我们可以去做真正想做的事 - 执行安全操作。 在保护我们的应用程序时,我们关心的最重要的问题往往是“谁是当前用户?”或“当前用户是否允许执行X”?在我们编写代码或设计用户界面时,通常会问这些问题。应用程序通常是基于用户构建的,因此,我们在应用程序中考虑安全性的最自然方式是基于当前用户。 Shiro的API从根本上代表了“当前用户”的 Subject
概念。
在几乎所有环境中,您都可以通过以下调用获取当前正在执行的用户:
Subject currentUser = SecurityUtils.getSubject();
使用SecurityUtils.getSubject()方法,我们可以获取当前正在执行的 Subject
。 Subject是一个安全术语,大意是指“特定于安全性领域的当前正在执行的用户的视图”。它不被称为“用户”,因为“用户”一词通常与人类相关联。在安全领域,术语 Subject
可以指人类,也可以指第三方流程,cron作业,守护程序帐户或类似内容。它只是意味着“当前与软件交互的东西”。但是,在大多数情况下,您都可以将 Subject
视为Shiro的“用户”概念。 单个应用程序中的 getSubject()
方法调用可能会根据应用程序的位置中的用户数据返回 Subject
,而在服务器环境(例如Web应用程序)中,它会根据与当前线程或传入请求关联的用户数据获取 Subject
。
现在你有了一个 Subject
,你能用它做什么?
如果您希望在当前应用程序的会话期间向用户提供内容,则可以获取其会话:
Session session = currentUser.getSession();
session.setAttribute(“someKey”,“aValue”);
Session
是一个特定于Shiro的实例,它提供了您对常规HttpSession熟悉的大部分内容,但也有一些额外的好处和一个很大的区别:它不需要HTTP环境!
如果在Web应用程序内部署,默认情况下**Session
**将会基于HttpSession生成。但是,在非Web环境中,比如本文的应用程序,Shiro默认会自动使用企业会话管理。这意味着无论部署环境如何,您都可以在应用程序中使用相同的API!这等于打开了一个全新的应用程序世界,因为任何需要会话的应用程序都不需要被强制使用HttpSession或EJB的有状态会话Bean。而且,现在任何客户端都可以共享会话数据。
所以现在你可以获得一个 Subject
和 Session
。所以现在可以真正处理那些需要被处理的事情了吗?比如检查是否允许他们做某些事情,比如检查角色和权限? 很抱歉,我们现在还是只能对已知用户进行检查。上面的我们的 Subject
实例代表当前用户,但谁是当前用户?直到他们至少登录一次我们才能知道。所以,让我们这样做:
if ( !currentUser.isAuthenticated() ) {
//以某种特定的方式收集用户主体和凭据
//比如网页的form表单 username/password, X509 certificate, OpenID 等等.
//我们在这里将使用最通用的方法 username/password.
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
//这就是添加“记住我”功能
token.setRememberMe(true);
currentUser.login(token);
}
大功告成,怎么样,是不是简单的不可思议?
但如果用户登录失败了怎么办?我们需要一个异常处理机制来捕获登录异常:
try {
currentUser.login( token );
//如果没有异常,代表请求正常走完!
} catch ( UnknownAccountException uae ) {
//用户名不存在?
} catch ( IncorrectCredentialsException ice ) {
//密码验证失败,请重试?
} catch ( LockedAccountException lae ) {
//用户状态被锁?
}
... 其他更多的异常 ...
} catch ( AuthenticationException ae ) {
//意外错误?
}
安全性最佳做法是向用户提供通用的登录失败消息(比如用户名或密码失败),因为您不希望错误提示消息会被帮助攻击者侵入您的系统。 |
---|
现在我们拿到了登录成功的当前用户,我们才可以真正去做之前想做的事了。
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.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Tutorial {
private static final transient Logger log = LoggerFactory.getLogger(Tutorial.class);
public static void main(String[] args) {
log.info("My First Apache Shiro Application");
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
// 获取当前用户:
Subject currentUser = SecurityUtils.getSubject();
// 用Session做一些事情(不需要web或EJB容器!!!)
Session session = currentUser.getSession();
session.setAttribute("someKey", "aValue");
String value = (String) session.getAttribute("someKey");
if (value.equals("aValue")) {
log.info("Retrieved the correct value! [" + value + "]");
}
// 让我们登录当前用户,这样我们就可以检查角色和权限:
if (!currentUser.isAuthenticated()) {
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
token.setRememberMe(true);
try {
currentUser.login(token);
} catch (UnknownAccountException uae) {
log.info("There is no user with username of " + token.getPrincipal());
} catch (IncorrectCredentialsException ice) {
log.info("Password for account " + token.getPrincipal() + " was incorrect!");
} catch (LockedAccountException lae) {
log.info("The account for username " + token.getPrincipal() + " is locked. " +
"Please contact your administrator to unlock it.");
}
// ... 捕获其他更多的异常?
catch (AuthenticationException ae) {
//unexpected condition? error?
}
}
//打印用户主体(在这种情况下,用户名):
log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");
//测试角色:
if (currentUser.hasRole("schwartz")) {
log.info("May the Schwartz be with you!");
} else {
log.info("Hello, mere mortal.");
}
//测试类型化的权限
if (currentUser.isPermitted("lightsaber:wield")) {
log.info("You may use a lightsaber ring. Use it wisely.");
} else {
log.info("Sorry, lightsaber rings are for schwartz masters only.");
}
//测试级别化的权限:
if (currentUser.isPermitted("winnebago:drive:eagle5")) {
log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. " +
"Here are the keys - have fun!");
} else {
log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
}
//所有事都已完成,可以退出了!
currentUser.logout();
System.exit(0);
}
}
摘要
希望本教程能够帮助您了解如何在应用程序中设置Shiro以及Shiro的主要设计概念,即 Subject
和 SecurityManager
。 但这是一个相当简单的应用程序。您可能会问自己,“如果我不想使用INI用户帐户而是想连接到更复杂的用户数据源,该怎么办?” 要回答这个问题,需要更深入地了解Shiro的架构和配置机制。下篇文章将介绍Shiro的 Architecture
。