上一篇已经分析了shiro的入口filter是SpringShiroFilter, 那么它的doFilter在哪儿呢?

我们看到它的直接父类AbstractShrioFilter继承了OncePerRequestFilter类,该类是shiro内置的大部分filter的父类(抽像公共部分),在该类中定义了doFilter方法

OncePerRequestFilte

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

由于我们是通过SpringShiroFilter拦截进来的那么会调用AbstractShrioFilter中的doFilterInternal

AbstractShrioFilter

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(new Callable() {
public Object call() throws Exception {
updateSessionLastAccessTime(request, response); //修改session的最后活动时间
executeChain(request, response, chain); //执行过滤链
return null;
}
});
}
//创建subject对象
protected WebSubject createSubject(ServletRequest request, ServletResponse response) {
return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject();
}
//7、执行过滤链
protected void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain)
throws IOException, ServletException {
//获取当前请求对应的过滤链
FilterChain chain = getExecutionChain(request, response, origChain);
chain.doFilter(request, response);
}

WebSubject

//3、
public Builder(SecurityManager securityManager, ServletRequest request, ServletResponse response) {
//每次都创建subject上下文(subjectContext), 并设置securityManager对象
super(securityManager);
//将request和response添加到subject上下文(subjectContext), 即上面创建的对象
setRequest(request);
setResponse(response);
}
//4、创建
public WebSubject buildWebSubject() {
Subject subject = super.buildSubject();
return (WebSubject) subject;
}

Subject


//5、调用DefaultSecurityManager的createSubject方法
public Subject buildSubject() {
  return this.securityManager.createSubject(this.subjectContext);
}

DefaultSecurityManager

// 6
public Subject createSubject(SubjectContext subjectContext) {
//web的subjectContext时,会重新创建一个新的,其他的(ini等),只是copy
SubjectContext context = copy(subjectContext); //验证是否subject上下文中有securityMangary对象,如果没有创建一个
context = ensureSecurityManager(context); //将session放入subjectContext 该session会从cookie或者rul上带的JSESSIONID(默认) 注意:第一次访问项目来到这儿没有session
context = resolveSession(context); //校验用户登陆信息, 并放入context, 如果subject,session,和授权认证AuthenticationInfo中都没有,将会从rememberMeManager(cookie)中获取
   //注意:第一次访问项目来到这儿没有这些信息
context = resolvePrincipals(context); //创建一个WebDelegatingSubject对象
Subject subject = doCreateSubject(context); //保存当前认证的用户信息(一般是用户名)在session里,并标记当前用户已经被认证过 为了下次remember使用
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;
}
//登陆
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
info = authenticate(token);
} catch (AuthenticationException ae) {
onFailedLogin(token, ae, subject);
}
Subject loggedIn = createSubject(token, info, subject);
//登陆成功后 根据配置的"记住我" 保存认证信息
onSuccessfulLogin(token, info, loggedIn);
return loggedIn;
} 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);
}

 DefaultWebSecurityManager

//创建sessionKey
@Override
protected SessionKey getSessionKey(SubjectContext context) {
//从context中获取sessonId和request,response
if (WebUtils.isWeb(context)) {
Serializable sessionId = context.getSessionId();
ServletRequest request = WebUtils.getRequest(context);
ServletResponse response = WebUtils.getResponse(context);
return new WebSessionKey(sessionId, request, response);
} else {
...
}
}
// 第一次调用时 getSession方法最后会调用到这儿来 返回null
protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
Serializable sessionId = getSessionId(sessionKey);
if (sessionId == null) {
return null;
}
Session s = retrieveSessionFromDataSource(sessionId);
if (s == null) {
//session ID was provided, meaning one is expected to be found, but we couldn't find one:
String msg = "Could not find session with ID [" + sessionId + "]";
throw new UnknownSessionException(msg);
}
return s;
}

现在开始执行请求路径对应的过滤器

