Shiro

一、权限框架介绍

1.1 什么是权限管理

权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。

权限管理包括用户身份认证和授权两部分,简称认证授权。对于需要访问控制的资源用户首先经过身份认证,认证通过后用户具有该资源的访问权限方可访问。

  1. 用户身份认证

    身份认证,就是判断一个用户是否为合法用户的处理过程。

    最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确。对于采用指纹等系统,则出示指纹;对于硬件 Key 等刷卡系统,则需要刷卡。

    用户名密码身份认证流程:

  2. 授权流程

    授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的。

1.2 常见权限框架

  1. Shiro

    Apache Shiro 是 Java 的一个安全框架。目前,使用 Apache Shiro 的人越来越多,因为它相当简单,对比 Spring Security,可能没有 Spring Security 做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的 Shiro 就足够了。对于它俩到底哪个好,这个不必纠结,能更简单的解决项目问题就好了。

  2. Spring Security

    Spring Security 是一个能够为基于 Spring 的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在 Spring 应用上下文中配置的 Bean,充分利用了 Spring IoC(控制反转),DI(依赖注入)和 AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

    它是一个轻量级的安全框架,它确保基于 Spring 的应用程序提供身份验证和授权支持。它与 Spring MVC 有很好地集成,并配备了流行的安全算法实现捆绑在一起。

    安全主要包括两个操作“认证”与“验证”(有时候也会叫做权限控制)。“认证”是为用户建立一个其声明的角色的过程,这个角色可以一个用户、一个设备或者一个系统。“验证”指的是一个用户在你的应用中能够执行某个操作。在到达授权判断之前,角色已经在身份认证过程中建立了。

  3. Shiro 和 Spring Security 比较

    • Shiro 比 Spring 更容易使用,实现和最重要的理解
    • Spring Security 更加知名的唯一原因是因为品牌名称
    • “Spring”以简单而闻名,但很多人发现安装 Spring Security 很难
    • Spring Security 却有更好的社区支持
    • Apache Shiro 在 Spring Security 处理密码学方面有一个额外的模块
    • Spring-security 对 spring 结合较好,如果项目用的 springmvc,使用起来很方便。但是如果项目中没有用到 spring,那就不要考虑它了。
    • Shiro 功能强大、且 简单、灵活。是 Apache 下的项目比较可靠,且不跟任何的框架或者容器绑定,可以独立运行。

二、shiro

2.1 shiro 简介

组件与方法 说明
Authentication 身份认证/登录,验证用户是不是拥有相应的身份
Authorization 授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限
Session Manager 会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的
Cryptography 加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储
Web Support Web 支持,可以非常容易的集成到 Web 环境
Caching 缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率
Concurrency shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去
Testing 提供测试支持
Run As 允许一个用户假装为另一个用户(如果他们允许)的身份进行访问
Remember Me 这个是非常常见的功能,即一次登录后,下次再来的话不用登录记住一点,Shiro 不会去维护用户、维护权限;这些需要我们自己去设计/提供;然后通过相应的接口注入给 Shiro 即可

2.2 shiro 实现原理

最简单的一个 Shiro 应用:

应用代码通过 Subject 来进行认证和授权,而 Subject 又委托给 SecurityManager;我们需要给 Shiro 的 SecurityManager 注入 Realm,从而让 SecurityManager 能得到合法的用户及其权限进行判断。

2.3 shiro 的架构理解

名词 说明
Subject 主体。可以是任何可以与应用交互的“用户”
SecurityManager 相当于 SpringMVC 中的 DispatcherServlet 或者 Struts2 中的FilterDispatcher;是 Shiro 的心脏;所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。
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 数据访问对象,用于会话的 CRUD,比如我们想把 Session 保存到数据库,那么可以实现自己的 SessionDAO,通过如 JDBC 写到数据库;比如想把 Session 放到 Memcached 中,可以实现自己的 Memcached SessionDAO;另外 SessionDAO中可以使用 Cache 进行缓存,以提高性能
CacheManager 缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能
Cryptography 密码模块,Shiro 提高了一些常见的加密组件用于如密码加密

