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
相关阅读
Shiro的认证原理(Subject#login的背后故事)
登录操作一般都是我们触发的: Subject subject = SecurityUtils.getSubject(); AuthenticationToken authenticationToken = new