shiro框架是一个强大的轻量级java安全框架。它提供了权限验证、加密、session管理的功能。shiro易用、上手快,应用场景大到企业级应用、小到手机应用都可以使用。本文就针对shiro的subject一个点展开,讲讲这个subject的来龙去脉。
我关注这个类要从一次错误说起。在我的项目里面突然就出现subject无法获得principals字段信息的情况,自然我每次登陆再请求什么都是subject.getPrincipal()等于空。
SecurityUtils.getSubject()这个方法是从线程获取的数据。在不了解subject原理的时候我的判断是线程号换了所以数据就找不到了。所以,我一直在研究为啥线程号总换。这个思路是非常错误的,错误在并没有真正了解subject这个类里面的数据是怎么来的。
那么subject里面的数据究竟是怎么来的,怎么就能从线程级别获取到subject了呢?
我们在使用shiro的时候首先配置了一个它的代理过滤器在web.xml里面。所以要从shiro的过滤器开始说起,shiro的内部过滤器的实现在这段代码。

protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
throws ServletException, IOException {
Throwable t = null;
try {
final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
final Subject subject = createSubject(request, response);
//noinspection unchecked
subject.execute(new Callable() {
public Object call() throws Exception {
updateSessionLastAccessTime(request, response);
executeChain(request, response, chain);
return null;
}
});
} catch (ExecutionException ex) {
t = ex.getCause();
} catch (Throwable throwable) {
t = throwable;
}
if (t != null) {
if (t instanceof ServletException) {
throw (ServletException) t;
}
if (t instanceof IOException) {
throw (IOException) t;
}
//otherwise it's not one of the two exceptions expected by the filter method signature - wrap it in one:
String msg = "Filtered request failed.";
throw new ServletException(msg, t);
}
}

shiro过滤器第一步就将servletRequest、servletResponse两个数据包装成shiro类型的request和response。
第二步就是创建subject。

protected WebSubject createSubject(ServletRequest request, ServletResponse response) {
return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject();
}

这个方法包括两个部分:
1、获取核心类securityManager 。
2、使用创造者模式创建subject。
2.1、Builder方法将securityManager、request、response属性设置到subjectContext中。
2.2、调用buildWebSubject()方法做具体的创建。

public WebSubject buildWebSubject() {
Subject subject = super.buildSubject();//1
if (!(subject instanceof WebSubject)) {
String msg = "Subject implementation returned from the SecurityManager was not a " +
WebSubject.class.getName() + " implementation.Please ensure a Web-enabled SecurityManager " +
"has been configured and made available to this builder.";
throw new IllegalStateException(msg);
}
return (WebSubject) subject;
}

看下标注1的实现

public Subject buildSubject() {
return this.securityManager.createSubject(this.subjectContext);//1.1
}

1.1具体实现如下:

public Subject createSubject(SubjectContext subjectContext) {
//获取subjectContext信息到context
SubjectContext context = copy(subjectContext);
//设置securityManager到context
context = ensureSecurityManager(context);
//设置cotext的session信息到context
context = resolveSession(context);
//设置principals信息到context
context = resolvePrincipals(context);
//创建subject
Subject subject = doCreateSubject(context);
//保存subject 的登陆信息保存到session中或者持久化库中
save(subject);
return subject;
}

从创建subject步骤来看subject数据应该是从context里面获取到的。具体怎么获取的呢?

public Subject createSubject(SubjectContext context) {
if (!(context instanceof WebSubjectContext)) {
return super.createSubject(context);
}
WebSubjectContext wsc = (WebSubjectContext) context;
SecurityManager securityManager = wsc.resolveSecurityManager();
Session session = wsc.resolveSession();
boolean sessionEnabled = wsc.isSessionCreationEnabled();
PrincipalCollection principals = wsc.resolvePrincipals();
boolean authenticated = wsc.resolveAuthenticated();
String host = wsc.resolveHost();
ServletRequest request = wsc.resolveServletRequest();
ServletResponse response = wsc.resolveServletResponse();
return new WebDelegatingSubject(principals, authenticated, host, session, sessionEnabled,
request, response, securityManager);
}

原来是subjectFacotry方法中创建的WebDelegatingSubject实例。也就是说subject里面的各个字段都是从这个方法里面获得的。下面我们就来看看我遇到的那个问题,pricipals怎么为空了?数据应该从哪里来的。

