Apache Shiro是一个强大易用的Java安全框架,提供了认证、授权、加密和会话管理等功能,可以应用于各种类型的应用程序。相比Spring Security,它更加小巧简单,使用更加方便,很适合在实际工作中解决实际项目问题。

Shiro的基本功能

Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在 JavaSE 环境,也可以用在 JavaEE 环境。Shiro 可以帮助我们完成:认证、授权、加密、会话管理、与 Web 集成、缓存等。这不就是我们想要的嘛,而且 Shiro 的 API 也是非常简单;其基本功能点如下图所示:

Shiro的基本功能点

Shiro的基本功能包括:

Authentication:身份认证 / 登录,验证用户是不是拥有相应的身份;

Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;

Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的;

Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;

具有以下特点:

Web Support:Web 支持,可以非常容易的集成到 Web 环境;

Caching:缓存,比如用户登录后,其用户信息、拥有的角色 / 权限不必每次去查,这样可以提高效率;

Concurrency:shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;

Testing:提供测试支持;

Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;

Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。

Shiro的架构

从应用程序的角度来看,Shiro本身并不维护用户和权限,需要我们自己并在程序中实现,然后通过相应的借口注入程序。Shiro的工作过程如下:

Shiro的工作过程

Subject

应用程序直接交互的Shiro对象为Subject,  代表当前会话的“用户”,这个用户不一定是具体的人,可以是和应用程序交互的任意主体,如:网络爬虫,机器人等。所有的Subject都绑定到SecurityManager,  对Subject的所有调用都会委托给SecurityManager。可以把Subject认为是一个门面,SecurityManager才是实际的执行者。

每个Subject包含两部分信息:

Principals: 身份信息,可以是用户Id,用户名,手机号码,邮箱等用户唯一性标识;一个主体可以有多个principals,但只有一个Primary principals, 一般是用户名/密码/手机号。

Credentials:凭证信息,只有主体知道的安全值,可以是用户密码,token,数字证书等认证凭据信息。

SecurityManager

所有与安全有关的操作都会与SecurityManager交互。它是Shiro的核心,管理着所有Subject, 也负责和后面的其他组件进行交互,类似于Spring MVC中的DispatcherServlet前端控制器。

Realm

Realm可以翻译作“域”。Shiro从Realm中获取安全数据,如用户、角色、权限,例如SecurityManager要验证用户身份,就需要从一个或多个Realm中获取相应的用户进行比较,以确定当前用户身份是否合法,也需要从Realm中获取用户的角色和权限来验证用户能否进行某项操作。Realm可以被看作Shiro中的数据源注入器,用户设计/实现的用户权限通过Realm提供给Shiro。

对于应用程序而言,一个简单的Shiro应用提供下面两种能力:

  1. 应用程序可以通过Subject进行用户认证和授权,Subject将操作委托给SecurityManager进行处理;
  2. SecurityManager通过Realm注入的安全数据完成用户认证和授权。

在使用Shiro进行应用程序权限管理时,必须为其提供/指定相应的Realm。Shiro本身提供了一些可直接使用的Realm实现,如果默认的Realm不能满足需求,开发人员也可以定制自己的Realm。

从Shiro的内部来看,其架构如下:

从图中可以看到,Shiro中的Subject可以是跨机器/网络的Subject或进程内的Subject:跨机器/网络的Subject主要用于实现异构应用的认证授权;进程内的Subject主要实现同语言同进程的Web和桌面应用的授权。所有的安全操作请求都会通过Subject委托给SecurityManager进行相应处理。SecurityManager中包含:

Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;

Authrizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;

Realm:可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等等;由用户提供;注意:Shiro 不知道你的用户 / 权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的 Realm;

SessionManager:如果写过 Servlet 就应该知道 Session 的概念,Session 呢需要有人去管理它的生命周期,这个组件就是 SessionManager;而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境、EJB 等环境;所有呢,Shiro 就抽象了一个自己的 Session 来管理主体与应用之间交互的数据;这样的话,比如我们在 Web 环境用,刚开始是一台 Web 服务器;接着又上了台 EJB 服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到 Memcached 服务器);

