• spring集成shiro登陆流程(下)


    首先声明入门看的张开涛大神的《跟我学shiro》

    示例:https://github.com/zhangkaitao/shiro-example

    博客:http://jinnianshilongnian.iteye.com  (今年是龙年)

    现在我们接着上一篇来说, 话说我们现在已经被上一次没有认证过的请求到了登陆界面

    这里先给初authc的过滤器(FormAuthenticationFilter)对应的继承关系

    被springShiroFilter拦截后来到OncePerRequestFilter的doFilter方法

    OncePerRequestFilter

    public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
                throws ServletException, IOException {
        //当前过滤器的名字
            String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
         //如果该过滤器执行过,那么将不执行同一个名字的过滤器 直接执行过滤链中的下一个过滤器
            if ( request.getAttribute(alreadyFilteredAttributeName) != null ) {
                filterChain.doFilter(request, response);
    
            } else if (!isEnabled(request, response) || shouldNotFilter(request) ) {
            //如果当前过滤器设置了enabled属性为false,则不执行,直接执行过滤链中的下一个过滤器
                filterChain.doFilter(request, response);
            } else {
                //标志当前过滤器已经执行过
                request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
                try {
              //1、 核心方法
                    doFilterInternal(request, response, filterChain);
                } finally {
                    //过滤链执行完毕后,清空request中的过滤链执行记录
                    request.removeAttribute(alreadyFilteredAttributeName);
                }
            }
        }
    View Code

    AbstractShiroFilter

    protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
            throws ServletException, IOException {
                
         //封装容器的request和response为shiro自己的 其中在request中标识了当前不为servlet容器的session (在创建session时会用到servlet容器调用getSession()时 )
        final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
        final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
        //2、 创建subject(可以看出每次请求都会创建一个Subject对象)
        final Subject subject = createSubject(request, response);
    
        // 执行过滤链  注意 这里是subject调用的execute方法
        subject.execute(new Callable() {
            public Object call() throws Exception {
                updateSessionLastAccessTime(request, response);  //修改session的最后活动时间
                executeChain(request, response, chain);  //执行过滤链
                return null;
            }
        });
    }

    然后进入DefaultSecurityManager的createSubject方法

    // 6
    public Subject createSubject(SubjectContext subjectContext) {
        //web的subjectContext时,会重新创建一个新的,其他的(ini等),只是copy
        SubjectContext context = copy(subjectContext);
    
        //验证是否subject上下文中有securityMangary对象,如果没有创建一个
        context = ensureSecurityManager(context);
        
        //当前已经有session了,是第一次重定向生成的,先从cookie中拿cookieID,如果没有就从url中拿,再到sessionDao中根据sessionID获取session
        context = resolveSession(context);
    
        //登陆之前这儿没有认证信息
        context = resolvePrincipals(context);
        
        //创建一个WebDelegatingSubject对象
        Subject subject = doCreateSubject(context);
    
        //将认证信息和认证状态保存到session,认证前没有
        save(subject);
    
        return subject;
    }
    
    // 从context中获取session
    protected SubjectContext resolveSession(SubjectContext context) {
        Session session = resolveContextSession(context);
        if (session != null) {
            context.setSession(session);
        }
        return context;
    }
    protected Session resolveContextSession(SubjectContext context) throws InvalidSessionException {
        //调用的下面子类DefaultWebSecurityManager的方法
        SessionKey key = getSessionKey(context);
        if (key != null) {
            //调用 SessionsSecurityManager#getSession
            return getSession(key);
        }
        return null;
    }

    当执行完AbstractShiroFilter的doFilterInternal后(springShiroFilter过滤器走完),会调用过滤链,继续会执行到OncePerRequestFilter的doFilter方法

    由于我们是登陆功能,会调用AdviceFilter的doFilterInternal方法(一般我们的自定义的过滤器都继承了AdviceFilter)

    public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
                throws ServletException, IOException {
            Exception exception = null;
            try {
           //前置方法
    boolean continueChain = preHandle(request, response);if (continueChain) { executeChain(request, response, chain); }
           //后置方法 postHandle(request, response);
    } catch (Exception e) { exception = e; } finally {
            //完成后执行 cleanup(request, response, exception); } }

     然后是流水账 PathMatchingFilter#preHandle->AccessControlFilter#onPreHandle->AuthenticatingFilter#isAccessAllowed->AuthenticationFilter#isAccessAllowed

       1、在PathMatchingFilter#preHandle中校验当前是否被对应的拦截规则匹配到

       2、在AccessControlFilter#onPreHandle定义isAccessAllowed和onAccessDenied方法供子类实现, 前者是判断是否已登陆或者是否有权限,后者是没有前者条件之后的处理

       FormAuthenticationFilter过滤器是调用登陆操作

      3、判断是否已登陆或者是否有权限,如果没有执行onAccessDenied方法(AccessControlFilter#onPreHandle中定义)

     由于我们是第一次登陆操作,那么将会执行FormAuthenticationFilter#onAccessDenied

    FormAuthenticationFilter

    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        //条件: 配置的该过滤器的登陆路径和请求路径相同
        if (isLoginRequest(request, response)) {
            //1、HttpServletRequest  2、post请求
            if (isLoginSubmission(request, response)) {
           //执行登陆
    return executeLogin(request, response); } else { //登陆页面的url 请求方式为get return true; } } else { //如果一个请求路径配置的authc过滤器,然后没有登陆直接调用,会走到这里 //重定向到登陆页面 会创建一个StoppingAwareProxiedSession类型的session 并把sessionId放在登陆页面的url上 saveRequestAndRedirectToLogin(request, response); return false; } } protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { //调用 new UsernamePasswordToken(username, password, rememberMe, host); AuthenticationToken token = createToken(request, response); try { Subject subject = getSubject(request, response); subject.login(token);
          //成功后重定向到上次请求未认证失败后的url(当然,如果你是直接get请求访问的登陆界面,也就是没有重定向过,那么会直接重定向到登陆后的目标页面)
    return onLoginSuccess(token, subject, request, response); } catch (AuthenticationException e) { return onLoginFailure(token, e, request, response); } } //登陆成功后 重定向到上一次重定向过来的路径或者当前过滤器的登陆路径 //可以重写该方法登陆后直接跳到当前过滤器配置的url 而不是上一次失败的url protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception { //调用父类AuthenticationFilter的issueSuccessRedirect方法 issueSuccessRedirect(request, response); //重定向后,阻止过滤连调用 return false; }

     让我们瞧瞧subject的login

    DelegatingSubject

    public void login(AuthenticationToken token) throws AuthenticationException {
        clearRunAsIdentitiesInternal();
        //在这儿委托给securiManager登陆
        Subject subject = securityManager.login(this, token);
        PrincipalCollection principals;
        String host = null;
        if (subject instanceof DelegatingSubject) {
            DelegatingSubject delegating = (DelegatingSubject) subject;
            //认证信息
            principals = delegating.principals;
            host = delegating.host;
        } else {
            principals = subject.getPrincipals();
        }
        this.principals = principals;
        //标记已经登陆过
        this.authenticated = true;
        if (token instanceof HostAuthenticationToken) {
            host = ((HostAuthenticationToken) token).getHost();
        }
        if (host != null) {
            this.host = host;
        }
        //获取登陆时的session
        Session session = subject.getSession(false);
        if (session != null) {
            //执行new StoppingAwareProxiedSession(session, this);  登陆后的session封装成StoppingAwareProxiedSession代理对象
            this.session = decorate(session);
        } else {
            this.session = null;
        }
    }

    又来到DefaultSecurityManager

    登陆的认证使用的是认证器对象进行认证,默认是ModularRealmAuthenticator类(AuthenticatingSecurityManager的构造方法中创建)

    //登陆
    public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
        AuthenticationInfo info;
        try {
         //这里会调用ModularRealmAuthenticator的doAuthenticate认证方法 info
    = authenticate(token); } catch (AuthenticationException ae) { onFailedLogin(token, ae, subject); }
    //执行完认证之后看这儿,又创建了一个新的subject Subject loggedIn
    = createSubject(token, info, subject); //登陆成功后 根据配置的"记住我" 保存认证信息 onSuccessfulLogin(token, info, loggedIn); return loggedIn; }

    ModularRealmAuthenticator

    //认证的时候注意别忘了配置AuthenticatingRealm类型的realm,否则会报错
    protected
    AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
          //校验是否有AuthenticatingRealm类型的realm assertRealmsConfigured(); Collection
    <Realm> realms = getRealms(); if (realms.size() == 1) { return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken); } else { return doMultiRealmAuthentication(realms, authenticationToken); } }

     当配置了多个realm时,会调用认证策略来判断是否认证成功, 默认的认证策略是AtLeastOneSuccessfulStrategy(ModularRealmAuthenticator的构造器中创建)

      即有一个认证成功就算成功!

    下面来到AuthenticatingRealm的getAuthenticationInfo方法

    AuthenticatingRealm

    public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
         //先从缓存中获取认证信息(如果配置了)
            AuthenticationInfo info = getCachedAuthenticationInfo(token);
            if (info == null) {
                //如果没有缓存,执行查询(执行我们自定义的Realm)
                info = doGetAuthenticationInfo(token);if (token != null && info != null) {
              //认证完后,缓存认证信息(默认认证的缓存是关闭的) cacheAuthenticationInfoIfPossible(token, info); } }
         //这里如果配置了凭证的匹配功能,则进行密码匹配操作 if (info != null) { assertCredentialsMatch(token, info); } return info; }

    那么就认证完成了,调用了自定义的Realm的doGetAuthenticationInfo方法,继续看到上面的DefaultSecurityManager#login方法

    DefaultSecurityManager

    其中有这么段代码

      ...  
      //
    执行完认证之后看这儿,又创建了一个新的subject Subject loggedIn = createSubject(token, info, subject); //登陆成功后 根据配置的"记住我" 保存认证信息 onSuccessfulLogin(token, info, loggedIn);
    }


    protected Subject createSubject(AuthenticationToken token, AuthenticationInfo info, Subject existing) {
      //创建新的subject上下文
    SubjectContext context = createSubjectContext();
      //设置认证状态为true
    context.setAuthenticated(true);
      //设置realm中返回的token
    context.setAuthenticationToken(token);
      //设置realm中返回的认证信息
    context.setAuthenticationInfo(info);
    if (existing != null) {
         //将当前subject保存到MapContext
    context.setSubject(existing);
    }
    return createSubject(context);
    }
    //又调用了一次
    public Subject createSubject(SubjectContext subjectContext) {
        //web的subjectContext时,会重新创建一个新的,其他的(ini等),只是copy
        SubjectContext context = copy(subjectContext);
    
        //验证是否subject上下文中有securityMangary对象,如果没有创建一个
        context = ensureSecurityManager(context);
        
        //将session放到cotext
        context = resolveSession(context);
    
        //将认证信息保存到context
        context = resolvePrincipals(context);
        
        //创建一个WebDelegatingSubject对象
        Subject subject = doCreateSubject(context);
    
        //将认证信息和认证状态保存到session,认证前没有
        save(subject);
      //到这儿,subject中包含了
    principals, authenticated, host, session, sessionEnabled,request, response, securityManager
      return subject;
    }
    //认证成功后,将认证信息保存到cookie中(base64加密后的)
    protected void onSuccessfulLogin(AuthenticationToken token, AuthenticationInfo info, Subject subject) {
        rememberMeSuccessfulLogin(token, info, subject);
    }
    protected void rememberMeSuccessfulLogin(AuthenticationToken token, AuthenticationInfo info, Subject subject) {
        //获取 rememberMeManager管理器
        RememberMeManager rmm = getRememberMeManager();
        rmm.onSuccessfulLogin(subject, token, info);
    }
     

    那么DefaultSecurityManager中的login就走完了, 继续回到Subject的login方法, 这时会将很多认证之后的信息放到subject中(认证完成之前创建的那个,AbstractShiroFilter#doFilterInternal)

    登陆成功后会重定向到上次请求未认证失败后的url(当然,如果你是直接get请求访问的登陆界面,也就是没有重定向过,那么会直接重定向到登陆后的目标页面)

    小结:

      1、登陆时会先执行AbstractShiroFilter的doFilterInternal准备一些参数

        如果你不借助web,你会发现你会先调用SecurityUtils.getSubject(),设置SecurityManager等方法,然后使用这个subject做操作, 这个过程在第一次拦截时已经给你做了

      2、一次请求调用了两次DefaultSecurityManager#createSubject方法,第一次时做准备操作,第二次是填满这个subject对象

      3、登陆时已经有session了(我只发现了shiro框架默认的两个创建session的地方)

    当然我们在这儿应该引出一个问题:

      问题就是我们常常调用的SecurityUtils.getSubject() 和SecurityUtils.getSecurityManager() 中的对象从哪里来?,下一篇见分晓

    如果大家对我的分析有疑问或者觉得不对的地方亦或者哪儿有漏的地方,请留言。

  • 相关阅读:
    Shell 同时读取多个文件
    Shell 进度条效果的一个实现
    在 CentOS7 上安装 Tomcat9
    Redis原子计数器incr,防止并发请求
    .Net高并发解决思路
    Redis windows服务器配置可远程连接
    Lambda表达式和For循环使用需要注意的一个地方
    C#多线程和线程池
    MongoDb 物理位置应用实现
    Android Studio3.0 配置AndroidAnnotation注解框架
  • 原文地址:https://www.cnblogs.com/qiaozhuangshi/p/10779561.html
Copyright © 2020-2023  润新知