• 厉害啊!第一次见到把Shiro运行流程写的这么清楚的,建议收藏起来慢慢看


    前言

    shiro是apache的一个开源框架,是一个权限管理的框架,实现 用户认证、用户授权。
    spring中有spring security (原名Acegi),是一个权限框架,它和spring依赖过于紧密,没有shiro使用简单。
    shiro不依赖于spring,shiro不仅可以实现 web应用的权限管理,还可以实现c/s系统,分布式系统权限管理,shiro属于轻量框架,越来越多企业项目开始使用shiro。

    Shiro运行流程学习笔记

    项目中使用到了shiro,所以对shiro做一些比较深的了解。

    也不知从何了解起,先从shiro的运行流程开始。

    运行流程

    1. 首先调用 Subject.login(token) 进行登录,其会自动委托给 Security Manager,调用之前必须通过 SecurityUtils.setSecurityManager() 设置;
    2. SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份验证;
    3. Authenticator 才是真正的身份验证者,Shiro API 中核心的身份认证入口点,此处可以自定义插入自己的实现;
    4. Authenticator 可能会委托给相应的 AuthenticationStrategy 进行多 Realm 身份验证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm 身份验证;
    5. Authenticator 会把相应的 token 传入 Realm,从 Realm 获取身份验证信息,如果没有返回 / 抛出异常表示身份验证失败了。此处可以配置多个 Realm,将按照相应的顺序及策略进行访问。

    绑定线程

    这里从看项目源码开始。

    看第一步,Subject.login(token)方法。

    UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
    Subject subject = SecurityUtils.getSubject();
    subject.login(token);
    
    

    出现了一个UsernamePasswordToken对象,它在这里会调用它的一个构造函数。

    public UsernamePasswordToken(final String username, final String password, final boolean rememberMe) {
        this(username, password != null ? password.toCharArray() : null, rememberMe, null);
    }
    
    

    据笔者自己了解,这是shiro的一个验证对象,只是用来存储用户名密码,以及一个记住我属性的。

    之后会调用shiro的一个工具类得到一个subject对象。

    public static Subject getSubject() {
        Subject subject = ThreadContext.getSubject();
        if (subject == null) {
            subject = (new Subject.Builder()).buildSubject();
            ThreadContext.bind(subject);
        }
        return subject;
    }
    
    

    通过getSubject方法来得到一个Subject对象。

    这里不得不提到shiro的内置线程类ThreadContext,通过bind方法会将subject对象绑定在线程上。

    public static void bind(Subject subject) {
        if (subject != null) {
            put(SUBJECT_KEY, subject);
        }
    }
    
    
    public static void put(Object key, Object value) {
        if (key == null) {
            throw new IllegalArgumentException("key cannot be null");
        }
    
        if (value == null) {
            remove(key);
            return;
        }
    
        ensureResourcesInitialized();
        resources.get().put(key, value);
    
        if (log.isTraceEnabled()) {
            String msg = "Bound value of type [" + value.getClass().getName() + "] for key [" +
                    key + "] to thread " + "[" + Thread.currentThread().getName() + "]";
            log.trace(msg);
        }
    }
    
    

    shirokey都是遵循一个固定的格式。

    public static final String SUBJECT_KEY = ThreadContext.class.getName() + "_SUBJECT_KEY";
    
    

    经过非空判断后会将值以KV的形式put进去。

    当你想拿到subject对象时,也可以通过getSubject方法得到subject对象。

    在绑定subject对象时,也会将securityManager对象进行一个绑定。

    而绑定securityManager对象的地方是在Subject类的一个静态内部类里(可让我好一顿找)。

    getSubject方法中的一句代码调用了内部类的buildSubject方法。

    subject = (new Subject.Builder()).buildSubject();
    
    

    PS:此处运用到了建造者设计模式,可以去菜鸟教程仔细了解

    进去观看源码后可以看见。

    首先调用无参构造,在无参构造里调用有参构造函数。

    public Builder() {
        this(SecurityUtils.getSecurityManager());
    }
    
    public Builder(SecurityManager securityManager) {
        if (securityManager == null) {
            throw new NullPointerException("SecurityManager method argument cannot be null.");
        }
        this.securityManager = securityManager;
        this.subjectContext = newSubjectContextInstance();
        if (this.subjectContext == null) {
            throw new IllegalStateException("Subject instance returned from 'newSubjectContextInstance' " +
                    "cannot be null.");
        }
        this.subjectContext.setSecurityManager(securityManager);
    }
    
    

    在此处绑定了securityManager对象。

    当然,他也对securityManager对象的空状况进行了处理,在getSecurityManager方法里。

    public static SecurityManager getSecurityManager() throws UnavailableSecurityManagerException {
        SecurityManager securityManager = ThreadContext.getSecurityManager();
        if (securityManager == null) {
            securityManager = SecurityUtils.securityManager;
        }
        if (securityManager == null) {
            String msg = "No SecurityManager accessible to the calling code, either bound to the " +
                    ThreadContext.class.getName() + " or as a vm static singleton.  This is an invalid application " +
                    "configuration.";
            throw new UnavailableSecurityManagerException(msg);
        }
        return securityManager;
    }
    
    

    真正的核心就在于securityManager这个对象。

    SecurityManager

    SecurityManager是一个接口,他继承了步骤里所谈到的AuthenticatorAuthorizer类以及用于Session管理的SessionManager

    public interface SecurityManager extends Authenticator, Authorizer, SessionManager {
    
    	Subject login(Subject subject, AuthenticationToken authenticationToken) throws AuthenticationException;   
    
        void logout(Subject subject);
    
        Subject createSubject(SubjectContext context);
    }
    
    

    看一下它的实现。

    且这些类和接口都有依次继承的关系。

    Relam

    接下来了解一下另一个重要的概念Relam

    Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当与像用户帐户这类安全相关数据进行交互,执行认证(登录)和授权(访问控制)时,Shiro会从应用配置的Realm中查找很多内容。

    从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个。

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

    一般情况下,都会自定义Relam来使用。

    先看一下实现。

    以及自定义的一个UserRelam

    看一下类图。

    每个抽象类继承后所需要实现的方法都不一样。

    public class UserRealm extends AuthorizingRealm
    
    

    这里继承AuthorizingRealm,需要实现它的两个方法。

    //给登录用户授权
    protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals);
    
    //这个抽象方法属于AuthorizingRealm抽象类的父类AuthenticatingRealm类   登录认证,也是登录的DAO操作所在的方法
    protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;
    
    

    之后再来看看这个验证方法,在之前的步骤里提到了,验证用到了Authenticator ,也就是第五步。

    Authenticator

    Authenticator 会把相应的 token 传入 Realm,从 Realm 获取身份验证信息,如果没有返回 / 抛出异常表示身份验证失败了。此处可以配置多个 Realm,将按照相应的顺序及策略进行访问。

    再回到之前登录方法上来看看。

    subject.login(token)在第一步中调用了Subjectlogin方法,找到它的最终实现DelegatingSubject类。

    里面有调用了securityManagerlogin方法,而最终实现就在DefaultSecurityManager这个类里。

    Subject subject = securityManager.login(this, token);
    
    
    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
        }
    
    

    之后就是验证流程,这里我们会看到第四步,点进去会到抽象类AuthenticatingSecurityManager。再看看它的仔细调用。

    public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
        return this.authenticator.authenticate(token);
    }
    
    

    真正的调用Relam进行验证并不在这,而是在ModularRealmAuthenticator

    他们之间是一个从左到右的过程。

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

    在这里咱们就看这个doSingleRealmAuthentication方法。

    Relam验证。

    protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
        if (!realm.supports(token)) {
            String msg = "Realm [" + realm + "] does not support authentication token [" +
                    token + "].  Please ensure that the appropriate Realm implementation is " +
                    "configured correctly or that the realm accepts AuthenticationTokens of this type.";
            throw new UnsupportedTokenException(msg);
        }
        //在此处调用你自定义的Relam的方法来验证。
        AuthenticationInfo info = realm.getAuthenticationInfo(token);
        if (info == null) {
            String msg = "Realm [" + realm + "] was unable to find account data for the " +
                    "submitted AuthenticationToken [" + token + "].";
            throw new UnknownAccountException(msg);
        }
        return info;
    }
    
    

    再看看多Relam的。

    protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
    
        AuthenticationStrategy strategy = getAuthenticationStrategy();
    
        AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
    
        for (Realm realm : realms) {
    
            aggregate = strategy.beforeAttempt(realm, token, aggregate);
    
            if (realm.supports(token)) {
    
                AuthenticationInfo info = null;
                Throwable t = null;
                try {
                    //调用自定义的Relam的方法来验证。
                    info = realm.getAuthenticationInfo(token);
                } catch (Throwable throwable) {
                    t = throwable;
                }
    
                aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
    
            } else {
                log.debug("Realm [{}] does not support token {}.  Skipping realm.", realm, token);
            }
        }
    
        aggregate = strategy.afterAllAttempts(token, aggregate);
    
        return aggregate;
    }
    
    

    会发现调用的都是RelamgetAuthenticationInfo方法。

    看到了熟悉的UserRelam,此致,闭环了。

    但是也只是了解了大概的流程,对每个类的具体作用并不是很了解,所以笔者还是有很多地方要去学习,不,应该说我本来就是菜鸡,就要学才能变带佬。

    最后

    大家看完有什么不懂的可以在下方留言讨论,也可以关注我私信问我,我看到后都会回答的。也欢迎大家关注我的公众号:前程有光,马上金九银十跳槽面试季,整理了1000多道将近500多页pdf文档的Java面试题资料放在里面,助你圆梦BAT!文章都会在里面更新,整理的资料也会放在里面。谢谢你的观看,觉得文章对你有帮助的话记得关注我点个赞支持一下!

  • 相关阅读:
    unzip解压3G或者4G以上文件失败的解决方法
    zencart批量删除无图片产品
    zencart后台管理中选项名称和选项内容和属性控制页面出错解决办法 WARNING: An Error occurred, please refresh the page and try again
    在线随机密码生成工具
    zencart更改css按钮的宽度css buttons
    IE8"HTML Parsing Error:Unable to modify the parent container element before the child element is closed"错误
    css改变背景透明度
    phpMyAdmin出现Fatal error: Maximum execution time of 300 seconds
    da面板修改SSH端口号
    原生js三级联动
  • 原文地址:https://www.cnblogs.com/lwh1019/p/13435746.html
Copyright © 2020-2023  润新知