Apache Shiro 教程
第一个Apache Shiro应用
如果你是第一次接触Apache Shiro,这个简短的教程将会通过一个非常简单的项目教你如何使用Apache Shiro,并将会讨论Shiro的核心概念,帮助你熟悉Shiro的设计和API
如果你不想自己编写源文件,可以通过Git获取相关源代码:https://github.com/apache/shiro/tree/master/samples/quickstart
Setup
在这个简单的例子中,将会创建一个非常简单的命令行应用程序,熟悉一下Shiro的API
Apache Shiro从一开始就被设计为支持任何应用程序——从最小的命令行应用程序到集群web应用程序。
本教程是一个简单的应用程序,但是无论应用程序是如何创建的或部署在哪里,使用模式都是相同的。
本教程要求Java的版本不低于1.6 ,使用Maven作为构建工具,但是Maven对于Shiro来说并不是必须的,可以使用Shiro的jar文件,或者Ant,Ivy集成到你的应用程序中
本教程要求Maven版本至少为2.2.1,请在命令行使用mvn --version确保maven的版本
获取maven版本
hazlewood:~/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"
现在,创建一个新目录,这里以Tutorial为例,将下面的pom.xml文件保存到这个新目录中
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>
<!-- 这个插件仅仅用于测试,并不是必须的 -->
<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使用SLF4j来进行日志记录,这里使用slf4j-simple,详情请见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>
现在的目录结构如下所示:
Tutorial 类
本教程将要演示一个简单的命令行程序,所以首先需要创建一个具有main方法的类
在和上述pom.xml文件的同级目录下,创建一个src/main/java子目录,在src/main/java目录中创建一个Tutorial.java文件,该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);
}
}
先别关注上面的import语句,后面介绍他们。现在已经创建了一个典型的命令行程序,它只会输出文本“My First Apache Shiro Application”然后退出
现在的目录结构如下所示:
测试运行
现在,在命令行中把工作目录切换到项目根目录,即pom.xml所在的目录,然后输入如下命令:
mvn compile exec:java
你将会看到你的程序运行并退出,类似下面的结果
lhazlewood:~/projects/shiro-tutorial$ mvn compile exec:java
... 一大堆Maven的输出 ...
1 [Tutorial.main()] INFO Tutorial - My First Apache Shiro Application
lhazlewood:~/projects/shiro-tutorial$
上述程序运行成功,接下来在这个项目中使用Apache Shiro,在代码更改之后,可以继续使用mvn compile exec:java来看更改的结果
启动 Shiro
需要注意的是,Shiro中的一切都和一个核心组件相关,那就是SecurityManager,但这和Java中的java.lang.SecurityManager是不一样的
后面会详细介绍Shiro的架构设计,在每个应用中,至少得存在一个Security Manager的实例,因此,接下来在Toturial项目中设置一个SecurityManager实例
配置介绍
Shiro的SecurityManager相关实现有着足够多的配置选项和内部组件,这使得通过基于文本的配置格式来配置SecurityManager是非常简单的
为此,Shiro通过基于文本的INI配置提供了一个默认的通用解决方案。INI易于阅读和使用,且不需要太多的依赖。通过对象图导航,可以有效地使用INI配置简单的对象图,如SecurityManager
Shiro的SecurityManager实现和所有的Shiro组件都是JavaBean形式的
这允许Shiro可以通过XML,YAML,JSON等等格式进行配置
INI配置是Shiro的通用配置方案,以防止其他选择不可用的时候
shiro.ini
接下来在项目中,通过INI文件来配置Shiro的SecurityManager,首先,在和pom.xml同级目录下创建一个src/main/resources目录,然后在src/main/resources子目录下创建shiro.ini文件,文件内容如下:
src/main/resources/shiro.ini
# =============================================================================
# Tutorial INI configuration
#
# Usernames/passwords are based on the classic Mel Brooks' film "Spaceballs" :)
# =============================================================================
# -----------------------------------------------------------------------------
# Users and their (optional) assigned roles
# username = password, role1, role2, ..., roleN
# -----------------------------------------------------------------------------
[users]
root = secret, admin
guest = guest, guest
presidentskroob = 12345, president
darkhelmet = ludicrousspeed, darklord, schwartz
lonestarr = vespa, goodguy, schwartz
# -----------------------------------------------------------------------------
# Roles with assigned permissions
# roleName = perm1, perm2, ..., permN
# -----------------------------------------------------------------------------
[roles]
admin = *
schwartz = lightsaber:*
goodguy = winnebago:drive:eagle5
如上所示,在INI配置文件中,设置了一个少量的用户集合,之后会介绍更复杂的用户数据源,如关系型数据库,LDAP,等等
通过 ini 配置 SecurityManager
现在将创建一个SecurityManager实例,相关代码如下所示:
public static void main(String[] args) {
log.info("My First Apache Shiro Application");
//1.
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//2.
SecurityManager securityManager = factory.getInstance();
//3.
SecurityUtils.setSecurityManager(securityManager);
System.exit(0);
}
在上面标注的三行代码之后,Shiro就可以在程序中使用了,通过运行mvn compile exec:java,就能看到所有的东西仍然成功运行,由于Shiro默认的日志级别是debug或者更低,因此会看到很多Shiro的日志信息,如果运行没有错误,那就是运行成功了
上面标注的3行代码作用如下:
- 通过我们之前创建的shiro.ini文件,来构造一个IniSecurityManagerFactory实例,classpath:前缀是一个资源标识符,它来告诉Shiro要去哪里寻找ini文件,其他的前缀如url:和file:
- factory.getInstance()方法将会解析ini文件,通过反射的方式,构造并返回一个SecurityManger实例
- 在这个例子中,以静态单例的方式在JVM中设置了一个SecurityManager,但是请注意,如果在单个JVM中有多个启用Shiro的应用程序,那么这就不可取了。在更复杂的应用中,会把SecurityManager放在应用特有的内存中,如Web的ServletContent,Spring容器等等
使用 Shiro
在程序中,最常用的安全相关问题就是“当前用户是谁”和“当前用户是否允许执行某操作”,这和写代码或设计用户接口是一样的:应用是基于用户故事构建的,我们希望对不同的用户展现不同的功能。因此,当前用户是谁,在进行安全保护时是一个核心问题。Shiro中,把当前用户称之为Subject(主体)
在几乎所有的环境中,可以通过下面的代码来获取当前用户:
Subject currentUser = SecurityUtils.getSubject();
通过SecurityUtils.getSubject(),我们可以获取当前执行的主体,主体是一个安全术语,它意味着“当前执行用户的特定于安全的视图”。之所以不把他称之为用户,是因为用户总会和一个人相关,而主体可以是人,可以是第三方进程,可以是一个定时任务等等,主体表示“当前和软件进行交互的东西”。在大多数应用中,可以将主体和用户的概念等同起来
在一个独立的应用程序中,getSubject()调用可能会根据应用程序特定位置的用户数据返回一个Subject,而在一个服务器环境中(例如web应用程序),它会根据与当前线程或传入请求相关的用户数据获取Subject
如果你想要某些东西在用户的会话过程中生效,可以通过如下代码获取会话:
Session session = currentUser.getSession();
session.setAttribute( "someKey", "aValue" );
这个API的方式和HttpSession很像,但是它不需要HTTP的环境。在Web应用程序中,Shiro的Session默认就是HttpSession,但是在非Web环境中,Shiro将会使用企业会话管理。这意味着不管在哪一层,哪个环境中,都可以使用同样的API。任何需要会话的应用程序现在都不需要强制使用HttpSession或EJB Stateful Session Bean,任何客户端技术现在都可以共享会话数据。
目前,用户是匿名的,因为我们还没有进行登录,现在来演示登录
if ( !currentUser.isAuthenticated() ) {
//collect user principals and credentials in a gui specific manner
//such as username/password html form, X509 certificate, OpenID, etc.
//We'll use the username/password example here since it is the most common.
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
//this is all you have to do to support 'remember me' (no config - built in!):
token.setRememberMe(true);
currentUser.login(token);
}
如果登录失败,login操作将会抛出异常,可以捕获这些特定的异常来了解具体发生了什么,来进行处理:
try {
currentUser.login( token );
//if no exception, that's it, we're done!
} catch ( UnknownAccountException uae ) {
//username wasn't in the system, show them an error message?
} catch ( IncorrectCredentialsException ice ) {
//password didn't match, try again?
} catch ( LockedAccountException lae ) {
//account for that username is locked - can't login. Show them a message?
}
... more types exceptions to check if you want ...
} catch ( AuthenticationException ae ) {
//unexpected condition - error?
}
可以检查许多不同类型的异常,或者针对Shiro可能没有考虑到的自定义条件抛出自己的异常。参考Shiro的AuthenticationException
保障安全的一个手段是向用户提供通用的登录失败消息,因为没有人会想帮助试图侵入系统的攻击者
可以通过添加如下代码,获知是谁登录了系统
//print their identifying principal (in this case, a username):
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(); //removes all identifying information and invalidates their session too.
最终的Tutorial类
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);
// get the currently executing user:
Subject currentUser = SecurityUtils.getSubject();
// Do some stuff with a Session (no need for a web or EJB container!!!)
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 + "]");
}
// let's login the current user so we can check against roles and permissions:
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 more exceptions here (maybe custom ones specific to your application?
catch (AuthenticationException ae) {
//unexpected condition? error?
}
}
//say who they are:
//print their identifying principal (in this case, a username):
log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");
//test a role:
if (currentUser.hasRole("schwartz")) {
log.info("May the Schwartz be with you!");
} else {
log.info("Hello, mere mortal.");
}
//test a typed permission (not instance-level)
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.");
}
//a (very powerful) Instance Level permission:
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!");
}
//all done - log out!
currentUser.logout();
System.exit(0);
}
}