由于过滤链中的过滤器也是OncePerRequestFilte的子类,继续走OncePerRequestFilte#doFilter方法 然后会调用第一步doFilterInternal方法

  我们自定义的方法一般也是继承了AdviceFilter过滤器

AdviceFilter

public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
throws ServletException, IOException { Exception exception = null;
try {
       //8、执行前置方法
boolean continueChain = preHandle(request, response);
       if (continueChain) {
executeChain(request, response, chain);
} postHandle(request, response);
if (log.isTraceEnabled()) {
log.trace("Successfully invoked postHandle method");
} } catch (Exception e) {
exception = e;
} finally {
cleanup(request, response, exception);
}
}

AccessControlFilter

public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
//9、是登陆的rul或者已经认证过 否则重定向到登陆页面
return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
}

UserFilter

(这里以user过滤器为例,如果没有认证过,直接重定向到登陆url)

该过滤器重写了这两个方法

//10、判断是否认证过
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
  //判断当前请求的路径是否为当前过滤器配置的登陆路径(登陆不需要任何权限,返回true)
if (isLoginRequest(request, response)) {
return true;
} else {
    //判断是否已经认证过
Subject subject = getSubject(request, response);
return subject.getPrincipal() != null;
}
}
//11、返回false
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
  //重定向到登陆rul
saveRequestAndRedirectToLogin(request, response);
return false;
}
//12、
protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
//调用webUtils的方法,将当前请求失败的的信息保存起来(便于下次认证成功后直接重定向到该路径)
saveRequest(request);
//重定向的时候会生成session(shrio的)
redirectToLogin(request, response);
}

WebUtils

//保存
public static void saveRequest(ServletRequest request) {
Subject subject = SecurityUtils.getSubject();
//13、这里会创建一个 StoppingAwareProxiedSession AbstractNativeSessionManager#start是创建simpleSession并调用session监听器
  //会将sessinID存在cookie中和sessionDao中(默认时ehcache缓存,可以自己实现redis等)(在DefaultSessionManager#create(Session session)方法中)
Session session = subject.getSession();
HttpServletRequest httpRequest = toHttp(request);
//将当前目标路径的请求信息保存起来
SavedRequest savedRequest = new SavedRequest(httpRequest);
//存到session中,跳到登陆页面后登陆成功后会重定向到此次失败的路径
session.setAttribute(SAVED_REQUEST_KEY, savedRequest);
}
//重定向到savedRequet保存的路径 如果是直接访问的登陆url,则直接重定向到当前过滤器配置的登陆成功url
//成功后的重定向可是不生成session的
public static void redirectToSavedRequest(ServletRequest request, ServletResponse response, String fallbackUrl)
throws IOException {
String successUrl = null;
boolean contextRelative = true;
//从session中获取,并清空上一次失败保存的信息
SavedRequest savedRequest = WebUtils.getAndClearSavedRequest(request);
//上一次请求失败的保存的对象 而且是get请求(这里一般是直接浏览器输入的url) 如果是post请求过来的(一般是表单),直接返回目标路径
if (savedRequest != null && savedRequest.getMethod().equalsIgnoreCase(AccessControlFilter.GET_METHOD)) {
successUrl = savedRequest.getRequestUrl();
contextRelative = false;
}
  //第一次请求时,successUrl为null, 登陆成功后,有值(上一次失败的url)
if (successUrl == null) {
successUrl = fallbackUrl;
}
  //15、发出重定向
WebUtils.issueRedirect(request, response, successUrl, null, contextRelative);
} public static void issueRedirect(ServletRequest request, ServletResponse response, String url, Map queryParams, boolean contextRelative) throws IOException {
issueRedirect(request, response, url, queryParams, contextRelative, true);
}
// 会把sessionID写在rul上, 下面的内容就不带大家看了
public static void issueRedirect(ServletRequest request, ServletResponse response, String url, Map queryParams, boolean contextRelative, boolean http10Compatible) throws IOException {
RedirectView view = new RedirectView(url, contextRelative, http10Compatible);
view.renderMergedOutputModel(queryParams, toHttp(request), toHttp(response));
}

