Apache Shiro 教程
你的第一个Apache Shiro程序
如果你刚开始接触Apache Shiro,那么这篇教程会向你展示如何搭建一个最基本最简单的Apache Shiro程序,与此同时我们会一边介绍Shiro的核心概念一边帮你熟悉Shiro的设计与API。
如果不想跟着教程编写所有的文件,可以从下面的位置获取一份类似的样例工程以作参考:
- Apache Shiro的SVN地址: https://svn.apache.org/repos/asf/shiro/trunk/samples/quickstart/
- Apache Shiro源码包中的 samples/quickstart 目录下。源码可以从 这里 获取。
搭建
该示例中我们会创建一个简单的命令行程序,运行然后立马退出,这样可以让你体验下Shiro的API。
任何应用 Apache Shiro在设计之初就是为了支持所有的程序——从最小的命令行到最庞大的web集群。即便我们在这里只是创建的一个简单的应用,但Shiro的使用方法却无关乎程序的创建或部署方式。 |
本教程需要用到Java 1.5或是更高的版本。我们将使用 Apache Maven作为构建工具,当然你也可以使用Apache Ant或Ivy。
请确保Maven的版本为2.2.1或更高,在命令行里输入mvn --version即可看到类似如下的版本信息:
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"
然后新建一个文件夹“shiro-tutorial”,将下面的Maven文件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>2.0.2</version> <configuration> <source>1.5</source> <target>1.5</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.1.0</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.6.1</version> <scope>test</scope> </dependency> </dependencies> </project>
Tutorial类
因为要运行一个命令行程序,所以需要创建一个带public static void main(String[] args)方法的类。
在pom.xml所在的目录下创建一个*src/main/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的声明语句,这个之后再谈。现在可以得到一个命令行shell程序,它打印完“My First Apache Shiro Application”接着就退出了。
测试运行
在上述工程所在的根目录命令行提示符中输入以下:
mvn compile exec:java
接着你会看到这个程序运行然后退出并打印类似如下的信息(注意加粗的部分):
lhazlewood:~/projects/shiro-tutorial$ mvn compile exec:java
... a bunch of Maven output ...
1 [Tutorial.main()] INFO Tutorial - My First Apache Shiro Application
lhazlewood:~/projects/shiro-tutorial\$
运行成功——接下来使用Apache Shiro。随着教程的进行,你可以在每次新增某些代码时运行mvn compile exec:java命令来查看变更后的结果。
启用Shiro
在程序中使用Shiro需要知道的第一件事就是,在Shiro中几乎所有的东西都会跟一个叫SecurityManager的核心组件相关联。对于熟悉Java 安全编程的码农们要注意,这个SecurityManager是Shiro中的概念——它跟java.lang.SecurityManager可不是一回事。
虽然之后我们会于架构这一章中再探讨Shiro的设计,但是现在至少应该了解到在使用Shiro的程序中,SecurityManager处于核心的位置,而且一个程序只能有一个 SecurityManager。所以,我们现在需要做的就是创建一个SecurityManager的实例。
配置
虽然可以直接在代码里实例化SecurityManager类,但是Shiro中SecurityManager的所有实现类都有很多的配置选项和内部组件,这么做会很痛苦,所以还是使用更加灵活的文本配置方式简单一些。
Shiro默认使用基于INI文本格式的配置。相对于冗杂的XML文件,INI更便于阅读和使用,依赖更少。稍后你会发现INI还可以用来配置简单的对象图。
配置格式 Shiro中所有的SecurityManager实现和相关支持组件都是兼容JavaBeans规范的。所以Shiro可以通过各种格式进行配置比如XML (Spring, JBoss, Guice等), YAML, JSON, Groovy等等。INI只是Shiro的默认的通用配置方式。 |
shiro.ini
因此这里将使用INI文件来配置SecurityManager。首先创建目录结构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
如上,该配置文件只是创建了一组静态的用户帐号,不过对我们的程序来说也是足够了。在这之后的章节中你还会了解到如何使用像是数据库、LDAP等复杂的用户数据源。
引用配置
既然已经定义好了INI文件,现在可以在Tutorial类中创建SecurityManager的实例了。修改main方法如下:
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默认的日志级别,可能看不到Shiro的输出日志,如果程序运行正常没有错误,那么就是OK的)!
说明一下上面几行代码:
- 这里使用IniSecurityManagerFactory类来提取(解析)位于classpath根路径下的shiro.ini文件,这个类也反应了Shiro对工厂方法模式的支持,classpath:是一个资源指示符,用来告知Shiro载入ini文件的位置(其他前缀如url:和file:也是支持的)。
- factory.getInstance()会解析INI文件并返回一个配置好的SecurityManager实例。
- 在该示例中,我们把SecurityManager设为静态的单例,可以被整个JVM访问到,但是要注意,如果在一个JVM中有多个使用shiro的程序的话这么做是不可以的。简单的情况还好,对于复杂的应用环境应该将SecurityManager放到应用独占的内存中(比如web应用的ServletContext或是 Spring, Guice及JBoss DI的容器实例中)。
使用Shiro
既然SecurityManager已经配置好,现在可以做我们真正关心的事情——执行安全操作。
当保护我们的程序时,最关注的问题可能就是“当前的用户是谁?”或“用户是否允许执行某项操作?”。在设计/编写基于用户故事的代码/界面而你又想拥有基于每个用户展现(或被保护)的功能时这些问题就很常见了。所以很常见的思路就是根据当前的用户来保证应用的安全。而“当前用户”这个概念在Shiro的API中是用Subject来表示的。
在几乎所有环境中,你都可以通过如下调用获取当前正执行操作的用户:
Subject currentUser = SecurityUtils.getSubject();
使用 SecurityUtils.getSubject()可以得到当前执行(操作的)Subject。 Subject是一个安全术语,大概意思就是安全领域视角中的“当前执行操作的用户”。没有使用“User”的原因是这个词通常会联想到人,而在安全领域,术语“Subject”可能是指人,或者第三方进程,定时任务,后台账户等等。所以Subject大概就是“当前正与软件交互的事物”。多数情况下,你可以认为Subject就是Shiro中的“User”。
getSubject()调用在独立程序中返回的Subject是基于程序特定位置的用户数据,而在服务器环境下返回的Subject是基于与当前进程或当前请求关联的用户数据。.
现在有了Subject,可以做些什么呢?
如果想让某些数据在用户的当前会话中都可使用,可以如下:
Session session = currentUser.getSession(); session.setAttribute( "someKey", "aValue" );
这个Session是Shiro特有的,它提供了HttpSessions大部分的以及其他一些功能,而最大的不同是,Session是不需要HTTP 环境的。
如果部署到web环境,Session默认使用HttpSession。但若是在非web环境,就比如本教程,Shiro会自动使用它自有的企业会话管理功能。也就是说,不管什么样的环境下,你只需要使用一样的API就可以了!这简直就是打开了新世界的大门,从此不必再纠缠于HttpSession或EJB的Session Beans,而且任何的客户端都可以共享会话数据了。
现在可以得到Subject以及Session了,那么对于做那些真正实用的事情(比如检查是否允许做什么或是查看权限角色么的)又怎样呢?
好吧,我们只能对一个已知的用户做那样的判定/检查。Subject的实例虽然代表的是当前用户——不过,当前用户又是谁呢?至少在登录过前还是匿名的,所以我们需要这么做:
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"); // 使用“记住我”就只需这一行(无需配置,自带支持) token.setRememberMe(true); currentUser.login(token); }
就是这样,简直不能更简单了!
不过要是登录失败了呢?你可以通过捕捉各种异常来知晓原因并做出相应的回应:
try { currentUser.login( token ); // 如果没有问题,那就对了,搞定! } catch ( UnknownAccountException uae ) { // 未知用户的帐号,告诉他们错误信息? } catch ( IncorrectCredentialsException ice ) { // 密码不匹配,重试? } catch ( LockedAccountException lae ) { // 该帐号被锁定了,无法登录。给他们显示一条信息? } ... more types exceptions to check if you want ... } catch ( AuthenticationException ae ) { //unexpected condition - error? }
还有很多类型的异常可以检查,或者也可以抛出自己定义的异常。详情还是参考文档AuthenticationException JavaDoc。
提醒一下 安全领域的最佳实践是给用户提供通用的登录错误信息,毕竟你也不想帮助黑客们根据不同的提示攻破系统。 |
好吧,现在有一个登录的用户了,还可以做点什么呢?
看看登录用户的身份吧:
//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:weild" ) ) { 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类
通过上面的示例,下面就是我们最终的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:weild")) { 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); } }
总结
希望这篇介绍能帮助你了解如何搭建一个Shiro环境以及一些主要的概念,比如Subject、SecurityManager等。
不过这只是一个简单应用。你可能会问:“如果要使用更加复杂而不是INI文件配置的用户数据源该怎么做呢?”
解决这个问题需要对Shiro的架构和配置机制有更深入的理解,我们下一章Architecture再探讨。