admin管理员组

文章数量:1530518

问题描述

之前在公司搭项目平台的时候权限框架采用的是shiro,由于系统主要面向的是APP端的用户,PC端仅仅是公司内部人员在使用,而且考虑到系统的可用性和扩展性,服务端首先基于shiro做了一些改造以支持多数据源认证和分布式会话(关于分布式session可查看{% post_link SpringBoot集成Shiro实现多数据源认证授权与分布式会话(一)%}).我们知道在web环境下http是一种无状态的通讯协议,要想记录和校验用户的登录状态必须通过session的机制来实现,浏览器是通过cookie中存储的sessionid来确定用户的session数据的,shiro默认也是采用这种机制.而对于移动端用户来讲,则可以使用token的方式来进行身份鉴权,原理跟浏览器使用cookie传输是一样的.

token身份鉴权的流程

1.服务端在用户正常登录之后,通过特定算法生成一个全局唯一的字符串token并返回给客户端.

2.客户端在接下来的请求都会在请求头中携带token,服务端拦截token并对用户做身份鉴权.

3.token带有自动失效的机制,当用户主动退出或者失效时间一到则服务端删除会话信息.

遇到的问题

网上查了一下我们知道shiro也是通过携带cookie中的sessionid来做鉴权的,既然移动端使用的是token的机制,那么要想使shiro能够支持这套机制就必须改造shiro的鉴权方式.之前在搭框架的时候为了解决这个问题曾经草草的翻了一下shiro的源码(这货的代码量真心大啊,看的人一头雾水),找了很久也没找到它是在何处处理的,当时因为时间关系只好放弃,用了一种很笨的方法在请求头header中存储以键为Cookie,值为token=web_session_key-xxx的键值对的方式来确保shiro能通过解析校验,这样app端是能够正常交互的,但是对于后面增加的h5应用或者小程序则不行,首先是跨域问题(关于跨域可查看{% post_link 前后端分离之CORS跨域请求踩坑总结%}),由于是前后端分离的应用,浏览器的同源策略不允许js访问跨域的cookie,这样每次请求shiro获取的cookie都为空,过滤器会拦截下这个请求并作出如下响应:

image.png

为了h5应用能够与服务端正常交互只好想办法绕过shiro的拦截校验,既然无法传输cookie,只好在header中传一个token,并在自定义的过滤器(继承自shiro的FormAuthenticationFilter)中覆写它的isAccessAllowed方法,此方法返回值若为true则说明shiro鉴权通过,否则执行redirectToLogin方法跳转到登录页面.

@Override

protected boolean isAccessAllowed(ServletRequest request,

ServletResponse response, Object mappedValue) {

HttpServletRequest httpRequest = (HttpServletRequest) request;

boolean isLogin;

String device = httpRequest.getHeader("device");

// 如果是客户端是H5

if (StringHelpUtils.isNotBlank(device) && device.equals("H5")) {

String h5Token = httpRequest.getHeader("token");

Cookie[] cookies = httpRequest.getCookies();

if (null != cookies) {

for (Cookie cookie : cookies) {

if (cookie.getName().equals("token")) {

cookie.setValue(h5Token);

}

}

}

isLogin = isH5Login(h5Token);//绕过shiro,直接到redis中校验token

} else {

// 如果是APP或者PC端

Subject subject = getSubject(request, response);

isLogin = subject.isAuthenticated();

}

return isLogin;

}

到这里基本上shiro的登录校验是绕过去了,其实这里并不是真的绕过,因为shiro该做的事情还是会照做,只不过是我们再到redis中去匹配一次而已,但是却带来了一个新的问题,那就是服务端通过SecurityUtils.getSubject().getSession();取到的用户session对象与之前登录时产生的session对象并不是同一个,原因是shiro本身在执行校验时由于无法获取到cookie中的token,所以它把这个请求当成是一个新的请求,每次调用都会创建一个新的session,但这个新session里面并不存在我们需要的用户相关登录信息,而由于app与小程序是同一套接口,这样就影响到了原先已写好的业务代码了...虽然解决方法还是有的,但是总觉得整个过程下来,代码和功能的实现都让人觉得很别扭,因此本文想从源码的角度去逐步剖析shiro是如何拦截未登录请求的,从根源上来寻求解决方案,同时又不会对已对接好的业务接口造成影响.

源码跟踪