public PrincipalCollection resolvePrincipals() {
//MapContext的backingMap是否存在principals
PrincipalCollection principals = getPrincipals();
//MapContext的backingMap是否存在info,如果存在在这里获取。
if (CollectionUtils.isEmpty(principals)) {
//check to see if they were just authenticated:
AuthenticationInfo info = getAuthenticationInfo();
if (info != null) {
principals = info.getPrincipals();
}
}
//MapContext的backingMap是否存在subject,如果存在在这里获取。
if (CollectionUtils.isEmpty(principals)) {
Subject subject = getSubject();
if (subject != null) {
principals = subject.getPrincipals();
}
}
//MapContext的backingMap是否存在session,如果存在从session里面获取
if (CollectionUtils.isEmpty(principals)) {
//try the session:
Session session = resolveSession();
if (session != null) {
principals = (PrincipalCollection) session.getAttribute(PRINCIPALS_SESSION_KEY);
}
}
return principals;
}

从principals的获取顺序可以猜测principals这个数据应首先出现在session中。这样如果在系统尚未登录时候,session刚刚创建,表单的信息应该先放在session中,这样我们就能获得这个principals数据了。
接下来,我们从登录的过程开始看看数据是如何被放入session中的。
我们在登陆的时候会配置一个CustomFormAuthenticationFilter过滤器实例,如下:


