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);
}
}
}
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方法
//
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() 中的对象从哪里来?,下一篇见分晓
如果大家对我的分析有疑问或者觉得不对的地方亦或者哪儿有漏的地方,请留言。
spring集成shiro登陆流程(下)的更多相关文章
- spring集成shiro登陆流程(上)
		
上一篇已经分析了shiro的入口filter是SpringShiroFilter, 那么它的doFilter在哪儿呢? 我们看到它的直接父类AbstractShrioFilter继承了OncePerR ...
 - Spring集成shiro做登陆认证
		
一.背景 其实很早的时候,就在项目中有使用到shiro做登陆认证,直到今天才又想起来这茬,自己抽空搭了一个spring+springmvc+mybatis和shiro进行集成的种子项目,当然里面还有很 ...
 - spring集成shiro报错解决(no bean named 'shiroFilter' is defined)
		
引言: 本人在使用spring集成shiro是总是报“no bean named 'shiroFilter' is defined”,网上的所有方式挨个试了一遍,又检查了一遍, 还是没有解决,最后,抱 ...
 - shiro实战系列(十五)之Spring集成Shiro
		
Shiro 的 JavaBean 兼容性使得它非常适合通过 Spring XML 或其他基于 Spring 的配置机制.Shiro 应用程序需要一个具 有单例 SecurityManager 实例的应 ...
 - spring 集成shiro 之 自定义过滤器
		
在web.xml中加入 <!-- 过期时间配置 --> <session-config><session-timeout>3</session-timeout ...
 - Shiro学习总结(10)——Spring集成Shiro
		
1.引入Shiro的Maven依赖 [html] view plain copy <!-- Spring 整合Shiro需要的依赖 --> <dependency> <g ...
 - Shiro(三):Spring-boot如何集成Shiro(下)
		
上一篇文章介绍了shiro在spring-boot中通过filter实现authentication流程(通过设置filterMaps也可以达到authorization的目的):这篇文章主要介绍sp ...
 - Spring集成shiro+nginx  实现访问记录
		
最近公司的网站需要添加用户访问记录功能,由于使用了nginx请求转发直接通过HttpServletRequest无法获取用户真实Ip 关于nginx获取真实IP的资料 https://blog.cs ...
 - Spring集成Shiro使用小结
		
shiro的认证流程 Application Code:应用程序代码,由开发人员负责开发的 Subject:框架提供的接口,代表当前用户对象 SecurityManager:框架提供的接口,代表安全管 ...
 
随机推荐
- Circular view path [home]: would dispatch back to the current handler URL [/home] again. Check your ViewResolver setup!
			
Circular view path [home]: would dispatch back to the current handler URL [/home] again. Check your ...
 - python基础下的mysql学习之操作命令(一)
			
E-R模型简介: 该模型应用于数据库设计中的概念结构设计阶段 E-entry,实体,是指设计的主体.对象等: R-relationship,关系,是指设计的主体之间的联系,关系也是一种数据: 一个实体 ...
 - 查看centos系统位数和强制关闭yum
			
一个小命令查看centos 是什么多少位系统 getconf LONG_BIT 方法二: [root@linuxzgf ~]#uname -m 如果有x86_64就是64位的,没有就是32位的后面是X ...
 - linux 显示当前所在文件位置 以及git 分支所在
			
function git-branch-name { git symbolic-ref HEAD 2>/dev/null | cut -d"/" -f 3}function ...
 - AutoIT 测试GUI工具
			
今天听到同事提到AutoIT,可以用来测试GUI窗口.了解一下这个工具. 以下内容引自: http://www.jb51.net/article/14870.htm (此url非原出处,该博主未注明原 ...
 - 【bzoj2331】[SCOI2011]地板
			
题目链接: TP 题解: 分类讨论好烦啊! 0表示没有插头,1.2表示有插头,1表示接下来可以转弯,2表示接下来不能转弯,只能停在一个地方. 然后分类讨论: 插头状态 到达状态 0 0 2 2 | 1 ...
 - 【bzoj 1414】对称的正方形 单调队列+manacher
			
Description Orez很喜欢搜集一些神秘的数据,并经常把它们排成一个矩阵进行研究.最近,Orez又得到了一些数据,并已经把它们排成了一个n行m列的矩阵.通过观察,Orez发现这些数据蕴涵了一 ...
 - 【数学建模】【APIO2015】Palembang Bridges
			
Description 一条东西走向的穆西河将巴邻旁市一分为二,分割成了区域 A 和区域 B. 每一块区域沿着河岸都建了恰好 1000000001 栋的建筑,每条岸边的建筑都从 0 编号到 10000 ...
 - 【源码解析】Sharding-Jdbc模块分析
			
最新的2.0版本的Sharding-Jdbc版本,由于需要支持动态配置加载,所以最新的模块信息如下: |-sharding-jdbc |-sharding-jdbc-core |-api |-cons ...
 - 二逼平衡树 Tyvj 1730 BZOJ3196 Loj#106
			
树状数组+主席树,模板题,不多说... #include <cstdio> #include <algorithm> #include <cmath> #inclu ...