SessionDAO:DAO 大家都用过,数据访问对象,用于会话的 CRUD,比如我们想把 Session 保存到数据库,那么可以实现自己的 SessionDAO,通过如 JDBC 写到数据库;比如想把 Session 放到 Memcached 中,可以实现自己的 Memcached SessionDAO;另外 SessionDAO 中可以使用 Cache 进行缓存,以提高性能;

CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能

Cryptography:密码模块,Shiro 提高了一些常见的加密组件用于如密码加密 / 解密的。

Shiro的使用

环境准备

本文例子基于maven构建,首先添加依赖

<dependencies>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.9</version>
    </dependency>
    <dependency>
        <groupId>commons-logging</groupId>
        <artifactId>commons-logging</artifactId>
        <version>1.1.3</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.2.2</version>
    </dependency>
</dependencies>

其次需要准备一些用户和权限数据,在Resource目录下创建一个配置文件shiro.ini,内容如下:

# -----------------------------------------------------------------------------
# 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
aihe = aihe, goodguy, client

# -----------------------------------------------------------------------------
# Roles with assigned permissions
# roleName = perm1, perm2, ..., permN
# -----------------------------------------------------------------------------
[roles]
admin = *
client = look:*
goodguy = winnebago:drive:eagle5

用户认证

用户认证即是在应用中证明当前用户是本人,一般需要提供他们的身份id(Principals), 和凭据(Credentials)信息来证明。最常见的身份id 和凭据信息就是用户名和密码。

基于Shiro进行用户认证的基本过程包括三步(如下图):

  1. 用户发送请求进行 Subject 认证(调用 subject.login(token));
  2. SecurityManager 会去 Authenticator(认证器)中查找相应的 Realms(可能不止一个)源;
  3. Realms 可以根据不同类型的 Realm 中去查找用户信息,并进行判断是否认证成功。
shiro认证过程

一个简单的用户认证过程示例代码如下:

public class HelloWorld {

    public static void main(String[] args) {
//        加载配置文件,初始化 SecurityManager 工厂
        Factory<SecurityManager> factory = new IniSecurityManagerFactory
          ("classpath:shiro.ini");
//        获取 SecurityManager 实例
        SecurityManager securityManager = factory.getInstance();
//        把 SecurityManager 绑定到 SecurityUtils 中
        SecurityUtils.setSecurityManager(securityManager);
//        得到当前执行的用户
        Subject currentUser = SecurityUtils.getSubject();
//        创建 token 令牌,用户名/密码
        UsernamePasswordToken token = new UsernamePasswordToken("acey", "123456");
        try {
//            身份验证
            currentUser.login(token);
            System.out.println("登录成功");
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("登录失败");
        }
    }
}

用户授权

授权也叫访问控制,即在应用中控制哪些人可以访问哪些资源。在授权中有几个关键对象:主体(Subject)、资源(Resource)、权限(Permission)、角色(Role)。Subject: 即访问应用的用户,用户只有授权后才能访问相应资源。

Resource: 在应用中,用户可以访问的任何东西,比如Jsp页面,数据、业务方法、打印设备等等,都是资源。

Permission: 权限是安全策略中的原子授权单位,通过权限我们可以表示应用中用户有没有操作某个资源的权力。即权限表示在应用中用户能不能访问某个资源,如访问用户列表页面。通过赋权给用户,即可定义哪个用户允许在某个资源上做什么操作。Shiro支持粗粒度权限(如用户模块的所有权限)和细粒度权限(操作某个用户的权限,即实例级别权限),但并不提供具体实现,具体的赋权定义由开发者设计和实现。

Role: 角色代表操作集合,可以理解为权限的集合,一般情况向我们通过赋予用户角色实现授权。例如: 项目总监、技术总监、CTO、开发工程师等都是角色,不同的角色拥有一组不同的权限。角色分为显式角色和隐式角色。

授权流程

