shiro安全控制目录
Shiro提供了完整的企业级会话管理功能,不依赖底层容器(如web容器的tomcat),不管是JavaSE还是JavaEE环境都可以使用,提供了会话管理,会话监听,会话存储/持久化,容器无关的集群,失效/过期支持。对Web的透明支持,SSO单点登录的支持等特性。即使用Shiro的会话管理可以直接替换如Web容器的会话管理。
序 什么叫做会话?
会话是用户访问应用时保持的连接关系。
因为http协议是无状态的协议,所以,需要借助会话(session)来使得应用在多次交互中能够识别出当前访问的用户是谁。并且可以在多次会话中保存一些数据。
如访问一些网站时登录,网站可以记住用户,且在退出之前都可以识别当前用户是谁。
1. shiro Session简单的API
Shiro Session和HttpSession使用方式很像。当然它们最大的区别在于你可以在任何应用中使用Shiro Session,而不仅仅局限于Web应用。
1. 获取session对象
Shiro的会话支持不仅可以在普通JavaEE应用中使用,也可以在web应用中使用,且获取方式是一致的。
Subject subject = SecurityUtils.getSubject();
Session session = subject.getSession();
//这个参数用于判定会话不存在时是否创建新会话。
Session session = subject.getSession(boolean create);
可以使用subject.getSession()获取会话,其等价于subject.getSession(true),即如果当前没有创建Session对象,会创建一个。
2. 获取会话的唯一标识
session.getId();
3. 获取主机地址
session.getHost();
获取当前subject的主机地址,该地址是通过HostAuthenticationToken.getHost()
提供的。
4. 设置会话超时时间
//获取超时时间
session.getTimeout();
//设置超时时间
session.setTimeout(毫秒);
获取/设置当前Session的过期时间;如果不设置是默认的会话管理器的全局过期时间。
5. 获取启动/访问时间
//获取会话的启动时间
session.getStartTimestamp();
//获取会话的最后访问时间
session.getLastAccessTime();
获取会话的启动时间和最后访问时间;如果是JavaSE应用需要自己定期调用session.touch()去更新最后访问时间;如果是web应用,每次进入ShiroFilter都会自动调用session.touch()来更新最后访问时间。
6. 更新/删除会话
//更新会话最后访问时间
session.touch();
//销毁session会话
session.stop();
更新会话最后访问时间及销毁会话;当Subject.logout()时会自动调用stop方法来销毁会话的。如果在web中,调用javax.servlet.http.HttpSession. invalidate()也会自动调用Shiro Session.stop方法进行销毁Shiro的会话。
7. 操作会话
session.setAttribute("key", "123");
Assert.assertEquals("123", session.getAttribute("key"));
session.removeAttribute("key");
设置/获取/删除会话属性;在整个会话范围内都可以对这些属性进行操作。
SessionManager负责创建和管理用户Session生命周期,在任何环境下都可以提供用户健壮的session体验。默认情况下,Shiro会使用容器自带的session机制,但若是容器不存在session,那么Shiro会提供内置的企业级session来管理。当然在开发中,也可以使用SessionDAO允许数据源持久化Session。
2. 会话管理器
在安全框架领域,Apache Shiro提供了一些独特的东西,可以在任何应用和架构层一致的使用Session API,即Session不再依赖于Servlet或EJB容器。
Shiro会话最重要的一个好处便是它们独立于容器。通过Shiro会话,可以获取一个容器无关的集群解决方案。Shiro架构允许可插拔的会话数据存储,如企业缓存,关系型数据库,noSQL系统等。并且Shiro会话可以跨客户端技术进行共享。
值得一提的是Shiro在Web环境中对会话的支持。
缺省 Http 会话
对于Web应用,Shiro缺省将我们习以为常的Servlet容器会话作为其会话的基础设施。即调用subject.getSession()
和subject.getSession(boolean)
方法时,Shiro会返回Servlet容器的HttpSession实例支持的Session实例。这种方式巧妙之处在于调用subject.getSession()的业务层代码会跟一个Shiro Session实例交互,并且实际上它也会跟基于Web的HttpSession打交道。可以维护架构层之间的清晰隔离。
Web 层中 Shiro 的原生会话
如果需要Shiro的企业级会话特性(如与容器无关的集群)而打开了Shiro的原生会话管理。而实际上我们也希望HttpServletRequest.getSession()
和HttpSession API
能和Shiro原生的会话协作。而Shiro完整实现了Servlet规范中Session部分以及在Web应用中支持原生会话。这意味着,不管何时你使用相应的HttpServletRequest或HttpSession调用,Shiro都会将这些调用委托给内部的原生会话API。即无需修改Web代码,即使正在使用Shiro内置的Session机制,获取到的Servlet Session和Shiro Session依旧保持一致。
详见——让 Apache Shiro 保护你的应用
虚线:实现的接口;
实线:继承的父类;
Shiro提供了三个默认实现:
- DefaultSessionManager:用于JavaSE环境;
- ServletContainerSessionManager:用于Web环境,直接使用Servlet容器会话;
- DefaultWebSessionManager:用于Web环境,自己维护会话,不会使用Servlet容器的会话管理。
3. subject和request获取Session的区别
3.1. 两者方式获取的session是否相同
1. 在Spring mvc中获取session有两种方法:
- 使用request对象获取session
Session session = request.getSession();
- 通过shiro获取session
Subject currentUser = SecurityUtils.getSubject();
Session session = currentUser.getSession();
一般在web中,有两种会话管理器
- DefaultWebSessionManager (自己维护会话)
- ServletContainerSessionManager(默认,直接使用servlet的会话)
而在项目中需要配置shiro的securityManager,因为配置影响了shiro session的来源。
2. 两种会话操纵的session是否相同?(注:以Servlet会话进行分析)
在controller中打印session,发现request获取的会话类型是:org.apache.catalina.session.StandardSessionFacade
,而subject的session类型是org.apache.shiro.subject.support.DelegatingSubject$StoppingAwareProxiedSession
在上图中,我们可以知道request获取的session明显是httpSession
,而subject获取的session类型,本质上也是httpSession
。即两者在操作session时,都是操作的同一类型的session对象。
3.2 request对象中session的来源
- 如何获取过滤器filter
SpringMVC整合shiro,需要在web.xml中配置filter
shiroFilter
org.springframework.web.filter.DelegatingFilterProxy
targetFilterLifecycle
true
shiroFilter
/*
DelegateFilterProxy是一个过滤器,准确来说是目的过滤器的代理,由它在doFilter方法中,获取spring容器中的过滤器,并调用目标过滤器的doFilter方法。这样做的好处是:原来的过滤器配置放在web.xml中,现在可以把filter的配置放在spring中,并由spring管理它的生命周期。
DelegatingFilterProxy——将Filter交由Spring管理
我们可以知道,使用DelegatingFilterProxy那么过滤器的生命周期由Spring来管理。若是没有指定targetBeanName,那么使用
- spring.xml配置
/login/init/** = anon
/login/getVerifyCode/** = anon
/register/** = anon
/static/** = anon
熟悉spring的应该知道,bean的工厂是用来生产相关的bean,并将bean注册到spring容器中。通过查看工厂bean的getObject方法,可见,委托类调用的filter类型是SpringShiroFilter。
既然SpringShiroFilter属于过滤器,那么肯定有一个doFilter方法,doFilter由它的父类OncePerRequestFilter实现。
OncePerRequestFilter在doFilter方法中,判断是否在request中有"already filtered"这个属性设置为true,如果有,则交给下一个过滤器,如果没有,就执行doFilterInternal()抽象方法。
doFilterInternal由AbstractShiroFilter类实现,即SpringShiroFilter的直属父类实现。
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
throws ServletException, IOException {
//包装request/response
final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
//创建subject,其实创建的是Subject的代理类DelegatingSubject
final Subject subject = createSubject(request, response);
// 继续执行过滤器链,此时的request/response是前面包装好的request/response
subject.execute(new Callable() {
public Object call() throws Exception {
updateSessionLastAccessTime(request, response);
executeChain(request, response, chain);
return null;
}
});
}
在doFilterInternal中,可以看到对ServletRequest和ServletResponse进行了包装,除此之外,还把包装后的request/response作为参数,创建了Subject,这个subject其实就是代理类DelegatingSubject。
那么这个包装后的request是什么呢?
我们继续解析prepareServletRequest。
protected ServletRequest prepareServletRequest(ServletRequest request, ServletResponse response, FilterChain chain) {
ServletRequest toUse = request;
if (request instanceof HttpServletRequest) {
HttpServletRequest http = (HttpServletRequest) request;
toUse = wrapServletRequest(http); //真正去包装request的方法
}
return toUse;
}
protected ServletRequest wrapServletRequest(HttpServletRequest orig) {
//看看看,ShiroHttpServletRequest
return new ShiroHttpServletRequest(orig, getServletContext(), isHttpSessions());
}
由此我们可以看到controller获取到的ShiroHttpServletRequest对象。
ShiroHttpServletRequest构造方法的第三个参数是关键参数。进入ShiroHttpServletRequest里面看看它有什么用?
- 在getRequestedSessionId()方法用到,获取sessionId。
- 在getSession()用到,获取session会话对象。
(1)先看下getRequestedSessionId()。isHttpSessions决定sessionid是否来自servlet。
public String getRequestedSessionId() {
String requestedSessionId = null;
if (isHttpSessions()) {
requestedSessionId = super.getRequestedSessionId(); //从servlet中获取sessionid
} else {
Object sessionId = getAttribute(REFERENCED_SESSION_ID); //从request中获取REFERENCED_SESSION_ID这个属性
if (sessionId != null) {
requestedSessionId = sessionId.toString();
}
}
return requestedSessionId;
}
(2)再看下getSession。isHttpSession决定了session是否来自servlet。
public HttpSession getSession(boolean create) {
HttpSession httpSession;
if (isHttpSessions()) {
httpSession = super.getSession(false); //从servletRequest获取session
if (httpSession == null && create) {
if (WebUtils._isSessionCreationEnabled(this)) {
httpSession = super.getSession(create); //从servletRequest获取session
} else {
throw newNoSessionCreationException();
}
}
} else {
if (this.session == null) {
boolean existing = getSubject().getSession(false) != null; //从subject中获取session
Session shiroSession = getSubject().getSession(create); //从subject中获取session
if (shiroSession != null) {
this.session = new ShiroHttpSession(shiroSession, this, this.servletContext);
if (!existing) {
setAttribute(REFERENCED_SESSION_IS_NEW, Boolean.TRUE);
}
}
}
httpSession = this.session;
}
return httpSession;
}
既然isHttpSessions()如此重要,那么我们要看下在什么情况下,他返回true。
protected boolean isHttpSessions() {
return getSecurityManager().isHttpSessionMode();
}
isHttpSessions是否返回true是由shiro安全管理器isHttpSessionMode()决定的。我们使用的安全管理器是DefaultWebSecurityManager
,我们在DefaultWebSecurityManager的源码找到isHttpSessionMode()方法。
public boolean isHttpSessionMode() {
SessionManager sessionManager = getSessionManager();
return sessionManager instanceof WebSessionManager && ((WebSessionManager)sessionManager).isServletContainerSessions();
}
需要注意的是:在配置文件中,我们并没有配置SessionManager,安全管理器会使用会话管理器ServletContainerSessionManager,在ServletContainerSessionManager中,isServletContainerSessions返回true。
因此,在前面的配置中,request中获取的session将是servlet context下的session。
3.3. subject的session来源
前面的doFilterInternal的分析中,还提到了subject创建的过程。接着我们继续分析该过程,判断subject中的session的来源。
在controller中subject获取session
Subject currentUser = SecurityUtils.getSubject();
Session session = currentUser.getSession();
我们看下shiro定义的session类图,具有一些与HttpSession相同的方法,例如setAttribute和getAttribute。
在doFilterInternal中,shiro把包装后的request/response作为参数,创建subject
final Subject subject = createSubject(request, response);
最终,由DefaultWebSubjectFactory创建subject,并把principals [资本 普瑞色跑死]
, session, request, response, securityManager参数封装到subject。由于第一次创建session,此时session没有实例。
那么,当我们第一次调用subject.getSession()尝试获取session时,发生了什么?从前面的代码我们知道,我们获取到的subject是WebDelegatingSubject类型,它的父类DelegatingSubject实现了getSession方法。
public Session getSession(boolean create) {
if (this.session == null && create) {
// 创建session上下文,上下文里面封装有request/response/host
SessionContext sessionContext = createSessionContext();
// 根据上下文,由securityManager创建session
Session session = this.securityManager.start(sessionContext);
// 包装session
this.session = decorate(session);
}
return this.session;
}
接下来解析一下,securityManager根据sessionContext 创建session这个流程。它是交由sessionManager会话管理器进行会话创建。这里的sessionManager其实就是ServletContainerSessionManager
类,找到它的createSession方法。
protected Session createSession(SessionContext sessionContext) throws AuthorizationException {
HttpServletRequest request = WebUtils.getHttpRequest(sessionContext);
// 从request中获取HttpSession
HttpSession httpSession = request.getSession();
String host = getHost(sessionContext);
// 包装成 HttpServletSession
return createSession(httpSession, host);
}
这里就可以知道,其实Session是来源于request的HttpSession,也就是说,来源上一个过滤器中的request的HttpSession。HttpSession以成员变量的形式存在HttpServletSession中。并且从安全管理器获取HttpServletSession后,还调用decorate()装饰session,装饰后的session类型就是StoppingAwareProxiedSession
,HttpServletSession就是它的成员。
session的getAttribute和addAttribute方法,StoppingAwareProxiedSession做了些什么?
它是由父类ProxiedSession实现session.getAttribute和session.addAttribute方法。
public Object getAttribute(Object key) throws InvalidSessionException {
return delegate.getAttribute(key);
}
public void setAttribute(Object key, Object value) throws InvalidSessionException {
delegate.setAttribute(key, value);
}
可见,getAttribute和addAttribute由委托类delegate完成,这里的delegate就是HttpServletSession。
public Object getAttribute(Object key) throws InvalidSessionException {
try {
return httpSession.getAttribute(assertString(key));
} catch (Exception e) {
throw new InvalidSessionException(e);
}
}
public void setAttribute(Object key, Object value) throws InvalidSessionException {
try {
httpSession.setAttribute(assertString(key), value);
} catch (Exception e) {
throw new InvalidSessionException(e);
}
}
最后总结一下,通过request.getSeesion()与subject.getSeesion()获取session后,对session的操作是相同的。而session的来源是servletRequest还是shiro。主要是由安全管理器SecurityManager和SessionManager会话管理器决定。
参考文档:
springmvc集成shiro后,session、request姓汪还是姓蒋?