SavedRequest

// SavedRequest的构造方法
public SavedRequest(HttpServletRequest request) {
  //当前请求的方式(get|post...)
this.method = request.getMethod();
  //当前请求的参数
this.queryString = request.getQueryString();
  //当前请求失败的路径
this.requestURI = request.getRequestURI();
}

RedirectView (拼接重定向的参数请求头、url加sessionID,url编码等操作都由这儿进入)

public final void renderMergedOutputModel(
Map model, HttpServletRequest request, HttpServletResponse response) throws IOException { // Prepare name URL.
StringBuilder targetUrl = new StringBuilder();
if (this.contextRelative && getUrl().startsWith("/")) {
targetUrl.append(request.getContextPath());
}
targetUrl.append(getUrl());
     //拼接请求参数
appendQueryProperties(targetUrl, model, this.encodingScheme);
sendRedirect(request, response, targetUrl.toString(), this.http10Compatible);
}

AbstractNativeSessionManager

//14、创建session时会调用
public Session start(SessionContext context) {
//创建simpleSession
Session session = createSession(context);
//重置session时间
applyGlobalSessionTimeout(session);
  //会将sessionID存到cookie
onStart(session, context);
//调用session的Listner
notifyStart(session);
//Don't expose the EIS-tier Session object to the client-tier:
return createExposedSession(session, context);
}

那么此时一个没有授权的请求就执行完毕,现在就来到了我们的登陆界面

  登陆使用authc过滤器

FormAuthenticationFilter

和上面的过程一样,会判断是否认证,如果没有会执行onAccessDenied方法

// 这里需要该过滤器的 登陆url 和登陆所在的界面的url一样
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);
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;
}

AuthenticationFilter

protected void issueSuccessRedirect(ServletRequest request, ServletResponse response) throws Exception {
//当前过滤器配置的登陆url
WebUtils.redirectToSavedRequest(request, response, getSuccessUrl());
}

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

AbstractRememberMeManager  处理 remeberme

public void onSuccessfulLogin(Subject subject, AuthenticationToken token, AuthenticationInfo info) {
//清空之前的认证信息
forgetIdentity(subject); //如果是rememberMe类型的token
if (isRememberMe(token)) {
//记录
rememberIdentity(subject, token, info);
}
}
public void rememberIdentity(Subject subject, AuthenticationToken token, AuthenticationInfo authcInfo) {
//从认证后的信息中获取
PrincipalCollection principals = getIdentityToRemember(subject, authcInfo);
rememberIdentity(subject, principals);
}
// 加密处理
protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
byte[] bytes = convertPrincipalsToBytes(accountPrincipals);
rememberSerializedIdentity(subject, bytes);
}
//使用CipherService类进行处理
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
byte[] bytes = serialize(principals);
if (getCipherService() != null) {
bytes = encrypt(bytes);
}
return bytes;
}
// 返回加密后的认证信息
protected byte[] encrypt(byte[] serialized) {
byte[] value = serialized;
CipherService cipherService = getCipherService();
if (cipherService != null) {
// getEncryptionCipherKey() 获取的是rememberMe cookie加密和解密的密钥
ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());
value = byteSource.getBytes();
}
return value;
}
//加密
protected byte[] encrypt(byte[] serialized) {
byte[] value = serialized;
CipherService cipherService = getCipherService();
if (cipherService != null) {
ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());
value = byteSource.getBytes();
}
return value;
}

CookieRememberMeManager

protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {
HttpServletRequest request = WebUtils.getHttpRequest(subject);
HttpServletResponse response = WebUtils.getHttpResponse(subject); //base 64 encode it and store as a cookie:
String base64 = Base64.encodeToString(serialized);
//rememberMe的cookie模板 key为自定义的名字 我这儿是rememberMe
Cookie template = getCookie();
Cookie cookie = new SimpleCookie(template);
cookie.setValue(base64);
cookie.saveTo(request, response);
}

下面是remember的配置

