一、简介

1、简介

Apache Shiro是一个功能强大且易于使用的Java安全框架,它可以执行身份验证(Authentication)、授权(Authorization)、加密(Cryptography)和管理会话(Session Management)。借助于Shiro易于理解的API,可以快速轻松地保护任何应用程序(从命令行应用程序、移动应用程序到最大的 web 和企业应用程序)。

2、核心概念

  • Subject

Subject是一个安全术语,可以表示”当前正在执行的用户”;在安全领域它可以表示人,也可以表示第三方进程、守护进程或任何类似的东西。

  • SecurityManager

如果Subject表示当前用户的安全操作(security operations),那么SecurityManager则管理所有用户的安全操作。它可以通过Java代码、Spring XML、.properties.ini文件等来配置。

  • Realms

Realm(领域)是Shiro和应用程序的安全数据之间的”桥梁”或”连接器”,当需要与安全性相关的数据进行交互以执行身份验证和授权时,Shiro会从应用程序配置的一个或多个Realms中查找相关内容。

二、快速入门

1、pom.xml

添加依赖:

<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-core</artifactId>
	<version>1.6.0</version>
</dependency>
<dependency>
	<groupId>org.slf4j</groupId>
	<artifactId>slf4j-log4j12</artifactId>
	<version>1.7.25</version>
</dependency>
<dependency>
	<groupId>org.slf4j</groupId>
	<artifactId>jcl-over-slf4j</artifactId>
	<version>1.7.25</version>
</dependency>
<dependency>
	<groupId>log4j</groupId>
	<artifactId>log4j</artifactId>
	<version>1.2.17</version>
</dependency>

2、log4j.properties

log4j.rootLogger=INFO, stdout

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n

# Default Shiro logging
log4j.logger.org.apache.shiro=INFO

# Disable verbose logging
log4j.logger.org.apache.shiro.util.ThreadContext=WARN
log4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN

3、shiro.ini

shiro配置:

[roles]
admin = *
schwartz = lightsaber:*
goodguy = winnebago:drive:eagle5

[users]
root = secret, admin
guest = guest, guest
presidentskroob = 12345, president
darkhelmet = ludicrousspeed, darklord, schwartz
lonestarr = vespa, goodguy, schwartz

上面的配置中定义了三个角色:

  • admin

拥有所有权限

  • schwartz

可以使用光剑(lightsaber)做任何事

  • goodguy

可以驾驶车牌号为eagle5的winnebago车

五个用户:

  • root

密码为secret,角色为admin

  • guest

密码为guest,角色为guest

  • presidentskroob

密码为12345,角色为president

  • darkhelmet

密码为ludicrousspeed,角色为darklord和schwartz

  • lonestarr

密码为vespa,角色为goodguy和schwartz

4、Quickstart.java

public class Quickstart {

    private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);


    public static void main(String[] args) {

    	//使用指定配置创建Shiro SecurityManager
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        SecurityManager securityManager = factory.getInstance();

        //作为快速入门样例,使SecurityManager作为JVM单例访问,通过静态内存使SecurityManager实例可用于整个应用程序;通常使用容器配置或web.xml配置
        SecurityUtils.setSecurityManager(securityManager);

        //获取当前执行的用户
        Subject currentUser = SecurityUtils.getSubject();

        //使用Session做一些事情
        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");
			//支持remember me
            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) {
                //意外情况或 错误
            }
        }

        //打印其标识主体(在这种情况下,为用户名):
        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);
    }
}

主要输出内容如下:

Enabling session validation scheduler... 
Retrieved the correct value! [aValue] 
User [lonestarr] logged in successfully. 
May the Schwartz be with you! 
You may use a lightsaber ring.  Use it wisely. 
You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'.  Here are the keys - have fun! 

如果在登录时输入的用户名不存在或密码错误,会进入相应异常分支。

三、权限

权限是安全策略中最底层的结构,它们明确定义可以执行的”操作”,例如:打开文件、查看某个列表、打印文档或删除某个用户等。

1、通配符权限

为了启用易于处理且可读的权限声明,Shiro提供了强大而直观的权限语法:WildcardPermission。

  • 简单用法