一个简单的授权流程如下:

  1. SecurityManager拦截用户的资源访问请求;
  2. SecurityManager调用授权器Authrizer;
  3. Authorizer根据Subject的身份,去相应的Realm中查找该Subject是否有权限访问该资源。

其简单实现包括一个权限定义、角色定义和授权代码。权限定义如下:

#resources/shiro_permissions.ini

[main] 
authc.loginUrl=/login  //表示用户登录失败跳转到 /login
roles.unauthorrizedUrl=/unauthorrized.jsp //表示用户没有对应的访问角色跳转到/unauthorrized.jsp
perms.unauthorrizedUrl=/unauthorrized.jsp  //表示用户没有对应的访问权限跳转到/unauthorrized.jsp

[users]
acey=123456,role1,role2
jack=123,role1
[roles]
role1=user:select // role1 角色有访问 user:select 的权限
role2=user:add,/delete //role2 角色有访问 user:add 和 /delete 的权限

[urls]
/login=anon  //表示任何用户都可以访问 /login
/index=authc //表示只有身份认证通过的用户才可以访问 /index
/index=roles[role1,role2...] //表示只有用户含有 role1 role2 ... 角色才可以访问 /index
/index=perms["user:create","/update"]  //表示只有用户含有 "user:create" 
                      和"/update"权限才可以访问 /index 
/index?=authc //`?`通配符,表示一个字符,如/index1 /indexa /index- (不能匹配/index) ,
                      将符合这种规则的请求进行`authc`拦截
/index*=authc  `*`通配符,表示零个或一个或多个字符,如/index1213asd /index /index2 ,
                      将符合这种规则的请求进行`authc`拦截
/index/**=authc  `**`表示匹配零个或一个或多个路径,如/index/create /index/create/update/...  ,
                      将符合这种规则的请求进行`authc`拦截
/index*/**authc  可以匹配 /index12/create/update/...

角色定义如下:

#resources/shiro_role.ini
[users]
acey=123456,role1,role2 //表示有一个用户,用户名是acey,密码为123456,有role1和role2角色
jack=123,role1

授权代码如下:

#RoleTest.java
public class RoleTest {

//  使用 checkRole 来检验角色时,若权限不足会返回 false
    @Test
    public void testHasRole() {
        Subject currentUser= ShiroUtil.login("classpath:shiro_role.ini", "acey", "123456");
        // Subject currentUser=ShiroUtil.login("classpath:shiro_role.ini", "jack", "123");
        System.out.println(currentUser.hasRole("role1")?"has role1":"has not role1");
        currentUser.logout();
    }

    //  使用 checkRole 来检验角色时,若权限不足会抛出异常
    @Test
    public void testCheckRole() {
        Subject currentUser=ShiroUtil.login("classpath:shiro_role.ini", "acey", "123456");
        // Subject currentUser=ShiroUtil.login("classpath:shiro_role.ini", "jack", "123");
        currentUser.checkRole("role1");

        currentUser.logout();
    }
}

#Permissionpublic class PermissionTest {

//  使用 checkPermission 来检验权限时,若权限不足会返回 false
    @Test
    public void testIsPermitted() {
        Subject currentUser= ShiroUtil.login("classpath:shiro_permission.ini", "acey", "123456");
        System.out.println(currentUser.isPermitted("user:select")?"has user:select":"hsa not user:select");

        currentUser.logout();
    }

//  使用 checkPermission 来检验权限时,若权限不足会抛出异常
    @Test
    public void testCheckPermitted() {
        Subject currentUser=ShiroUtil.login("classpath:shiro_permission.ini", "acey", "123456");
        // Subject currentUser=ShiroUtil.login("classpath:shiro_permission.ini", "jack", "123");
        currentUser.checkPermission("user:select");
        currentUser.logout();
    }
}est

Shiro和Spring的结合

通过和Spring结合,我们可以很方便的实现一个用于实际项目的用户认证和权限管理系统。

配置过滤器

SpringWeb中在web.xml文件中配置Shiro过滤器

