今天我们来聊聊Shiro的那些事。

Shiro的基本介绍

1.Shiro是什么?

很多人对Shiro并不了解,Shiro是Apache(这公司啥玩意都做)公司发布的一款安全框架,主要用于Java,Shiro具有以下功能:

  • 认证 - 用户身份识别,常被称为用户“登录”;
  • 授权 - 访问控制;
  • 密码加密 - 保护或隐藏数据防止被偷窥;
  • 会话管理 - 每个用户相关的时间敏感的状态。

Shiro可以提供安全而全面的管理服务,广泛应用与JavaEE后台的权限管理。Shiro相对于其他的安全框架(如:Spring Security)更加轻量级。

2.关于Shiro的组成

Shiro拥有三大核心组件:Subject, SecurityManager 和 Realms。如图:

Shiro三大组件
Shiro三大组件

下面我们对三大组件做一个简单的了解:

Subject:即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。但考虑到大多数目的和用途,你可以把它认为是Shiro的“用户”概念。

Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。

SecurityManager:它是Shiro框架的核心,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。

Realm: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。

给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个。

Shiro内置了可以连接大量安全数据源(又名目录)的Realm,如LDAP、关系数据库(JDBC)、类似INI的文本配置资源以及属性文件等。如果缺省的Realm不能满足需求,你还可以插入代表自定义数据源的自己的Realm实现。

Shiro完整架构图
Shiro完整架构图

其实,Shiro组件还包括:

Authenticator :认证就是核实用户身份的过程。这个过程的常见例子是大家都熟悉的“用户/密码”组合。多数用户在登录软件系统时,通常提供自己的用户名(当事人)和支持他们的密码(证书)。如果存储在系统中的密码(或密码表示)与用户提供的匹配,他们就被认为通过认证。

Authorizer :授权实质上就是访问控制 - 控制用户能够访问应用中的哪些内容,比如资源、Web页面等等。

SessionManager :在安全框架领域,Apache Shiro提供了一些独特的东西:可在任何应用或架构层一致地使用Session API。即,Shiro为任何应用提供了一个会话编程范式 - 从小型后台独立应用到大型集群Web应用。这意味着,那些希望使用会话的应用开发者,不必被迫使用Servlet或EJB容器了。或者,如果正在使用这些容器,开发者现在也可以选择使用在任何层统一一致的会话API,取代Servlet或EJB机制。

CacheManager :对Shiro的其他组件提供缓存支持。

Shiro的认证

认证就是验证用户身份的过程。在认证过程中,用户需要提交实体信息(Principals)和凭据信息(Credentials)以检验用户是否合法。最常见的“实体/凭证”组合便是“用户名/密码”组合。

1.实体/凭证的获取
1
2
3
4
//Example using most common scenario of username/password pair:
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
//”Remember Me” built-in:
token.setRememberMe(true);

到这里,系统记住了用户的相关信息,但是它并非是完全认证通过的用户,当你访问需要认证用户的功能时,你仍然需要重新提交认证信息。

如你登陆某网站,网站会默认记住登录的用户,再次访问网站时,对于非敏感的页面功能,页面上会显示记住的用户信息,但是当你访问网站敏感(如账户信息)信息信息时仍然需要再次进行登录认证。

2.实体/凭证的提交
1
2
Subject currentUser = SecurityUtils.getSubject();
currentUser.login(token);
3.认证处理
1
2
3
4
5
6
7
8
9
10
11
try {
currentUser.login(token);
} catch ( UnknownAccountException uae ) { ...
} catch ( IncorrectCredentialsException ice ) { ...
} catch ( LockedAccountException lae ) { ...
} catch ( ExcessiveAttemptsException eae ) { ...
} ... catch your own ...
} catch ( AuthenticationException ae ) {
//unexpected error?
}

如果login方法执行完毕且没有抛出任何异常信息,那么便认为用户认证通过。之后在应用程序任意地方调用SecurityUtils.getSubject() 都可以获取到当前认证通过的用户实例,使用subject.isAuthenticated()判断用户是否已验证都将返回true.
相反,如果login方法执行过程中抛出异常,那么将认为认证失败。Shiro有着丰富的层次鲜明的异常类来描述认证失败的原因,如代码示例。

4.登出操作

登出操作可以通过调用subject.logout()来删除你的登录信息,如:

1
currentUser.logout(); //removes all identifying information and invalidates their session too
5.认证内部处理机制

下面将详细解说Shiro认证的内部处理机制。

认证处理
认证处理

如图,Shiro有着这样的执行顺序:

1、应用程序构建了一个终端用户认证信息的AuthenticationToken 实例后,调用Subject.login方法。
2、Subject的实例通常是DelegatingSubject类(或子类)的实例对象,在认证开始时,会委托应用程序设置的securityManager实例调用securityManager.login(token)方法。
3、SecurityManager接受到token(令牌)信息后会委托内置的Authenticator的实例(通常都是ModularRealmAuthenticator类的实例)调用authenticator.authenticate(token). ModularRealmAuthenticator在认证过程中会对设置的一个或多个Realm实例进行适配,它实际上为Shiro提供了一个可拔插的认证机制。
4、如果在应用程序中配置了多个Realm,ModularRealmAuthenticator会根据配置的AuthenticationStrategy(认证策略)来进行多Realm的认证过程。在Realm被调用后,AuthenticationStrategy将对每一个Realm的结果作出响应。
注:如果应用程序中仅配置了一个Realm,Realm将被直接调用而无需再配置认证策略。
5、判断每一个Realm是否支持提交的token,如果支持,Realm将调用getAuthenticationInfo(token); getAuthenticationInfo 方法就是实际认证处理,我们通过覆盖Realm的doGetAuthenticationInfo方法来编写我们自定义的认证处理。

Shiro的授权

授权即访问控制,它将判断用户在应用程序中对资源是否拥有相应的访问权限。
如,判断一个用户有查看页面的权限,编辑数据的权限,拥有某一按钮的权限,以及是否拥有打印的权限等等。

授权的三要素

授权有着三个核心元素:权限、角色和用户。

权限
权限是Apache Shiro安全机制最核心的元素。它在应用程序中明确声明了被允许的行为和表现。一个格式良好好的权限声明可以清晰表达出用户对该资源拥有的权限。
大多数的资源会支持典型的CRUD操作(create,read,update,delete),但是任何操作建立在特定的资源上才是有意义的。因此,权限声明的根本思想就是建立在资源以及操作上。
而我们通过权限声明仅仅能了解这个权限可以在应用程序中做些什么,而不能确定谁拥有此权限。
于是,我们就需要在应用程序中对用户和权限建立关联。
通常的做法就是将权限分配给某个角色,然后将这个角色关联一个或多个用户。

权限声明及粒度
Shiro权限声明通常是使用以冒号分隔的表达式。就像前文所讲,一个权限表达式可以清晰的指定资源类型,允许的操作,可访问的数据。同时,Shiro权限表达式支持简单的通配符,可以更加灵活的进行权限设置。
下面以实例来说明权限表达式。
可查询用户数据
User:view
可查询或编辑用户数据
User:view,edit
可对用户数据进行所有操作
User:* 或 user
可编辑id为123的用户数据
User:edit:123

角色
Shiro支持两种角色模式:
1、传统角色:一个角色代表着一系列的操作,当需要对某一操作进行授权验证时,只需判断是否是该角色即可。这种角色权限相对简单、模糊,不利于扩展。
2、权限角色:一个角色拥有一个权限的集合。授权验证时,需要判断当前角色是否拥有该权限。这种角色权限可以对该角色进行详细的权限描述,适合更复杂的权限设计。
下面将详细描述对两种角色模式的授权实现。

授权实现

Shiro支持三种方式实现授权过程:

  • 编码实现
  • 注解实现
  • JSP Taglig实现

1、基于编码的授权实现

1.1基于传统角色授权实现

当需要验证用户是否拥有某个角色时,可以调用Subject 实例的hasRole*方法验证。

1
2
3
4
5
6
Subject currentUser = SecurityUtils.getSubject();
if (currentUser.hasRole("administrator")) {
//show the admin button
} else {
//don't show the button? Grey it out?
}

相关验证方法如下:

Subject方法 描述
hasRole(String roleName) 当用户拥有指定角色时,返回true
hasRoles(List roleNames) 按照列表顺序返回相应的一个boolean值数组
hasAllRoles(Collection roleNames) 如果用户拥有所有指定角色时,返回true

断言支持

Shiro还支持以断言的方式进行授权验证。断言成功,不返回任何值,程序继续执行;断言失败时,将抛出异常信息。使用断言,可以使我们的代码更加简洁。

1
2
3
4
5
Subject currentUser = SecurityUtils.getSubject();
//guarantee that the current user is a bank teller and
//therefore allowed to open the account:
currentUser.checkRole("bankTeller");
openBankAccount();

断言的相关方法:

Subject方法 描述
checkRole(String roleName) 断言用户是否拥有指定角色
checkRoles(Collection roleNames) 断言用户是否拥有所有指定角色
checkRoles(String… roleNames) 对上一方法的方法重载

1.2 基于权限角色授权实现

相比传统角色模式,基于权限的角色模式耦合性要更低些,它不会因角色的改变而对源代码进行修改,因此,基于权限的角色模式是更好的访问控制方式。

它的代码实现有以下几种实现方式:

a.基于权限对象的实现

创建org.apache.shiro.authz.Permission的实例,将该实例对象作为参数传递给Subject.isPermitted()进行验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Permission printPermission = new PrinterPermission("laserjet4400n", "print");
Subject currentUser = SecurityUtils.getSubject();
if (currentUser.isPermitted(printPermission)) {
//show the Print button
} else {
//don't show the button? Grey it out?
}
Permission printPermission = new PrinterPermission("laserjet4400n", "print");
Subject currentUser = SecurityUtils.getSubject();
if (currentUser.isPermitted(printPermission)) {
//show the Print button
} else {
//don't show the button? Grey it out?
}

