上一篇已经分析了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. 第三章——分类(Classification)

    3.1 MNIST 本章介绍分类,使用MNIST数据集.该数据集包含七万个手写数字图片.使用Scikit-Learn函数即可下载该数据集: >>> from sklearn.data ...

  2. Spring Webflux: Kotlin DSL [片断]

    原文链接:https://dzone.com/articles/spring-webflux-kotlin-dsl-snippets 作者:Biju Kunjummen 译者:Jackie Tang ...

  3. Using variables inside Postman and Collection Runner

    Variables are among the most powerful features in Postman. Using variables in your Postman requests, ...

  4. SpringBoot操作数据库 2017.12.14

    http://blog.csdn.net/forezp/article/details/61472783

  5. sql server导出数据结构

    http://jingyan.baidu.com/article/eae07827ad76ba1fed548573.html

  6. W3C------JS

    ✄--------------------------------------------分割线--------------------------------------------✄ W3C:ht ...

  7. Integer的疑惑

    1.Integer m =200; Integer n =200; System.out.println(m==n); 输出falseInteger x =6; Integer y=6; System ...

  8. linux学习之路(1)

    Linux基础命令 命令格式:   命令名称  [参数]  [对象] 命令参数的长格式与短格式: 长格式:man -- help 短格式:man - h 常用系统工作命令 echo     输出语句  ...

  9. TypeScript 实现任务队列

    业务中经常会有一些批量操作的任务,比如使用 JavaScript 预加载一组图片,批量上传一些资源.如果这些任务一次性启动,势必会消耗很多资源和带宽.理想的做法应该对这些任务进行限制,比如一次只跑几个 ...

  10. 自学java能不能找到找到一份java工作。

    关于自学Java能不能通过社招找到一份互联网公司Java开发的工作,有无数的人问出这样的问题,答案没有标准的,只能从概率去考虑.有的人可以,有的人不可以,有的人自学就业的概率就是高,有的概率就是低. ...