在开始跟源码之前,我们先来看看下面的异常堆栈图

image.png

之所以要帖这个图是因为shiro的代码实在太多,全部去看不太现实,因此在程序里面造了个异常,从异常的底部开始一步步跟下去总可以发现根源的,我们知道shiro的入口是个过滤器shiroFilter,因此不用怀疑先从过滤器找起,首先是ApplicationFilterChain,看名字和包路径就知道这个不是shiro的实现类,大概查了一下知道它是tomcat实现的过滤器链.其采用了责任链的设计模式,我们在idea中打开这个类,在它的internalDoFilter方法上加断点.

image.png

由这行代码ApplicationFilterConfig filterConfig = this.filters[this.pos++];可知this.filters是一个ApplicationFilterConfig集合,这个集合存储了ApplicationFilterChain里面的所有过滤器,如下图.

QQ截图20180711145037.png

其中ApplicationFilterConfig是个filter容器,我们来看看它的定义:

org.apache.catalina.core.ApplicationFilterConfig

Implementation of a javax.servlet.FilterConfig useful in managing the filter instances instantiated when a web application is first started.

大致意思是说当web应用一开始启动时,会将工程中的所有的实例化的filter实例加载到此容器中.下面来看看容器启动后加载了哪些filter实例.

name=characterEncodingFilter

name=hiddenHttpMethodFilter

name=httpPutFormContentFilter

name=requestContextFilter

name=corsFilter

name=shiroFilter

name=Tomcat WebSocket(JSR356) Filter

其中characterEncodingFilter(用于处理编码问题)、hiddenHttpMethodFilter(隐藏HTTP函数)、httpPutFormContentFilter(form表单处理)、requestContextFilter(请求上下文)等是springboot自动添加的一些常用过滤器(注意这几个filter的完整限定类名为Ordered开头的,如OrderedCharacterEncodingFilter继承自CharacterEncodingFilter,里面多了个order属性,用于确定该filter的执行顺序).

corsFilter是我们后端统一使用cors来解决跨域问题的.

wsFilter这个filter应该是用来处理WebSocket的.

最后一个shiroFilter是我们要关注的重点,它是整个shiro框架的入口,注意它的filterClass是ShiroFilterFacotoryBean$SpringShiroFilter,看名字应该是spring生成的一个代理类了,先不管它是怎么生成的,继续往下看会发现上述列出来的过滤器中除了wsFilter和shiroFilter之外其他的filter都继承自org.springframework.web.filter.OncePerRequestFilter.而spring的OncePerRequestFilter是一个抽象过滤器类,其中定义的抽象方法doFilterInternal是由其子类来实现的,doFilter方法则是final的子类只能继承不能覆写.比如CharacterEncodingFilter继承了OncePerRequestFilter的doFilter方法且实现了doFilterInternal方法.

所以从上面的异常堆栈图中我们可以看出每次ApplicationFilterChain执行链中filter的doFilter方法时都会先执行它的父类OncePerRequestFilter的doFilter方法然后再执行这个filter实现的doFilterInternal方法,一直到shiro自己的OncePerRequestFilter(注意shiro自己实现的这个filter跟spring的不是同一个)为止.你一定会很奇怪为什么这里会执行shiro的OncePerRequestFilter,按道理springboot默认的filter和跨域的filter都执行过去了,那么接下来要执行的应该是shiro的入口shiroFilter才对.所以到目前为止我们一共有两个疑惑:

1.ShiroFilterFacotoryBean$SpringShiroFilter是怎么来的.

2.shiro的执行入口为什么是其内部实现的OncePerRequestFilter.

带着这些问题我们继续往下看,首先是项目中shiroFilter的配置.

ShiroFilterFactoryBean

@Bean

public ShiroFilterFactoryBean shiroFilter() {

ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

// 必须设置 SecurityManager

shiroFilterFactoryBean.setSecurityManager(getDefaultWebSecurityManager());

........

return shiroFilterFactoryBean;

}

从上述配置可知使用了ShiroFilterFactoryBean来创建shiroFilter,所以重点在于ShiroFilterFactoryBean这个类.它的主要源代码如下:

public class ShiroFilterFactoryBean implements FactoryBean, BeanPostProcessor {

private static final transient Logger log = LoggerFactory.getLogger(ShiroFilterFactoryBean.class);

private SecurityManager securityManager;

private Map filters = new LinkedHashMap();

private Map filterChainDefinitionMap = new LinkedHashMap();

private String loginUrl;

private String successUrl;

private String unauthorizedUrl;

private AbstractShiroFilter instance;

......

}

从类的定义中可知ShiroFilterFactoryBean实现了接口FactoryBean和BeanPostProcessor.

BeanPostProcessor接口的作用是在Spring容器启动时,容器中所有的bean在初始化的前后都会调用这个接口的方法postProcessBeforeInitialization并在这个方法中判断当前的bean是否为Filter,若是则装载进Map集合filters中.

public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {

if (bean instanceof Filter) {

log.debug("Found filter chain candidate filter '{}'", beanName);

Filter filter = (Filter)bean;

this.applyGlobalPropertiesIfNecessary(filter);

this.getFilters().put(beanName, filter);

} else {

log.trace("Ignoring non-Filter bean '{}'", beanName);

}

return bean;

}

public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

return bean;

}

而ShiroFilterFactoryBean本身又实现了另外一个接口FactoryBean,FactoryBean的作用是在执行getBean("shiroFilter")时会调用其getObject方法来获取一个代理实例,看源码可知其调用的是this.createInstance()函数,返回的是一个SpringShiroFilter实例.代码如下:

public Object getObject() throws Exception {

if (this.instance == null) {

this.instance = this.createInstance();

}

return this.instance;

}

public Class getObjectType() {

return ShiroFilterFactoryBean.SpringShiroFilter.class;

}

protected AbstractShiroFilter createInstance() throws Exception {

log.debug("Creating Shiro Filter instance.");

SecurityManager securityManager = this.getSecurityManager();

String msg;

if (securityManager == null) {

msg = "SecurityManager property must be set.";

throw new BeanInitializationException(msg);

} else if (!(securityManager instanceof WebSecurityManager)) {

msg = "The security manager does not implement the WebSecurityManager interface.";

throw new BeanInitializationException(msg);

} else {

FilterChainManager manager = this.createFilterChainManager();

PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();

chainResolver.setFilterChainManager(manager);

return new ShiroFilterFactoryBean.SpringShiroFilter((WebSecurityManager)securityManager, chainResolver);

}

}

SpringShiroFilter是ShiroFilterFactoryBean类的静态内部类,继承自shiro的AbstractShiroFilter.

