首先声明入门看的张开涛大神的《跟我学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登陆流程(下)的更多相关文章

  1. spring集成shiro登陆流程(上)

    上一篇已经分析了shiro的入口filter是SpringShiroFilter, 那么它的doFilter在哪儿呢? 我们看到它的直接父类AbstractShrioFilter继承了OncePerR ...

  2. Spring集成shiro做登陆认证

    一.背景 其实很早的时候,就在项目中有使用到shiro做登陆认证,直到今天才又想起来这茬,自己抽空搭了一个spring+springmvc+mybatis和shiro进行集成的种子项目,当然里面还有很 ...

  3. spring集成shiro报错解决(no bean named 'shiroFilter' is defined)

    引言: 本人在使用spring集成shiro是总是报“no bean named 'shiroFilter' is defined”,网上的所有方式挨个试了一遍,又检查了一遍, 还是没有解决,最后,抱 ...

  4. shiro实战系列(十五)之Spring集成Shiro

    Shiro 的 JavaBean 兼容性使得它非常适合通过 Spring XML 或其他基于 Spring 的配置机制.Shiro 应用程序需要一个具 有单例 SecurityManager 实例的应 ...

  5. spring 集成shiro 之 自定义过滤器

    在web.xml中加入 <!-- 过期时间配置 --> <session-config><session-timeout>3</session-timeout ...

  6. Shiro学习总结(10)——Spring集成Shiro

    1.引入Shiro的Maven依赖 [html] view plain copy <!-- Spring 整合Shiro需要的依赖 --> <dependency> <g ...

  7. Shiro(三):Spring-boot如何集成Shiro(下)

    上一篇文章介绍了shiro在spring-boot中通过filter实现authentication流程(通过设置filterMaps也可以达到authorization的目的):这篇文章主要介绍sp ...

  8. Spring集成shiro+nginx 实现访问记录

    最近公司的网站需要添加用户访问记录功能,由于使用了nginx请求转发直接通过HttpServletRequest无法获取用户真实Ip 关于nginx获取真实IP的资料  https://blog.cs ...

  9. Spring集成Shiro使用小结

    shiro的认证流程 Application Code:应用程序代码,由开发人员负责开发的 Subject:框架提供的接口,代表当前用户对象 SecurityManager:框架提供的接口,代表安全管 ...

随机推荐

  1. 玩转JPA(一)---异常:Repeated column in mapping for entity/should be mapped with insert="false" update="fal

    最近用JPA遇到这样一个问题:Repeated column in mapping for entity: com.ketayao.security.entity.main.User column: ...

  2. Python中使用MongoEngine1

    pymongo来操作MongoDB数据库,但是直接把对于数据库的操作代码都写在脚本中,这会让应用的代码耦合性太强,而且不利于代码的优化管理 一般应用都是使用MVC框架来设计的,为了更好地维持MVC结构 ...

  3. TestNG 相对路径与绝对路径getResourceAsStream

    以下内容引自: http://blog.csdn.net/zmx729618/article/details/51144588 (注: 此url并非原出处,该文章也是转自他人.但博主未注明出处) Ja ...

  4. 多线程动态规划算法求解TSP(Traveling Salesman Problem) 并附C语言实现例程

    TSP问题描述: 旅行商问题,即TSP问题(Travelling Salesman Problem)又译为旅行推销员问题.货郎担问题,是数学领域中著名问题之一.假设有一个旅行商人要拜访n个城市,他必须 ...

  5. 利用异或求(整数数组中,有2K+1个数,其中有2k个相同,找出不相同的那个数)

    转自https://blog.csdn.net/renjie_998003/article/details/50738025 java的位运算符中有一个叫异或的运算符,用符号(^)表示,其运算规则是: ...

  6. BZOJ_5301_[Cqoi2018]异或序列&&CF617E_莫队

    Description 已知一个长度为 n 的整数数列 a[1],a[2],…,a[n] ,给定查询参数 l.r ,问在 [l,r] 区间内,有多少连续子 序列满足异或和等于 k . 也就是说,对于所 ...

  7. iOS 社交化分享功能

    iOS 开发过程中可能会遇到需要进行第三方分享的需求,比如向QQ,微信,微博等分享 如下图 我们今天要讲到的方式是使用了一个第三方工具: http://www.sharesdk.cn 一,注册账号 去 ...

  8. 禁用后退键 BackSpace

    <script language="JavaScript">document.onkeydown = check;function check(e) {    var  ...

  9. Mybatis学习笔记之一(环境搭建和入门案例介绍)

    一.Mybatis概述 1.1 Mybatis介绍 MyBatis 本是apache的一个开源项目iBatis, 2010年这个项目由apache software foundation 迁移到了go ...

  10. java~springboot~目录索引

    回到占占推荐博客索引 最近写了不过关于java,spring,微服务的相关文章,今天把它整理一下,方便大家学习与参考. java~springboot~目录索引 Java~关于开发工具和包包 Java ...