queryPrinter
  • 复杂用法

通配符权限支持多个级别(levels)或部分(parts),上面的权限定义可以表示为:

printer:query

第一部分printer表示操作的域,第二部分query表示执行的动作;同样,对于printer的其他权限可以表示为:

printer:print
printer:manage

每个部分也可以包含多个值,例如:除了向用户授予printer:printprinter:query权限外,也可以按如下方式直接授予一个权限:

printer:print,query

如果想授予用户特定域的所有值,可以使用通配符*

printer:*

等价于:

printer:query,print,manage

也可以授予用户所有域(不仅仅是打印机)的查看操作:

*:view
  • 实例级别

通配符权限的另一种常见用法是实例级访问控制,由三个部分组成:域、操作和要执行的实例,例如:

printer:query:lp7200
printer:print:epsoncolor

指定多个操作:

printer:query,print:lp7200

同样支持通配符*

#所有打印机的打印操作
printer:print:*

#所有打印机的所有操作
printer:*:*

#单个打印机的所有操作
printer:*:lp7200

如果后面的部分缺失,则表示*;例如:

printer:print
#等价于
printer:print:*
printer
#等价于
printer:*:*

2、检查权限

向用户授予了queryPrinter权限后,可以通过以下方法检查用户是否具有该权限:

Subject subject = SecurityUtils.getSubject();

subject.isPermitted("queryPrinter")
//相当于
subject.isPermitted(new WildcardPermission("queryPrinter"))

实例级别检查:

if ( SecurityUtils.getSubject().isPermitted("printer:print:lp7200") ) {
    //print the document to the lp7200 printer }
}

四、配置

1、程序配置

创建SecurityManager并将其提供给应用程序的最简单的方法是创建一个org.apache.shiro.mgt.DefaultSecurityManager并将其连接到代码中,例如:

Realm realm = //实例化或获取Realm实例
SecurityManager securityManager = new DefaultSecurityManager(realm);

//通过静态内存使SecurityManager实例可用于整个应用程序
SecurityUtils.setSecurityManager(securityManager);

至此便拥有了一个适用于许多应用程序的功能齐全的Shiro环境。

2、使用INI创建SecurityManager

可以从INI资源路径创建SecurityManager实例,资源可以来自文件系统、类路径、或者网络,前缀分别为file:classpath:url:。例如:

Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);

3、使用INI实例创建SecurityManager

也可以使用org.apache.shiro.config.Ini类以编程方式构造INI配置,Ini类的功能类似于JDK的java.util.Properties类,但还支持按section name进行分段。

Ini ini = new Ini();
//根据需要填充Ini实例
...
Factory<SecurityManager> factory = new IniSecurityManagerFactory(ini);
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);

4、ini配置

大多数应用程序都受益于基于文本的配置,因为该配置可以独立于源代码进行修改。

INI基本上是一种文本配置,由唯一命名的键/值对组成;键仅在每个部分(section)中是唯一的,而不是在整个配置中唯一(不同于JDK的Properties)。但它的每个部分(section)可以看作是一个单独的Properties定义。

注释行以;开头。

Shiro INI配置样例:

# =======================
# Shiro INI configuration
# =======================

[main]
#此处定义对象及其属性,例如SecurityManager、Realms和创建SecurityManager所需的其他任何东西

[users]
#当仅需要少量静态定义的用户帐户时定义,此部分适用于简单部署的情况

[roles]
#当仅需要少量静态定义的角色时定义,此部分适用于简单部署的情况

[urls]
#此部分用于Web应用程序中基于URL的安全性
  • main

此部分中可以配置应用程序的SecurityManager实例及其任何依赖项,例如Realm。样例:

[main]
sha256Matcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher

#实例化MyRealm类的对象
myRealm = com.company.shiro.realm.MyRealm
#设置对象属性
myRealm.connectionTimeout = 30000
myRealm.username = jsmith
myRealm.password = secret
#引用对象实例
myRealm.credentialsMatcher = $sha256Matcher

#嵌套属性 object.property1.property2....propertyN.value = blah
securityManager.sessionManager.globalSessionTimeout = 1800000