private static final class SpringShiroFilter extends AbstractShiroFilter {

protected SpringShiroFilter(WebSecurityManager webSecurityManager, FilterChainResolver resolver) {

if (webSecurityManager == null) {

throw new IllegalArgumentException("WebSecurityManager property cannot be null.");

} else {

this.setSecurityManager(webSecurityManager);

if (resolver != null) {

this.setFilterChainResolver(resolver);

}

}

}

跟到这里也就明白了ShiroFilterFacotoryBean$SpringShiroFilter是怎么来的了,那么第一个问题已解答,至于第二个问题的答案则要通过AbstractShiroFilter的源码来找了.

public abstract class AbstractShiroFilter extends OncePerRequestFilter {

}

通过源码可知AbstractShiroFilter是一个抽象过滤器,继承自shiro的抽象filter-OncePerRequestFilter,而OncePerRequestFilter中的抽象方法doFilterInternal则由其实现类AbstractShiroFilter负责实现,我们来看看doFilterInternal的具体代码:

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是如何拦截未登录请求的,根据前面的贴出来异常堆栈图的指示,我们重点来看看上面第10行代码中的createSubject方法,其实现如下:

protected WebSubject createSubject(ServletRequest request, ServletResponse response) {

return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject();

}

此处调用的buildWebSubject方法的实现是在接口WebSubject的内部静态类Builder中.

public WebSubject buildWebSubject() {

Subject subject = super.buildSubject();

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;

}

这个方法主要用来创建shiro的主体subject,在静态类Builder中并没有相关实现的代码,而是在其父类Subject中.

public Subject buildSubject() {

return this.securityManager.createSubject(this.subjectContext);

}

this.securityManager是shiro的安全管理器,管理着所有的Subject,且负责进行认证和授权、及会话、缓存等的管理,在实例化ShiroFilterFactoryBean时由springboot配置中注入过来的,我们来看看项目中的配置:

shiroFilterFactoryBean.setSecurityManager(getDefaultWebSecurityManager());

@Bean(name = "securityManager")

public DefaultWebSecurityManager getDefaultWebSecurityManager() {

logger.info("注入Shiro的Web过滤器-->securityManager", ShiroFilterFactoryBean.class);

DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

......

return securityManager;

}

从上面的代码调试跟踪可知createSubject方法的实现在DefaultWebSecurityManager的父类DefaultSecurityManager中.

public Subject createSubject(SubjectContext subjectContext) {

//create a copy so we don't modify the argument's backing map:

SubjectContext context = copy(subjectContext);

//ensure that the context has a SecurityManager instance, and if not, add one:

context = ensureSecurityManager(context);

//Resolve an associated Session (usually based on a referenced session ID), and place it in the context before

//sending to the SubjectFactory. The SubjectFactory should not need to know how to acquire sessions as the

//process is often environment specific - better to shield the SF from these details:

context = resolveSession(context);

//Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first

//if possible before handing off to the SubjectFactory:

context = resolvePrincipals(context);

Subject subject = doCreateSubject(context);

//save this subject for future reference if necessary:

//(this is needed here in case rememberMe principals were resolved and they need to be stored in the

//session, so we don't constantly rehydrate the rememberMe PrincipalCollection on every operation).

//Added in 1.2:

save(subject);

return subject;

}

其中第11行context = resolveSession(context);看注释是通过引用的sessionid来解析关联的会话,进去看看它的实现:

protected SubjectContext resolveSession(SubjectContext context) {

if (context.resolveSession() != null) {

log.debug("Context already contains a session. Returning.");

return context;

}

try {

//Context couldn't resolve it directly, let's see if we can since we have direct access to

//the session manager:

Session session = resolveContextSession(context);

if (session != null) {

context.setSession(session);

}

} catch (InvalidSessionException e) {

log.debug("Resolved SubjectContext context session is invalid. Ignoring and creating an anonymous " +

"(session-less) Subject instance.", e);

}

return context;

}

注意第9行Session session = resolveContextSession(context);跟进去这个方法.

protected Session resolveContextSession(SubjectContext context) throws InvalidSessionException {

SessionKey key = getSessionKey(context);

if (key != null) {

return getSession(key);

}

return null;

}

protected SessionKey getSessionKey(SubjectContext context) {

Serializable sessionId = context.getSessionId();

if (sessionId != null) {

return new DefaultSessionKey(sessionId);

}

return null;

}

其中getSession(key)调用的是抽象类SessionsSecurityManager中的getSession方法.

public Session getSession(SessionKey key) throws SessionException {

return this.sessionManager.getSession(key);

}

继续往下跑则到了抽象类AbstractNativeSessionManager中的getSession方法.

public Session getSession(SessionKey key) throws SessionException {

Session session = lookupSession(key);

return session != null ? createExposedSession(session, key) : null;

}

private Session lookupSession(SessionKey key) throws SessionException {

if (key == null) {

throw new NullPointerException("SessionKey argument cannot be null.");

}

return doGetSession(key);

}

而第10行的doGetSession方法则调用了抽象类AbstractValidatingSessionManager中的doGetSession方法

@Override

protected final Session doGetSession(final SessionKey key) throws InvalidSessionException {

enableSessionValidationIfNecessary();

log.trace("Attempting to retrieve session with key {}", key);

Session s = retrieveSession(key);

if (s != null) {

validate(s, key);

}

return s;

}

这里重点来看看第7行retrieveSession方法,跟进去发现它的实现在shiro的默认session管理器类DefaultSessionManager中.

protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {

Serializable sessionId = getSessionId(sessionKey);

if (sessionId == null) {

log.debug("Unable to resolve session ID from SessionKey [{}]. Returning null to indicate a " +

"session could not be found.", sessionKey);

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;

}

由第2行Serializable sessionId = getSessionId(sessionKey);进去getSessionId方法,其实现在类DefaultWebSessionManager中.

@Override

public Serializable getSessionId(SessionKey key) {

Serializable id = super.getSessionId(key);

if (id == null && WebUtils.isWeb(key)) {

ServletRequest request = WebUtils.getRequest(key);

ServletResponse response = WebUtils.getResponse(key);

id = getSessionId(request, response);//此处调用下面的getSessionId方法

}

return id;

}

protected Serializable getSessionId(ServletRequest request, ServletResponse response) {

return getReferencedSessionId(request, response);//调用下面的getReferencedSessionId方法

}

private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) {

//这句是重点

String id = getSessionIdCookieValue(request, response);

if (id != null) {

request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,

ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE);

} else {

//not in a cookie, or cookie is disabled - try the request URI as a fallback (i.e. due to URL rewriting):

//try the URI path segment parameters first:

id = getUriPathSegmentParamValue(request, ShiroHttpSession.DEFAULT_SESSION_ID_NAME);

if (id == null) {

//not a URI path segment parameter, try the query parameters:

String name = getSessionIdName();

id = request.getParameter(name);

if (id == null) {

//try lowercase:

id = request.getParameter(name.toLowerCase());

}

}

if (id != null) {

request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,

ShiroHttpServletRequest.URL_SESSION_ID_SOURCE);

}

}