> /user/login=authc
/** =sysUser,onlineSession,,perms,roles 

它的父类FormAuthenticationFilter。这个类是一个切面过滤器AccessControlFilter的子类。每一次请求都会首先执行该方法:

public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
}

isAccessAllowed(request, response, mappedValue)是一个空方法。onAccessDenied(request, response, mappedValue)方法在FormAuthenticationFilter中被实现。

protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
if (isLoginRequest(request, response)) {
if (isLoginSubmission(request, response)) {
if (log.isTraceEnabled()) {
log.trace("Login submission detected.Attempting to execute login.");
}
return executeLogin(request, response);
} else {
if (log.isTraceEnabled()) {
log.trace("Login page view.");
}
//allow them to see the login page ;)
return true;
}
} else {
if (log.isTraceEnabled()) {
log.trace("Attempting to access a path which requires authentication.Forwarding to the " +
"Authentication url [" + getLoginUrl() + "]");
}
saveRequestAndRedirectToLogin(request, response);
return false;
}
}

该方法首先判断请求路径和我们xml配置的登陆路径是否一致。然后判断请求是否是post方法。满足以上两个条件调用父类的executeLogin(request, response)执行登陆操作。由此,我们看出登陆这个shiro已经为我们封装好了,不需要我们自己写。

protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
AuthenticationToken token = createToken(request, response);
if (token == null) {
String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " +
"must be created in order to execute a login attempt.";
throw new IllegalStateException(msg);
}
try {
Subject subject = getSubject(request, response);
subject.login(token);
return onLoginSuccess(token, subject, request, response);
} catch (AuthenticationException e) {
return onLoginFailure(token, e, request, response);
}
}

executeLogin方法就做了三个事情:
1、将我们提交的表单数据封装成token
2、从request、response里面获取subject
3、执行subject的login方法。
4、按照我们配置的跳转路径或者默认的路径跳转到登陆成功页面。
第2步最终还是走了DefaultSecurityManager类的createSubject方法。这个时候由于是没有登陆,那么subject的pricipals、session字段自然是空的。重点来看第3步

public void login(AuthenticationToken token) throws AuthenticationException {
clearRunAsIdentitiesInternal();
//3.1
Subject subject = securityManager.login(this, token);
PrincipalCollection principals;
String host = null;
if (subject instanceof DelegatingSubject) {
DelegatingSubject delegating = (DelegatingSubject) subject;
//we have to do this in case there are assumed identities - we don't want to lose the 'real' principals:
principals = delegating.principals;
host = delegating.host;
} else {
principals = subject.getPrincipals();
}
if (principals == null || principals.isEmpty()) {
String msg = "Principals returned from securityManager.login( token ) returned a null or " +
"empty value.This value must be non null and populated with one or more elements.";
throw new IllegalStateException(msg);
}
this.principals = principals;
this.authenticated = true;
if (token instanceof HostAuthenticationToken) {
host = ((HostAuthenticationToken) token).getHost();
}
if (host != null) {
this.host = host;
}
Session session = subject.getSession(false);
if (session != null) {
this.session = decorate(session);
} else {
this.session = null;
}
}

注意下这个方法在DelegatingSubject类里面。所以这个方法作用就是填充subject。重点在代码中标注的3.1里面。

public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
info = authenticate(token);
} catch (AuthenticationException ae) {
try {
onFailedLogin(token, ae, subject);
} catch (Exception e) {
if (log.isInfoEnabled()) {
log.info("onFailedLogin method threw an " +
"exception.Logging and propagating original AuthenticationException.", e);
}
}
throw ae; //propagate
}
Subject loggedIn = createSubject(token, info, subject);
onSuccessfulLogin(token, info, loggedIn);
return loggedIn;
}

首先是校验我们表单提交过来的信息是否能够登陆到系统中。
代码太多不贴出,写下调用顺序:
AuthenticatingSecurityManager-》AbstractAuthenticator-》ModularRealmAuthenticator-》AuthenticatingRealm-》MyRealm(自定义)
这时候如果在我们自定义的MyRealm校验通过,就会返回一个

SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, password.toCharArray(), getName());

有了这些信息就能将subject的相应的登陆信息字段信息填充到subjectContext对象中,有了所有的数据再次调用createSubject(context)方法,重新创建subject实例。

protected Subject createSubject(AuthenticationToken token, AuthenticationInfo info, Subject existing) {
SubjectContext context = createSubjectContext();
context.setAuthenticated(true);
context.setAuthenticationToken(token);
context.setAuthenticationInfo(info);
if (existing != null) {
context.setSubject(existing);
}
return createSubject(context);
}

最后一件比较重要的事情就是session信息的填充。session是什么时候创建,并跟随request里的sessionid到浏览器,然后又是如何从session中恢复subject中的呢?
无论是否成功登陆了,session在shiro过滤器的时候就已经有了,如图。
参见这段代码:

final Subject subject = createSubject(request, response);

创建subject的过程,不仅仅是要从session中恢复一些数据,如果系统尚不存在session的时候会主动创建。这个创建过程是从cookie的sessionid中创建。首次没有session信息的时候,会根据cookie带过来的sessionId创建一个新的session。

类:DefaultWebSessionManager
private String getSessionIdCookieValue(ServletRequest request, ServletResponse response) {
if (!isSessionIdCookieEnabled()) {
log.debug("Session ID cookie is disabled - session id will not be acquired from a request cookie.");
return null;
}
if (!(request instanceof HttpServletRequest)) {
log.debug("Current request is not an HttpServletRequest - cannot get session ID cookie.Returning null.");
return null;
}
HttpServletRequest httpRequest = (HttpServletRequest) request;
return getSessionIdCookie().readValue(httpRequest, WebUtils.toHttp(response));
}

shiro配置的cookie会自动的带回来一个数字串,这个数字串就是我们新建session的id值。
DefaultSessionManager里面的retrieveSessionFromDataSource方法会从我们配置的sessionDAO中获取持久化的session里面是否有id为它的session信息。如果没有在我们持久化的sessionDAO中找到相应的session信息,在debug下会打印我们经常看到的一个异常信息:

org.apache.shiro.session.UnknownSessionException: There is no session with id [63916bfc-173c-4d39-a154-ae7c8f81a925]
at org.apache.shiro.session.mgt.eis.AbstractSessionDAO.readSession(AbstractSessionDAO.java:170) ~[shiro-core-1.2.3.jar:1.2.3]
at org.apache.shiro.session.mgt.eis.CachingSessionDAO.readSession(CachingSessionDAO.java:261) ~[shiro-core-1.2.3.jar:1.2.3]
at org.apache.shiro.session.mgt.DefaultSessionManager.retrieveSessionFromDataSource(DefaultSessionManager.java:236) ~[shiro-core-1.2.3.jar:1.2.3]

由此我们知道,session信息无论是否是新的还是已登录的session。在过滤器首次创建subject的时候都将session设置到了subject中。同时,subject信息也会被放置到session中。

类:DefaultSecurityManager
save(subject);
类DefaultSubjectDAO
public Subject save(Subject subject) {
if (isSessionStorageEnabled(subject)) {
saveToSession(subject);
} else {
log.trace("Session storage of subject state for Subject [{}] has been disabled: identity and " +
"authentication state are expected to be initialized on every request or invocation.", subject);
}
return subject;
}

那么session中如何将principal放置到session中的呢?同样还是这段代码

protected void saveToSession(Subject subject) {
//performs merge logic, only updating the Subject's session if it does not match the current state:
mergePrincipals(subject);
mergeAuthenticationState(subject);
}

当然,必须是在subject里面含有pricipal信息的时候才能够放置成功。
回到登陆的过程,登陆的过程最终还是调用了DefaultSecurityManager类里面的createSubject(SubjectContextsubjectContext)方法。由于在登陆的过程中一些登陆信息被设置。
到了subjectContext中,这样在调用完createSubject方法,登陆信息会在createSubject(SubjectContextsubjectContext)方法调用 save(subject);时候被设置到sessoin。
由此,我们可以得出一个结论:subject里面的登陆信息每次从线程获取之前,数据一定是从session中获取。所以cookie的配置正确与否会影响到subject数据的正常显示。cookie配置一定要注意两个参数:path和domain。不要把path配置的太深,会导致有些路径获取不到cookie导致subject数据读取失败。不要把domain配置成跨域,跨域会导致cookie获取不到。从而无法读到sessionid而获取不到session信息。

转载:https://www.aliyun.com/jiaocheng/825309.html

shiro 登陆成功后subject依然为空相关推荐

  1. shiro登陆成功后被拦截_Springboot+Shiro+redis整合

    1.Shiro是Apache下的一个开源项目,我们称之为Apache Shiro.它是一个很易用与Java项目的的安全框架,提供了认证.授权.加密.会话管理,与spring Security 一样都是 ...

  2. Spring security/Shiro ---登陆成功后返回登陆前界面<页面重定向>

    Spring security ---登陆成功后返回登陆前界面<页面重定向> 问题:在登陆/退出成功后,我们往往通过http.formLogin().successForwardUrl() ...

  3. javaWeb项目用过滤器filter实现登陆成功后才能访问主页面,否则直接输入主页面的地址自动跳转到登陆界面

    想用Filter实现一个登陆验证的功能,实现登陆成功后才能访问主页面,否则直接输入主页面的地址会自动跳转到登陆界面 原理很简单,每次登陆成功后,创建一个session域对象,将登陆成功的用户名保存在s ...

  4. java中登陆界面怎么连接到下一个界面啊_JavaWeb登陆成功后跳转到上一个页面

    JavaWeb登陆成功后跳转到上一个页面,这个标题注定要词不达意,你可能会遇到这样的情形,当点击页面的某个请求时,由于用户未登录,需要跳转到登录页,用户登录成功后,再跳转到上一个页面:还有一种情况,多 ...

  5. 织梦ajax登录界面,dede织梦后台登陆成功后又跳转到登陆页面

    dede织梦后台登陆成功后又跳转到登陆页面怎么办?相信在很多个人站长朋友们在dede建站运营中都有遇到过类似的问题,在登陆界面输入账号密码后会有3-5秒的卡顿才会进入下面的页面,然后再卡顿3-5秒就跳 ...

  6. JavaWeb登陆成功后跳转到上一个页面

    JavaWeb登陆成功后跳转到上一个页面,这个标题注定要词不达意,你可能会遇到这样的情形,当点击页面的某个请求时,由于用户未登录,需要跳转到登录页,用户登录成功后,再跳转到上一个页面:还有一种情况,多 ...

  7. SSM框架,ajax实现登陆界面验证和登陆成功后页面跳转问题

    账号.密码和验证码都正确后,使用了ajax实现验证,验证结束后不能像正常一样返回一个字符串,用视图解析器来跳转页面 <!--配置JSP 显示ViewResolver(视图解析器)--> & ...

  8. springboot shiro ajax,SpringBoot Shiro 登录成功后返回json数据 shiro使用ajax登录

    老规矩,先上代码: protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletReques ...

  9. Python 淘宝系列(三): 模拟登陆成功后获取购物车信息

    http://my.oschina.net/u/811744/blog/192604(本文的转载地址) ================================================ ...

最新文章

  1. 怎么理解ASM中的Failgroup
  2. 别忘记今天是父亲节六盘水
  3. DAS,NAS,SAN在数据库存储上的应用
  4. python string length_如何使用python获取字符串长度?哪些方法?
  5. javascript演变史_检查有史以来最著名JavaScript错误之一
  6. python读取多个txt文件数据恢复_带有Pandas的Python 2.7:如何恢复两个数据帧...
  7. 【LeetCode】33. Search in Rotated Sorted Array 解题小结
  8. rails logger 和 session, cookies, request方面的笔记
  9. linux下编程出现 对'sem_wait'未定义的引用解决方案
  10. android 仿饿了么地图,微信小程序仿饿了么地址定位、筛选与回传
  11. Civil 3d 交叉口标注
  12. 怎样彻底删除 mac 上的 Adobe 相关文件?
  13. VSCode设置网页代码实时预览
  14. android 渐变的背景色,Android背景渐变xml
  15. 并行计算综述————第一章 并行计算硬件平台:并行计算机
  16. 哪种耳机适合跑步用、跑步运动耳机推荐
  17. 如何在同一台服务器上绑定多个域名?
  18. AWS - Auto Scaling 介绍
  19. CAN SPLIT功能作用和SPLIT电容作用
  20. 【PPT制作工具】超实用!有哪些鲜为人知却好用到爆的PPT辅助工具?

热门文章

  1. 转型经验分享|作为传统汽车工程师,我如何转型去阿里做无人驾驶?
  2. ISO文件怎么安装?
  3. 网络空间安全未来就业前景和就业方向,看着六点
  4. 从千播大战到不足百家,倒闭潮之下直播创业何去何从?
  5. SAP 成套销售按项目销售
  6. Word文档中styles分析
  7. 陈臣java_小菜学设计模式——享元模式
  8. 手赚网试玩平台源码 可封装APP 带文章资讯功能 帝国cms7.5内核
  9. RequestMapping的映射URL模板
  10. 国庆弯道超车正当时,推荐一个免费的刷题网站。。。Python小伙伴可以看过来了