shiro 登陆成功后subject依然为空

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

你可能感兴趣的:(spring)