<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="rememberMe"/>
<property name="httpOnly" value="true"/>
<property name="maxAge" value="2592000"/><!-- 30天 -->
</bean> <!-- rememberMe管理器 -->
<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
<!-- rememberMe cookie加密和解密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)-->
<property name="cipherKey"
value="#{T(org.apache.shiro.codec.Base64).decode('4AvVhmFLUs0KTA3Kprsdag==')}"/>
<property name="cookie" ref="rememberMeCookie"/>
</bean>

由于篇幅原因,本节详细介绍的是登陆需要验证的请求跳转到登陆界面的源码解析

小结:

  1、当第一次请求失败后,会重定向到当前过滤器的登陆界面,并创建一个session,将sessinID存在cookie,重定向的url,还会存放在sessionDao中(默认是ehcache, 可自定义)

  2、当请求的路径为不用认证(anon等自定义preHandle返回true的路径),也会由servlet容器调用shiroRequest的getSession方法创建一个session,保存位置同1

spring集成shiro登陆流程(上)的更多相关文章

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

    首先声明入门看的张开涛大神的<跟我学shiro> 示例:https://github.com/zhangkaitao/shiro-example 博客:http://jinnianshil ...

  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. Shiro(二):Spring-boot如何集成Shiro(上)

    这篇文章主要介绍了spring-boot是如何集成shiro的authentication流程的. 从shiro-spring-boot-web-starter说起 shiro-spring-boot ...

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

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

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

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

  8. Spring集成Shiro使用小结

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

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

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

随机推荐

  1. Windows平台软件推荐:神器小工具(骨灰级)

    底层工具 "If you know how to use Process Monitor competently, people of both sexes will immediately ...

  2. 2. 网友对app后端写作系列文章的写作建议

    很感谢"app后端"qq群的网友,在发布消息后,就收到了大量网友的反馈 下面的建议会融入到写作当中: 1.还有,对版本升级很感兴趣,我们现在为了兼容旧版本,已经把工程代码搞的乱哄哄 ...

  3. &,|,^的用法

    &,|,~,^的用法 &按位与 |按位或 ~按位非 ^按位异或 举例: int x = 5; int y = 11; System.out.println(x|y); System.o ...

  4. return_fun.go 源码阅读

    ]     } else {         receive.To = ""     }     return receive }

  5. BZOJ_1828_[Usaco2010 Mar]balloc 农场分配_线段树

    BZOJ_1828_[Usaco2010 Mar]balloc 农场分配_线段树 Description Input 第1行:两个用空格隔开的整数:N和M * 第2行到N+1行:第i+1行表示一个整数 ...

  6. 关于react组件之间的通信

    才开始学react刚好到组件通信这一块,就简单的记录下组件间的通信方式:父到子:props.context,子到父:自定义事件.回调,兄弟组件:共父props传递.自定义事件import React, ...

  7. 关于” 记一次logback传输日志到logstash根据自定义设置动态创建ElasticSearch索引” 这篇博客相关的优化采坑记录

    之前写过一篇博客是关于记录日志的简单方式的   主要就是  应用->redis->logstash->elasticsearch 整个流程的配置方法和过程的 虽然我们部分线上应用使用 ...

  8. 视频转字符动画-Python-60行代码

    更新:2018-5-21 注意: 最后一步播放字符动画使用了只支持类 unix 系统的模块 curses, 因此在windows上是播放不了的... 解决方法: 1. 最近好像有一个移植 https: ...

  9. set命令详解

    我们依然是围绕这几个话题展开学习: 1.什么是set命令? 2.为什么要用set命令? 3.怎样使用set命令? 1.什么是set命令? ♦ set命令作用主要是显示系统中已经存在的shell变量,以 ...

  10. CentOS7搭建本地YUM仓库,并定期同步阿里云源

    CentOS7同步阿里云镜像rpm包并自建本地yum仓库 系统环境 # cat /etc/centos-release CentOS Linux release 7.6.1810 (Core) # u ...