if (id != null) {

request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);

//automatically mark it valid here. If it is invalid, the

//onUnknownSession method below will be invoked and we'll remove the attribute at that time.

request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);

}

// always set rewrite flag - SHIRO-361

request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, isSessionIdUrlRewritingEnabled());

return id;

}

关键在第18行String id = getSessionIdCookieValue(request, response);这句,继续跟进去.

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));

}

可以看到这个方法的返回值是getSessionIdCookie().readValue(httpRequest, WebUtils.toHttp(response));其中getSessionIdCookie()返回的是一个Cookie对象,但是注意这里的Cookie是shiro自定义的一个接口.

public interface Cookie {

/**

* The value of deleted cookie (with the maxAge 0).

*/

public static final String DELETED_COOKIE_VALUE = "deleteMe";

/**

* The number of seconds in one year (= 60 * 60 * 24 * 365).

*/

public static final int ONE_YEAR = 60 * 60 * 24 * 365;

/**

* Root path to use when the path hasn't been set and request context root is empty or null.

*/

public static final String ROOT_PATH = "/";

String getName();

void setName(String name);

String getValue();

void setValue(String value);

String getComment();

void setComment(String comment);

String getDomain();

void setDomain(String domain);

int getMaxAge();

void setMaxAge(int maxAge);

String getPath();

void setPath(String path);

boolean isSecure();

void setSecure(boolean secure);

int getVersion();

void setVersion(int version);

void setHttpOnly(boolean httpOnly);

boolean isHttpOnly();

void saveTo(HttpServletRequest request, HttpServletResponse response);

void removeFrom(HttpServletRequest request, HttpServletResponse response);

String readValue(HttpServletRequest request, HttpServletResponse response);

}

我们来看看其子类SimpleCookie中的readValue,这里的SimpleCookie就是我们之前在springboot中所配置的bean,我们给它命了个名字叫"token".

@Bean

public SimpleCookie wapsession() {

SimpleCookie simpleCookie = new SimpleCookie("token");

simpleCookie.setMaxAge(2592000);

return simpleCookie;

}

然后去看它的readValue方法.

public String readValue(HttpServletRequest request, HttpServletResponse ignored) {

String name = getName();

String value = null;

javax.servlet.http.Cookie cookie = getCookie(request, name);

if (cookie != null) {

// Validate that the cookie is used at the correct place.

String path = StringUtils.clean(getPath());

if (path != null && !pathMatches(path, request.getRequestURI())) {

log.warn("Found '{}' cookie at path '{}', but should be only used for '{}'", new Object[] { name, request.getRequestURI(), path});

} else {

value = cookie.getValue();

log.debug("Found '{}' cookie value [{}]", name, value);

}

} else {

log.trace("No '{}' cookie value", name);

}

return value;

}

上面第2行的getName()返回的是cookie的名称,我们在配置时传入的是字符串"token",再看第4行中的getCookie方法,虽然前面的Cookie对象是shiro自定义的,但这里获取的cookie却是java原生的javax.servlet.http.Cookie类.

private static javax.servlet.http.Cookie getCookie(HttpServletRequest request, String cookieName) {

javax.servlet.http.Cookie cookies[] = request.getCookies();

if (cookies != null) {

for (javax.servlet.http.Cookie cookie : cookies) {

if (cookie.getName().equals(cookieName)) {

return cookie;

}

}

}

return null;

}

