上一篇已经分析了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. elasticsearch x-pack

    elasticsearch-plugin.bat install x-pack D:\elasticsearch-5.5.3\bin>elasticsearch-plugin.bat insta ...

  2. NIO(一)——缓冲区Buffer

                                        NIO(一)--Buffer NIO简介 NIO即New IO,是用来代替标准IO的,提供了与标准IO完全不同传输方式. 核心: ...

  3. 前端BUG监控神器

    有时候,看到用户的反馈,我们往往会一脸茫然,因为反馈的信息太少了. 比如有用户反馈登录不了.为了解这个问题,一般的流程是这样的:首先试试自己能不能登录网站,发现没问题:然后查看后台日志,发现最近没有登 ...

  4. bzoj3811 玛里苟斯

    分三种情况讨论 k=1时,对于每一位而言,只要有一个数这一位是1,那么这个就有0.5的概率是1,选他就是1,不选就是0,有第二个的话,在第一个选或不选的前提下,也各有0.5的几率选或不选,0和1的概率 ...

  5. 关于Python元祖,列表,字典,集合的比较

      定义 方法 列表 可以包含不同类型的对象,可以增减元素,可以跟其他的列表结合或者把一个列表拆分,用[]来定义的 eg:aList=[123,'abc',4.56,['inner','list'], ...

  6. java  JDK配置环境变量

    1)将下载的jdk放置到一定文件夹中,注意文件夹名不能有中文! 2)设置环境变量 a.可以在系统变量中找到path这个变量,然后将jdk下的bin的根目录添加进去 注意:一定要放在path变量值的最前 ...

  7. MySQL-5.6.36-多实例-部署(编译版)

    MySQL多实例_沁贰百科 注:部署双实例前,首先需要部署单实例,单实例部署详情如下: https://www.cnblogs.com/wangqiner/p/9081002.html 1.如已经安装 ...

  8. Android 8.1 源码_启动篇(一) -- 深入研究 init(转 Android 9.0 分析)

    前言 init进程,它是一个由内核启动的用户级进程,当Linux内核启动之后,运行的第一个进程是init,这个进程是一个守护进程,确切的说,它是Linux系统中用户控件的第一个进程,所以它的进程号是1 ...

  9. 【转】W3C中国与百度联合组织移动网页加速技术研讨会

    2017 年 8 月 30 日,W3C 会员百度在北京中关村软件园国际会议中心主办了 "移动网页加速技术研讨会",W3C 中国以及腾讯.阿里巴巴及 UC.搜狗.小米.傲游.中国移动 ...

  10. API 测试的具体实现

    目录 API 测试的具体实现 基于 Spring Boot 构建的 API 使用 cURL 命令行工具进行测试 使用图形界面工具 Postman 进行测试 如何应对复杂场景的 API 测试? 总结 A ...