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:框架提供的接口,代表安全管 ...
随机推荐
- 集成学习之Boosting —— AdaBoost原理
集成学习大致可分为两大类:Bagging和Boosting.Bagging一般使用强学习器,其个体学习器之间不存在强依赖关系,容易并行.Boosting则使用弱分类器,其个体学习器之间存在强依赖关系, ...
- Java EE
- get.go
//获取空间文件 )) resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err ...
- dqname.go
package nsqd func getBackendName(topicName, channelName string) string { // backend names, for u ...
- MFC中打开选择文件夹对话框,并将选中的文件夹地址显示在编辑框中
一般用于选择你要将文件保存到那个目录下,此程序还包含新建文件夹功能 BROWSEINFO bi; ZeroMemory(&bi, sizeof(BROWSEINFO)); //指定存放文件的 ...
- ELK---日志分析系统
ELK就是一套完整的日志分析系统 ELK=Logstash+Elasticsearch+Kibana 统一官网https://www.elastic.co/products ELK模块说明 Logst ...
- java 日期类 小结
import java.text.*; import java.util.*; class Test2 { public static void main(String[] args) { Syste ...
- mysql服务设置远程连接 解决1251 client does not support ..问题
在docker里面创建mysql容器后设置的密码在远程主机连接时候出现错误: 一.如果是在docker里面安装的mysql镜像则需要先进入mysql里面:参考上一篇:https://www.cnblo ...
- 基于SpringBoot从零构建博客网站 - 确定需求和表结构
要确定一个系统的需求,首先需要明确该系统的用户有哪些,然后针对每一类用户,确定其需求.对于博客网站来说,用户有3大类,分别是: 作者,也即是注册用户 游客,也即非注册用户 管理员,网站维护人员 那么从 ...
- 基于滴答清单 Web 开发的 PC 客户端
基于滴答清单 Web 开发的 PC 客户端 关于「滴答清单」 滴答清单是一款不可多得的 GTD 效率工具,它有着清晰明了的界面设计.恰到好处的功能设置.稳定的同步服务,如果你还缺少一款简洁而有效的 G ...