2.4 shiro 认证功能流程

流程图:

@Test
public void testHelloworld() {
    // 1、获取 SecurityManager 工厂,此处使用 Ini 配置文件初始化 SecurityManager
    Factory<org.apache.shiro.mgt.SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
    // 2、得到 SecurityManager 实例并绑定给 SecurityUtils
    org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance();
    SecurityUtils.setSecurityManager(securityManager);
    // 3、得到 Subject 及创建用户名/密码身份验证 Token(即用户身份/凭证)
    Subject subject = SecurityUtils.getSubject();
    // UsernamePasswordToken 继承
    UsernamePasswordToken token = new UsernamePasswordToken("zhang", "123");
    try {
        // 4、登录,即身份验证
        subject.login(token);
    } catch (AuthenticationException e) {
        // 5、身份验证失败
    }
    Assert.assertEquals(true, subject.isAuthenticated()); // 断言用户已经登录
    // 6、退出
    subject.logout();
}
  1. UsernamePasswordToken 实现 HostAuthenticationToken 和RemeberAuthenticationToken,HostAuthenticationToken 实现AuthenticationToken
  2. 首先调用 Subject.login(token) 进行登录,其会自动委托给 Security Manager,调用之前必须通过 SecurityUtils.setSecurityManager() 设置 manager;
  3. SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份验证;SecurityManager 接口继承 Authenticator、Authrizer、sessionManage接口
  4. Authenticator 才是真正的身份验证者,Shiro API 中核心的身份认证入口点,此处可以自定义插入自己的实现;
  5. Authenticator 可能会委托给相应的 AuthenticationStrategy 进行多 Realm 身份验证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm 身份验证;
  6. Authenticator 会把相应的 token 传入 Realm,从 Realm 获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了。此处可以配置多个 Realm,将按照相应的顺序及策略进行访问。

2.5 Realm

一般继承 AuthorizingRealm(授权)即可;其继承了 AuthenticatingRealm(即身份验证),而且也间接继承了 CachingRealm(带有缓存实现),重写里面AuthenticationInfo 认证和 doGetAuthorizationInfo 授权方法。

2.6 shiro 授权实现的方式以及流程

  • 基于角色的访问控制(隐式角色)
  • 基于资源的访问控制(显示角色)
// 1、编程式(单个和多个角色)
subject.hasRole(“admin”), Array.aslist("admin1","admin2");

// 2、注解
@RequiresRoles("admin")

// 3、页面控制
<shiro:hasRole name="admin">

// 4、shiro配置文件中配置
 <property name="filterChainDefinitions">
            <value>
                /commons/** = anon
                /plugins/** = anon
                /assets/** = anon
                /css/** = anon
                /js/** = anon
                /img/** = anon
                /fonts/** = anon
                /bootstrap/** = anon
                /login = anon
                /interface/** = anon
                /** = user
                
            </value>
</property>

shiro 拦截器规则:



实现流程:

三、login

登录操作:

Subject subject = SecurityUtils.getSubject();
AuthenticationToken authenticationToken = new ...
subject.login(authenticationToken);

Subject 是 DelegatingSubject 类的实例。

public void login(AuthenticationToken token) throws AuthenticationException {
    clearRunAsIdentitiesInternal();
    Subject subject = securityManager.login(this, token);

    PrincipalCollection principals;

    String host = null;

    if (subject instanceof DelegatingSubject) {
        DelegatingSubject delegating = (DelegatingSubject) subject;
        //we have to do this in case there are assumed identities - we don't want to lose the 'real' principals:
        principals = delegating.principals;
        host = delegating.host;
    } else {
        principals = subject.getPrincipals();
    }

    if (principals == null || principals.isEmpty()) {
        String msg = "Principals returned from securityManager.login( token ) returned a null or " +
                "empty value.  This value must be non null and populated with one or more elements.";
        throw new IllegalStateException(msg);
    }
    this.principals = principals;
    this.authenticated = true;
    if (token instanceof HostAuthenticationToken) {
        host = ((HostAuthenticationToken) token).getHost();
    }
    if (host != null) {
        this.host = host;
    }
    Session session = subject.getSession(false);
    if (session != null) {
        this.session = decorate(session);
    } else {
        this.session = null;
    }
}

登录将委托给 SecurityManager 的 DefaultSecurityManager 类的实例,DefaultSecurityManager 类的 login 方法实际上是产生了一个新的 Subject,然后将相关属性赋予当前调用者 Subject:

public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
    AuthenticationInfo info;
    try {
        info = authenticate(token);
    } catch (AuthenticationException ae) {
        try {
            onFailedLogin(token, ae, subject);
        } catch (Exception e) {
            if (log.isInfoEnabled()) {
                log.info("onFailedLogin method threw an " +
                        "exception.  Logging and propagating original AuthenticationException.", e);
            }
        }
        throw ae; //propagate
    }

    Subject loggedIn = createSubject(token, info, subject);

    onSuccessfulLogin(token, info, loggedIn);

    return loggedIn;
}

继承链:

DefaultSecurityManager 的父类 AuthenticatingSecurityManager 实现了 authenticate 方法:

/**
 * Delegates to the wrapped {@link org.apache.shiro.authc.Authenticator Authenticator} for authentication.
 */
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
    return this.authenticator.authenticate(token);
}