#web.xml
 <!-- shiro过滤器定义 -->
    <filter>  
        <filter-name>shiroFilter</filter-name>  
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>  
    <init-param>  
    <!-- 该值缺省为false,表示生命周期由SpringApplicationContext管理,设置为true则表示由ServletContainer管理 -->  
    <param-name>targetFilterLifecycle</param-name>  
    <param-value>true</param-value>  
    </init-param>  
    </filter>  
    <filter-mapping>  
            <filter-name>shiroFilter</filter-name>  
            <url-pattern>/*</url-pattern>  
    </filter-mapping>
    
    

自定义Realm

一般都是通过数据库定义用户和权限数据,因此需要自行设计和实现Realm

#CustomRealm
public class MyRealm extends AuthorizingRealm{

    @Resource
    private UserService userService;
    
    /**
     * 为当前登录的用户授予角色和权限
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //获取用户名
        String userName=(String)principals.getPrimaryPrincipal();
        SimpleAuthorizationInfo authorizationInfo=new SimpleAuthorizationInfo();
        //进行授权角色
        authorizationInfo.setRoles(userService.getRoles(userName));
        //进行授权权限
        authorizationInfo.setStringPermissions(userService.getPermissions(userName));
        return authorizationInfo;
    }

    /**
     *验证当前登录的用户
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String userName=(String)token.getPrincipal();
        //根据用户名查找用户信息
            User user=userService.getByUserName(userName);
            if(user!=null){
                AuthenticationInfo authcInfo=new SimpleAuthenticationInfo(user.getUserName(),user.getPassword(),getName());
                return authcInfo;
            }else{
                return null;                
            }
    }
}

Spring Context 中的Shiro配置

# Spring-cotext.xml

...
<!-- 自定义Realm -->
    <bean id="myRealm" class="com.acey.realm.MyRealm"/>
    
    <!-- 安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">  
      <property name="realm" ref="myRealm"/>  
    </bean>  
    
    <!-- Shiro过滤器 -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">  
        <!-- Shiro的核心安全接口,这个属性是必须的 -->  
        <property name="securityManager" ref="securityManager"/>
        <!-- 身份认证失败,则跳转到登录页面的配置 -->  
        <property name="loginUrl" value="/index.jsp"/>
        <!-- 权限认证失败,则跳转到指定页面 -->  
        <property name="unauthorizedUrl" value="/unauthor.jsp"/>  
        <!-- Shiro连接约束配置,即过滤链的定义 -->  
        <property name="filterChainDefinitions">  
            <value>  
                 /login=anon
                /admin*=authc
                /student=roles[teacher]
                /teacher=perms["user:create"]
            </value>  
        </property>
    </bean>  
    
    <!-- 保证实现了Shiro内部lifecycle函数的bean执行 -->  
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>  
    
    <!-- 开启Shiro注解 -->
    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"/>  
        <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">  
      <property name="securityManager" ref="securityManager"/>  
    </bean>  
...

其中,ShiroFilter我们可以根据需要改为:

#spring-context.xml
<!-- Shiro过滤器 -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">  
        <!-- Shiro的核心安全接口,这个属性是必须的 -->  
        <property name="securityManager" ref="securityManager"/>
        <!-- 身份认证失败,则跳转到登录页面的配置 -->  
        <property name="loginUrl" value="/index.jsp"/>
        <!-- 权限认证失败,则跳转到指定页面 -->  
        <property name="unauthorizedUrl" value="/unauthor.jsp"/>  
        <property name="ownFilter" class="ownFilter.class">
        <!-- Shiro连接约束配置,即过滤链的定义 -->  
        <property name="filterChainDefinitions">  
            <value>  
                 /login=anon
               /**=ownFilter
            </value>  
        </property>
    </bean>  

本文只是对Shiro实现认证和授权的一个粗浅介绍,Shiro框架具有良好的扩展性,可以在哪些点扩展Shiro框架,以及如何基于Shiro实现我们自定义的的用户认证、权限管理,将在后续博文中给出,敬请期待。