跟到这里总算找到shiro是在哪里拦截cookie的了,前面我们说过在app端是以Cookie为键,token=web_session_key-xxx为值的键值对方式传输的所以不会被拦截,而h5或小程序由于无法传输cookie则直接传token,那么服务端自然取不到cookie也就是说sessionid为null,所以当调用doGetSession(key)方法时返回的session对象也是null的,我们再往回看DefaultSecurityManager的createSubject方法,执行完context = resolveSession(context);之后返回的是一个session为null的上下文信息,紧接着执行context = resolvePrincipals(context);获取登录用户的Principal信息,由于在上面返回的context中并没有找到相关已登录的信息,自然取出来的principal和authenticationInfo也是null.

protected SubjectContext resolvePrincipals(SubjectContext context) {

PrincipalCollection principals = context.resolvePrincipals();

if (CollectionUtils.isEmpty(principals)) {

log.trace("No identity (PrincipalCollection) found in the context. Looking for a remembered identity.");

principals = getRememberedIdentity(context);

if (!CollectionUtils.isEmpty(principals)) {

log.debug("Found remembered PrincipalCollection. Adding to the context to be used " +

"for subject construction by the SubjectFactory.");

context.setPrincipals(principals);

// The following call was removed (commented out) in Shiro 1.2 because it uses the session as an

// implementation strategy. Session use for Shiro's own needs should be controlled in a single place

// to be more manageable for end-users: there are a number of stateless (e.g. REST) applications that

// use Shiro that need to ensure that sessions are only used when desirable. If Shiro's internal

// implementations used Subject sessions (setting attributes) whenever we wanted, it would be much

// harder for end-users to control when/where that occurs.

//

// Because of this, the SubjectDAO was created as the single point of control, and session state logic

// has been moved to the DefaultSubjectDAO implementation.

// Removed in Shiro 1.2. SHIRO-157 is still satisfied by the new DefaultSubjectDAO implementation

// introduced in 1.2

// Satisfies SHIRO-157:

// bindPrincipalsToSession(principals, context);

} else {

log.trace("No remembered identity found. Returning original context.");

}

}

return context;

}

public PrincipalCollection resolvePrincipals() {

PrincipalCollection principals = getPrincipals();

if (CollectionUtils.isEmpty(principals)) {

//check to see if they were just authenticated:

AuthenticationInfo info = getAuthenticationInfo();

if (info != null) {

principals = info.getPrincipals();

}

}

if (CollectionUtils.isEmpty(principals)) {

Subject subject = getSubject();

if (subject != null) {

principals = subject.getPrincipals();

}

}

if (CollectionUtils.isEmpty(principals)) {

//try the session:

Session session = resolveSession();

if (session != null) {

principals = (PrincipalCollection) session.getAttribute(PRINCIPALS_SESSION_KEY);

}

}

return principals;

}

再接着就是Subject subject =doCreateSubject(context); 创建一个subject了.来看下大致是怎么创建subject的.

public Subject createSubject(SubjectContext context) {

//这里的context其实就是个map集合即上文调用context = resolveSession(context);返回的

if (!(context instanceof WebSubjectContext)) {

return super.createSubject(context);

}

WebSubjectContext wsc = (WebSubjectContext) context;

SecurityManager securityManager = wsc.resolveSecurityManager();

Session session = wsc.resolveSession();//取出的session是null

//在context中没有取到key的SESSION_CREATION_ENABLED的元素,所以直接返回为ture

boolean sessionEnabled = wsc.isSessionCreationEnabled();

PrincipalCollection principals = wsc.resolvePrincipals();//取出的principals是null

//由于前面得到的AuthenticationInfo是null,所以这里的authenticated 为false

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);

}

可以看到最终创建的subject对象.

image.png

subject创建完了,接着就是过滤器链的处理了,中间经过ProxiedFilterChain、OncePerRequestFilter、AdviceFilter、PathMatchingFilter这几个过滤器再到AccessControlFilter.onPreHandle方法.

public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {

return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);

}

isAccessAllowed调的是AuthenticationFilter中的isAccessAllowed方法.

protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {

Subject subject = getSubject(request, response);

return subject.isAuthenticated();

}

这里的subject就是我们之前创建的subject了,显然isAuthenticated方法返回false.接着执行onAccessDenied方法,此方法实现在AuthenticatingFilter的子类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;

}

}

代码很简单非登录请求则执行saveRequestAndRedirectToLogin方法.

protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException {

saveRequest(request);

redirectToLogin(request, response);

}

//以下两个方法皆在WebUtils类中实现.

protected void saveRequest(ServletRequest request) {

WebUtils.saveRequest(request);

}

public static void saveRequest(ServletRequest request) {

Subject subject = SecurityUtils.getSubject();

Session session = subject.getSession();//重点看这行

HttpServletRequest httpRequest = toHttp(request);

SavedRequest savedRequest = new SavedRequest(httpRequest);

session.setAttribute(SAVED_REQUEST_KEY, savedRequest);

}

上面的Session session = subject.getSession();真正调用的是getSession(true);方法参数为true表示会创建一个新的session对象.这块代码相对简单可以加断点一步步跟进去,大致上最终就是调用我们自定义的RedisSessionDao创建一个新的session对象之后,再执行DefaultWebSessionManager的storeSessionId方法创建一个SimpleCookie对象,最后在response中添加到请求头header里面.

public void saveTo(HttpServletRequest request, HttpServletResponse response) {

String name = getName();

String value = getValue();

String comment = getComment();

String domain = getDomain();

String path = calculatePath(request);

int maxAge = getMaxAge();

int version = getVersion();

boolean secure = isSecure();

boolean httpOnly = isHttpOnly();

addCookieHeader(response, name, value, comment, domain, path, maxAge, version, secure, httpOnly);

}

private void addCookieHeader(HttpServletResponse response, String name, String value, String comment,

String domain, String path, int maxAge, int version,

boolean secure, boolean httpOnly) {

String headerValue = buildHeaderValue(name, value, comment, domain, path, maxAge, version, secure, httpOnly);

response.addHeader(COOKIE_HEADER_NAME, headerValue);

if (log.isDebugEnabled()) {

log.debug("Added HttpServletResponse Cookie [{}]", headerValue);

}

}

新的session对象生成之后最终将执行redirectToLogin方法,由于我们自定义了扩展自FormAuthenticationFilter的过滤器,因此调用的是我们自己的方法,并在其中对不同的调用端做不同的响应处理.

@Override

protected void redirectToLogin(ServletRequest request,

ServletResponse response) throws IOException {

String loginUrl = getLoginUrl();

if (logger.isDebugEnabled()) {

logger.debug("客户端登录的URL:{}", loginUrl);

}

HttpServletRequest httpRequest = (HttpServletRequest) request;

//System.out.println(httpRequest.getRequestURL());

HttpServletResponse httpResponse = (HttpServletResponse) response;

httpResponse.setContentType("text/html; charset=utf-8");

httpRequest.setCharacterEncoding("UTF-8");

// 是否为APP登录请求

if (StringHelpUtils.isNotBlank(httpRequest.getHeader("device"))

&& (httpRequest.getHeader("device").equals("APP") || httpRequest

.getHeader("device").equals("H5"))) {

String token = httpRequest.getHeader(TOKEN);

if (logger.isDebugEnabled()) {

logger.debug("客户端设备:{},token:{}",

httpRequest.getHeader("device"), token);

}

if (StringHelpUtils.isBlank(token)) {

ResponseEntity result = new ResponseEntity().isOk(HttpStatus.TOKEN_NOT_EXIST,

"认证失败!");

httpResponse.getWriter().append(JSON.toJSONString(result));

httpResponse.getWriter().flush();

httpResponse.getWriter().close();

} else {

ResponseEntity result = new ResponseEntity().isOk(

HttpStatus.APP_UNKNOW_ACCOUNT, "认证失败!");

httpResponse.getWriter().append(JSON.toJSONString(result));

httpResponse.getWriter().flush();

httpResponse.getWriter().close();

}

} else {

// PC跳转 如果是非Ajax请求 按默认的配置跳转到登录页面

if (!"XMLHttpRequest".equalsIgnoreCase(httpRequest

.getHeader("X-Requested-With"))) {// 不是ajax请求

WebUtils.issueRedirect(request, response, loginUrl);

} else {

// 如果是Aajx请求,则返回会话失效的JSON信息

ResponseEntity result = new ResponseEntity().isOk(

HttpStatus.SESSION_UNVAILDATE

, "请求失败!");

httpResponse.getWriter().append(JSON.toJSONString(result));

httpResponse.getWriter().flush();

httpResponse.getWriter().close();

}

}

}

本文标签: shiroajaxShiro