sessionListener1 = com.company.my.SessionListenerImplementation
sessionListener2 = com.company.my.other.SessionListenerImplementation
#集合属性(列表),值为以逗号分隔的一组值或对象引用
securityManager.sessionManager.sessionListeners = $sessionListener1, $sessionListener2

object1 = com.company.some.Class
object2 = com.company.another.Class
anObject = some.class.with.a.Map.property
#集合属性(Map),指定键值对的逗号分隔列表,每个键值对以冒号分隔
anObject.mapProperty = key1:$object1, key2:$object2
  • users

此部分中可以定义一组静态的用户帐户,这在用户帐户数量很少或不需要在运行时动态创建用户帐户的环境中最有用。

格式为:username= password,roleName1,roleName2,...,roleNameN。样例:

[users]
admin = secret
lonestarr = vespa, goodguy, schwartz
darkhelmet = ludicrousspeed, badguy, schwartz

加密密码:如果不希望[users]部分的密码为纯文本格式,则可以根据需要使用哈希算法(MD5、Sha1、Sha256等)对它们进行加密,并将结果字符串用作密码值。

指定哈希文本密码值后必须告诉Shiro这些密码已加密,可以通过配置[main]部分中隐式创建的配置iniRealm,并使用CredentialsMatcher 指定相应算法:

[main]
...
sha256Matcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher
...
iniRealm.credentialsMatcher = $sha256Matcher
...

[users]
# user1 = sha256-hashed-hex-encoded password, role1, role2, ...
user1 = 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b, role1, role2, ...

默认情况下,密码字符串应使用十六进制编码,如果用户的密码字符串是Base64编码的,则需指定:

[main]
...
# true = hex, false = base64:
sha256Matcher.storedCredentialsHexEncoded = false
  • roles

此部分中将将权限与[users]中定义的角色相关联,同样在角色数量少的环境中或不需要在运行时动态创建角色的环境中很有用。

格式:rolename= PermissionDefinition1,PermissionDefinition2,...,permissionDefinitionN。样例:

[roles]
admin = *
schwartz = lightsaber:*
goodguy = winnebago:drive:eagle5
  • urls

    此部分可以为应用程序中任何匹配的URL路径定义临时过滤器链;左侧为相对于Web应用程序上下文根目录HttpServletRequest.getContextPath()的路径表达式,右侧为逗号分隔的过滤器列表,例如:

      [urls]
      /account/** = ssl, authc
      /index.html = anon
      /user/create = anon
      /user/** = authc
      /admin/** = authc, roles[administrator]
      /rest/** = authc, rest
      /remoting/rpc/** = authc, perms["remote:invoke"]
    

    上面的配置中,对于请求/account或它的任何子路径(/account/foo/account/bar/baz)都会触发ssl,authc过滤器链。

    • 过滤器链

    格式:

      filter1[optional_config1], filter2[optional_config2], ..., filterN[optional_configN]
    

    其中:filterN是在[main]中定义的过滤器bean的名称;[optional_configN]为可选的特定路径的过滤器配置,如果过滤器不需要该URL路径的特定配置,则可省略方括号,将filterN[]变为filterN。例如:

      [main]
      ...
      myFilter = com.company.web.some.FilterImplementation
      myFilter.property1 = value1
      ...
    
      [urls]
      ...
      /some/path/** = myFilter
    
    • 默认过滤器

    运行Web应用程序时,Shiro会创建一些有用的默认Filter实例:

    Filter Name Class
    anon org.apache.shiro.web.filter.authc.AnonymousFilter
    authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter
    authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
    authcBearer org.apache.shiro.web.filter.authc.BearerHttpAuthenticationFilter
    invalidRequest org.apache.shiro.web.filter.InvalidRequestFilter
    logout org.apache.shiro.web.filter.authc.LogoutFilter
    noSessionCreation org.apache.shiro.web.filter.session.NoSessionCreationFilter
    perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
    port org.apache.shiro.web.filter.authz.PortFilter
    rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
    roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
    ssl org.apache.shiro.web.filter.authz.SslFilter
    user org.apache.shiro.web.filter.authc.UserFilter
    • 过滤器启用/禁用

    将过滤器的enabled属性设置为true或false,可以为所有请求启用或禁用该过滤器;例如:

      [main]
      ...
      # configure Shiro's default 'ssl' filter to be disabled while testing:
      ssl.enabled = false
    
      [urls]
      ...
      /some/path = ssl, authc
      /another/path = ssl, roles[admin]
      ...
    
    • 全局过滤器

    全局过滤器可以为所有路由添加过滤器,默认情况下,全局过滤器包含invalidRequest过滤器,此过滤器可以阻止已知的恶意攻击。

    自定义或禁用全局过滤器:

      [main]
      ...
      # disable Global Filters
      filterChainResolver.globalFilters = null
    

    定义全局过滤器列表:

      [main]
      ...
      filterChainResolver.globalFilters = invalidRequest, port
    

五、身份验证

身份验证是通过向Shiro提交用户的主体(principals)和凭据(credentials)来查看它们是否与应用程序期望值相匹配。

  • principals

它是主题(Subject)的识别属性(identifying attributes),例如:用户名、邮件地址等。

  • credentials

它是仅由主题(Subject)知道的秘密值,例如:密码、生物特征数据等。

1、验证主题

身份验证的过程可以分为三个步骤:

  • 收集主题的主体和凭证
//Example using most common scenario of username/password pair:
UsernamePasswordToken token = new UsernamePasswordToken(username, password);

//"Remember Me" built-in: 
token.setRememberMe(true);

UsernamePasswordToken是最常见的用户名/密码身份验证方法,实现了org.apache.shiro.authc.AuthenticationToken接口。

同时,上面还使用了”记住我”的功能,此功能可以确保Shiro能够在以后返回应用程序时记住用户身份。

  • 提交主体和凭据
Subject currentUser = SecurityUtils.getSubject();

currentUser.login(token);

通过调用login方法尝试身份验证。

  • 处理成功或失败

如果login方法正常返回则表示认证通过,后续调用SecurityUtils.getSubject()将返回已通过身份验证的Subject实例,此实例调用subject.isAuthenticated()方法将返回true。

Shiro提供了丰富的运行时AuthenticationException层次结构,可以准确说明尝试失败的原因;如果认证失败,可以通过try/catch来获取异常原因:

try {
    currentUser.login(token);
} catch ( UnknownAccountException uae ) { ...
} catch ( IncorrectCredentialsException ice ) { ...
} catch ( LockedAccountException lae ) { ...
} catch ( ExcessiveAttemptsException eae ) { ...
} ... catch your own ...
} catch ( AuthenticationException ae ) {
    //unexpected error?
}

//No problems, continue on as expected...

如果现有的异常类不能满足需求,可以创建自定义异常来处理。

2、注销

当Subject完成与应用程序交互后,可以调用subject.logout()注销所有的身份信息:

currentUser.logout(); //removes all identifying information and invalidates their session too.

3、认证步骤

前面介绍了在应用程序中对Subject进行身份验证,下面将介绍身份验证时Shiro内部的验证步骤:

  • 应用程序调用Subject的login方法,并传入用户主体和凭据的AuthenticationToken实例。

  • Subject实例通过调用securityManager.login(token)来委拖SecurityManager做身份验证。

  • SecurityManager接收令牌(token),并通过调用authenticator.authenticate(token)来委派给其内部的Authenticator实例,通常是ModularRealmAuthenticator的实例,它支持在身份验证期间协调一个或多个Realm实例。

  • 如果为该应用程序配置了多个Realm,则ModularRealmAuthenticator实例将使用其配置的AuthenticationStrategy发起多域验证尝试。

  • 判断每个已配置的Realm来查看其是否支持提交的AuthenticationToken,如果支持,则使用提交的令牌token来调用该Realm的getAuthenticationInfo方法,此方法表示对该特定领域的单个身份验证尝试。

4、认证者

Shiro的SecurityManager默认使用ModularRealmAuthenticator实例。

在单一领域的应用程序中,ModularRealmAuthenticator将直接调用Realm;如果配置了两个或更多领域,则它将使用AuthenticationStrategy来协调尝试的发生方式。

也可以使用自定义的Authenticator实现来配置:

[main]
...
authenticator = com.foo.bar.CustomAuthenticator

securityManager.authenticator = $authenticator

5、认证策略

当为一个应用程序配置两个或多个领域时,ModularRealmAuthenticator依赖于内部AuthenticationStrategy组件来确定认证尝试成功或失败的条件。例如:如果只有一个Realm认证AuthenticationToken成功,而其他所有认证都失败,则该认证尝试是否被视为成功?还是所有领域都必须成功进行身份验证才能将整体尝试视为成功?

AuthenticationStrategy是无状态组件,在尝试进行身份验证时会被查询4次:

  • 在任何领域(Realm)被调用之前

  • 在调用单个Realm的getAuthenticationInfo()方法之前

  • 在调用单个Realm的getAuthenticationInfo()方法之后

  • 在所有领域(Realm)都被调用之后

另外,AuthenticationStrategy负责将每个成功Realm的结果汇总聚合成一个单一的AuthenticationInfo形式,这个最终的AuthenticationInfo实例是Authenticator实例返回的内容,也是Shiro用来表示Subject的最终身份(principals)的东西。

Shiro有3种具体的AuthenticationStrategy实现方式:

认证策略类 描述
AtLeastOneSuccessfulStrategy 如果一个(或多个)领域成功认证,则整个尝试都被视为成功。如果没有成功的身份验证,则尝试将失败。
FirstSuccessfulStrategy 仅使用从第一个成功通过身份验证的领域返回的信息。所有其他领域将被忽略。如果没有成功的身份验证,则尝试将失败。
AllSuccessfulStrategy 所有配置的领域都必须成功进行身份验证,才能将整体尝试视为成功。如果有任何未成功的身份验证,则尝试将失败。

默认的策略为AtLeastOneSuccessfulStrategy。如果需要自定义身份验证策略,可以继承AbstractAuthenticationStrategy类自动实现各域结果的汇聚,并且需要在INI中配置:

[main]
...
authcStrategy = org.apache.shiro.authc.pam.FirstSuccessfulStrategy

securityManager.authenticator.authenticationStrategy = $authcStrategy

...

6、领域认证顺序

  • 隐式排序

按照在INI文件中定义的顺序,下面的调用顺序为:blahRealm、fooRealm、barRealm

blahRealm = com.company.blah.Realm
...
fooRealm = com.company.foo.Realm
...
barRealm = com.company.another.Realm

等价于显示排序:

securityManager.realms = $blahRealm, $fooRealm, $barRealm
  • 显示排序

设置securityManager的realms属性来显示定义调用顺序:

blahRealm = com.company.blah.Realm
...
fooRealm = com.company.foo.Realm
...
barRealm = com.company.another.Realm

securityManager.realms = $fooRealm, $barRealm, $blahRealm
...

六、授权

授权是管理对资源访问的过程,即控制谁可以访问什么应用程序。

1、授权要素

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

  • 权限

权限代表了安全策略中最基本的元素,它是有关行为的声明,并明确表示可以在应用程序中完成的操作。权限声明至少应该基于”资源和操作”,例如:打开文件、查看用户列表等。

  • 角色

    • 隐式角色

      应用程序仅基于角色名称隐含一组行为(权限),例如:admin表示管理员角色,但软件程序无法基于此名称知道此角色的用户可以执行X、Y和Z,此种角色行为仅由名称暗示。

    • 显示角色

      显式角色本质上是实际权限声明的集合,这种形式中应用程序可以明确的知道具有此角色可以执行或不能执行的确切行为。Shiro提倡使用权限和显式角色。

  • 用户

    Subject实际上是Shiro的用户概念。

2、程序授权

此种方式直接以编程的方式与当前实例进行交互。

  • 基于角色的授权

    • 角色检查

    如果是基于简单/传统的隐式角色名称控制访问,可以执行角色检查:

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

    类似的还有hasRoles(List<String> roleNames)hasAllRoles(Collection<String> roleNames)方法。

    • 角色断言

    可以在执行逻辑之前简单地断言它们是否具有预期的角色,如果Subject没有预期的角色,则抛出AuthorizationException异常:

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

    它也有类似的方法checkRoles(roleNames)

  • 基于权限的授权

    如前面所述,执行访问控制的更好方法通常是基于权限的授权。

    基于权限的授权,由于它与应用程序的原始功能紧密相关,因此基于权限的授权源代码会在功能更改时更改,与角色授权相比,代码受到的影响要小得多。

    • 权限检查

      • 基于对象的权限检查

      实例化org.apache.shiro.authz.Permission接口的实例并传递给isPermitted*方法来检查:

        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?
        }
      

      类似的还有isPermitted(List<Permission> perms)isPermittedAll(Collection<Permission> perms)方法。

      • 基于字符串的权限检查

      与基于对象的权限检查方式相比,此种方式有时候显得更简洁一点,它不需要权限对象;例如:

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

      它也有isPermitted(String... perms)isPermittedAll(String... perms)等类似的方法。

      示例显示了Shiro默认的实现WildcardPermission定义的特殊冒号分隔格式,等价的写法:

        Subject currentUser = SecurityUtils.getSubject();
      
        Permission p = new WildcardPermission("printer:print:laserjet4400n");
      
        if (currentUser.isPermitted(p) {
            //show the Print button
        } else {
            //don't show the button?  Grey it out?
        }
      
    • 权限断言

      与角色断言,样例:

        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();
      

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

3、注解授权

  • RequiresAuthentication

此注解要求当前的Subject在其会话期间已经通过身份验证,以便可以访问或调用带注释的类/实例/方法;例如:

@RequiresAuthentication
public void updateAccount(Account userAccount) {
    //this method will only be invoked by a
    //Subject that is guaranteed authenticated
    ...
}

它等价于:

public void updateAccount(Account userAccount) {
    if (!SecurityUtils.getSubject().isAuthenticated()) {
        throw new AuthorizationException(...);
    }

    //Subject is guaranteed authenticated here
    ...
}
  • RequiresGuest

此注解要求当前的Subject是”guest”,即对于要访问或调用的带注释的类/实例/方法,它们在前一个会话中没有经过身份验证或被记住(remembered);例如:

@RequiresGuest
public void signUp(User newUser) {
    //this method will only be invoked by a
    //Subject that is unknown/anonymous
    ...
}

它等价于:

public void signUp(User newUser) {
    Subject currentUser = SecurityUtils.getSubject();
    PrincipalCollection principals = currentUser.getPrincipals();
    if (principals != null && !principals.isEmpty()) {
        //known identity - not a guest:
        throw new AuthorizationException(...);
    }

    //Subject is guaranteed to be a 'guest' here
    ...
}
  • RequiresPermissions

此注解要求当前的Subject要被授予一个或多个权限,以便执行带注释的方法;例如:

@RequiresPermissions("account:create")
public void createAccount(Account account) {
    //this method will only be invoked by a Subject
    //that is permitted to create an account
    ...
}

它等价于:

public void createAccount(Account account) {
    Subject currentUser = SecurityUtils.getSubject();
    if (!subject.isPermitted("account:create")) {
        throw new AuthorizationException(...);
    }

    //Subject is guaranteed to be permitted here
    ...
}
  • RequiresRoles

此注解要求当前Subject具有所有指定的角色,如果不具有则该方法将不会被执行,并抛出AuthorizationException;例如:

@RequiresRoles("administrator")
public void deleteUser(User user) {
    //this method will only be invoked by an administrator
    ...
}

它等价于:

public void deleteUser(User user) {
    Subject currentUser = SecurityUtils.getSubject();
    if (!subject.hasRole("administrator")) {
        throw new AuthorizationException(...);
    }

    //Subject is guaranteed to be an 'administrator' here
    ...
}
  • RequiresUser

此注解要求当前Subject是要访问或调用的带注释的类/实例/方法的应用程序用户;”应用程序用户”定义为具有已知身份的Subject,该身份可能是由于在当前会话期间通过了身份验证或是从上一个会话的”RememberMe”服务中记住的。样例:

@RequiresUser
public void updateAccount(Account account) {
    //this method will only be invoked by a 'user'
    //i.e. a Subject with a known identity
    ...
}

它等价于:

public void updateAccount(Account account) {
    Subject currentUser = SecurityUtils.getSubject();
    PrincipalCollection principals = currentUser.getPrincipals();
    if (principals == null || principals.isEmpty()) {
        //no identity - they're anonymous, not allowed:
        throw new AuthorizationException(...);
    }

    //Subject is guaranteed to have a known identity here
    ...

3、授权步骤

在进行授权时Shiro内部的执行步骤:

  • 通过代码调用Subject的hasRole*checkRole*isPermitted*checkPermission*方法。

  • Subject的实例(通常是DelegatingSubject或子类)通过调用securityManager的几乎相同的hasRole*checkRole*isPermitted*checkPermission*方法。

  • securityManager委托给其内部的org.apache.shiro.authz.Authorizer实例调用相应的方法;authorizer默认是ModularRealmAuthorizer,它支持Realm在任何授权操作期间协调一个或多个实例。

  • 判断每个已配置的Realm来查看其是否实现了Authorizer接口,如果是则执行Realm的hasRole*checkRole*isPermitted*checkPermission*方法。

七、Web支持

1、配置

将Shiro集成到任何Web应用程序中的最简单方法是在web.xml中配置Listener和Filter:

<listener>
    <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
</listener>

...

<filter>
    <filter-name>ShiroFilter</filter-name>
    <filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>ShiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
    <dispatcher>INCLUDE</dispatcher>
    <dispatcher>ERROR</dispatcher>
</filter-mapping>

shiro.ini文件位置的查找顺序依次为:/WEB-INF/shiro.ini、类路径的根目录;如果要将shiro.ini文件放在其他位置,需要在context-param中指定,例如:

<context-param>
    <param-name>shiroConfigLocations</param-name>
    <param-value>/WEB-INF/some/path/myshiro.ini</param-value>
</context-param>

2、会话管理

  • Servlet容器会话

在Web环境中,Shiro的默认会话管理器SessionManager实现类是ServletContainerSessionManager;它将所有会话管理职责委派给运行时Servlet容器,它本质上是Shiro的会话API到servlet容器的桥梁。

会话超时配置:

<session-config>
	<!-- web.xml expects the session timeout in minutes: -->
	<session-timeout>30</session-timeout>
</session-config>
  • 本地(Native)会话

如果希望会话配置和集群可以跨Servlet容器移植(例如,在测试中使用Jetty,但在生产中使用Tomcat或JBoss),或者想控制特定的会话/集群功能,则可以启用Shiro的本地会话管理;这需要在shiro.ini文件中配置本地Web会话管理器来覆盖默认的基于Servlet容器的会话管理器:

[main]
...
sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager
# configure properties (like session timeout) here if desired

# Use the configured native session manager:
securityManager.sessionManager = $sessionManager

会话超时配置(默认为30分钟):

[main]
...
# 3,600,000 milliseconds = 1 hour
securityManager.sessionManager.globalSessionTimeout = 3600000

会话监听器:通过实现SessionListener接口或继承SessionListenerAdapter创建会话监听器来在会话事件发生时做出相应操作。

[main]
...
aSessionListener = com.foo.my.SessionListener
anotherSessionListener = com.foo.my.OtherSessionListener

securityManager.sessionManager.sessionListeners = $aSessionListener, $anotherSessionListener, etc.

3、Remember Me

  • 编程方式
UsernamePasswordToken token = new UsernamePasswordToken(username, password);

token.setRememberMe(true);

SecurityUtils.getSubject().login(token);
  • Cookie配置

可以通过设置默认的RememberMeManager的各种cookie属性来配置:

[main]
...

securityManager.rememberMeManager.cookie.name = foo
securityManager.rememberMeManager.cookie.maxAge = blah
...
  • 自定义RememberMeManager
[main]
...
rememberMeManager = com.my.impl.RememberMeManager
securityManager.rememberMeManager = $rememberMeManager
参考资料:

Apache Shiro

Application Security With Apache Shiro

Apache Shiro Configuration

Apache Shiro Authentication

Apache Shiro Authorization

Session Management

Apache Shiro Web Support

Apache Shiro Realm

Introduction to Apache Shiro