相关方法如下:

Subject方法 描述
isPermitted(Permission p) Subject拥有制定权限时,返回treu
isPermitted(List perms) 返回对应权限的boolean数组
isPermittedAll(Collection perms) Subject拥有所有制定权限时,返回true

2、 基于字符串的实现

相比笨重的基于对象的实现方式,基于字符串的实现便显得更加简洁。

1
2
3
4
5
6
Subject currentUser = SecurityUtils.getSubject();
if (currentUser.isPermitted("printer:print:laserjet4400n")) {
//show the Print button
} else {
//don't show the button? Grey it out?
}

使用冒号分隔的权限表达式是org.apache.shiro.authz.permission.WildcardPermission 默认支持的实现方式。

这里分别代表了 资源类型:操作:资源ID

类似基于对象的实现相关方法,基于字符串的实现相关方法:

isPermitted(String perm)isPermitted(String... perms)isPermittedAll(String... perms)

a.基于权限对象的断言实现

1
2
3
4
5
6
Subject currentUser = SecurityUtils.getSubject();
//guarantee that the current user is permitted
//to open a bank account:
Permission p = new AccountPermission("open");
currentUser.checkPermission(p);
openBankAccount();

b.基于字符串的断言实现

1
2
3
4
5
Subject currentUser = SecurityUtils.getSubject();
//guarantee that the current user is permitted
//to open a bank account:
currentUser.checkPermission("account:open");
openBankAccount();

断言实现的相关方法

Subject方法 说明
checkPermission(Permission p) 断言用户是否拥有制定权限
checkPermission(String perm) 断言用户是否拥有制定权限
checkPermissions(Collection perms) 断言用户是否拥有所有指定权限
checkPermissions(String… perms) 断言用户是否拥有所有指定权限

3、基于注解的授权实现

Shiro注解支持AspectJ、Spring、Google-Guice等,可根据应用进行不同的配置。

相关的注解:

1
@ RequiresAuthentication

可以用户类/属性/方法,用于表明当前用户需是经过认证的用户。

1
2
3
4
5
6
@RequiresAuthentication
public void updateAccount(Account userAccount) {
//this method will only be invoked by a
//Subject that is guaranteed authenticated
...
}
1
2
3
4
@ RequiresGuest
表明该用户需为”guest”用户
@ RequiresPermissions
当前用户需拥有制定权限
1
2
3
4
5
6
@RequiresPermissions("account:create")
public void createAccount(Account account) {
//this method will only be invoked by a Subject
//that is permitted to create an account
...
}
1
2
3
4
@RequiresRoles
当前用户需拥有制定角色
@ RequiresUser
当前用户需为已认证用户或已记住用户

3、基于JSP TAG的授权实现

Shiro提供了一套JSP标签库来实现页面级的授权控制。

在使用Shiro标签库前,首先需要在JSP引入shiro标签:

1
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>

下面一一介绍Shiro的标签:

guest标签

验证当前用户是否为“访客”,即未认证(包含未记住)的用户

1
2
3
<shiro:guest>
Hi there! Please <a href="login.jsp">Login</a> or <a href="signup.jsp">Signup</a> today!
</shiro:guest>

user标签

认证通过或已记住的用户

1
2
3
<shiro:user>
Welcome back John! Not John? Click <a href="login.jsp">here<a> to login.
</shiro:user>

authenticated标签

已认证通过的用户。不包含已记住的用户,这是与user标签的区别所在。

1
2
3
<shiro:authenticated>
<a href="updateAccount.jsp">Update your contact information</a>.
</shiro:authenticated>

notAuthenticated标签

未认证通过用户,与authenticated标签相对应。与guest标签的区别是,该标签包含已记住用户。

1
2
<shiro:notAuthenticated>
Please <a href="login.jsp">login</a> in order to update your credit card information. </shiro:notAuthenticated>
Shiro授权的内部处理机制
img
img

1、在应用程序中调用授权验证方法(Subject的isPermitted或hasRole等)
2、Sbuject的实例通常是DelegatingSubject类(或子类)的实例对象,在认证开始时,会委托应用程序设置的securityManager实例调用相应的isPermitted或hasRole方法。
3、接下来SecurityManager会委托内置的Authorizer的实例(默认是ModularRealmAuthorizer 类的实例,类似认证实例,它同样支持一个或多个Realm实例认证)调用相应的授权方法。
4、每一个Realm将检查是否实现了相同的 Authorizer 接口。然后,将调用Reaml自己的相应的授权验证方法。

当使用多个Realm时,不同于认证策略处理方式,授权处理过程中:
1、当调用Realm出现异常时,将立即抛出异常,结束授权验证。
2、只要有一个Realm验证成功,那么将认为授权成功,立即返回,结束认证。

…未完待续!