利用一个 ModularRealmAuthenticator 类型的 authenticator 来实现:

protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
    assertRealmsConfigured();
    Collection<Realm> realms = getRealms();
    if (realms.size() == 1) {
        return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
    } else {
        return doMultiRealmAuthentication(realms, authenticationToken);
    }
}

然后根据 realms 集合是单个还是多个分别处理,最终都是:

AuthenticationInfo info = realm.getAuthenticationInfo(token);

父类 AuthenticatingRealmgetAuthenticationInfo 方法实现了 info 的获取和身份的校验,仅仅调用自己实现的 realm 的 doGetAuthenticationInfo 方法,初步验证后构造一个SimpleAuthenticationInfo

SimplePrincipalCollection principalCollection = new SimplePrincipalCollection(principals, this.getName());
return new SimpleAuthenticationInfo(principalCollection, authenticationInfo.getCredentials());

AuthenticatingRealm 的 getAuthenticationInfo 方法逻辑如下:

  1. 首先去缓存找 info

    AuthenticationInfo info = getCachedAuthenticationInfo(token);
    
  2. 缓存没有则调用子类实现的方法:

    info = doGetAuthenticationInfo(token);
    
  3. info 不为 null 时就要验证了(这里还可以加密验证):

    assertCredentialsMatch(token, info);
    

流程总结:

  1. Controller 中使用 subject.login(token) 来执行登陆操作;
  2. DelegatingSubject.login 中使用 securityManager.login(this.token)
  3. DefaultSecrityManager.login(自己注入的是这个secrityManager)中使用 authenticate(token)
  4. AuthenticatingSecurityManager.authenticate 中调用 authenticator.authenticate(token)
  5. AbstractAuthenticator.authenticate 中调用 doAuthenticate(token)
  6. ModularRealmAuthenticator.doAuthenticate 中通过 getRealms 来得到所有的 Realm,然后使用(假设这里只定义了一个 realm)doSingleRealmAuthentication(realm, authenticationtoekn)
  7. ModularReamlmAuthenticator.doSingleRealmAuthentication 中调用了 Realm.getAuthenticationInfo(token)
  8. AUthenticatingRealm 中使用 doGetAuthenticatingInfor(token),这个方法是自定义的 Realm 中重载的方法,而后使用 assertCredentialsMatch(token,info)
  9. 自定义或者默认的 CredentialMatcher 的 doCredentialsMatch 方法对 info 中的 Credential 和 token 中的 crediential 进行比对

四、内容来源

pengjwhx - shiro框架详解
官网
百度百科
故事爱人 - 权限框架之Shiro详解(非原创)
Lucare - Shiro的认证原理(Subject#login的背后故事)

posted @ 2020-03-23 16:45  和风细羽  阅读(250)  评论(0编辑  收藏  举报