所謂會話,即用戶訪問應用時保持的鏈接關係,在屢次交互中應用可以識別出當前訪問的用戶是誰,且能夠在屢次交互中保存一些數據。如訪問一些網站時登陸成功後,網站能夠記住用戶,且在退出以前均可以識別當前用戶是誰。html
Subject subject = SecurityUtils.getSubject(); Session session = subject.getSession();
登陸成功後使用 Subject.getSession() 便可獲取會話;其等價於 Subject.getSession(true),即若是當前沒有建立 Session 對象會建立一個;另外 Subject.getSession(false),若是當前沒有建立 Session 則返回 null(不過默認狀況下若是啓用會話存儲功能的話在建立 Subject 時會主動建立一個 Session)。java
//獲取當前會話的惟一標識。 session.getId(); //獲取當前 Subject 的主機地址,該地址是經過 HostAuthenticationToken.getHost() 提供的。 session.getHost(); //獲取 ,設置當前 Session 的過時時間;若是不設置默認是會話管理器的全局過時時間。 session.getTimeout(); session.setTimeout(毫秒) //獲取會話的啓動時間及最後訪問時間 session.touch(); session.stop()
更新會話最後訪問時間及銷燬會話;當 Subject.logout() 時會自動調用 stop 方法來銷燬會話。git
session.setAttribute("key", "123"); Assert.assertEquals("123", session.getAttribute("key")); session.removeAttribute("key");
設置 / 獲取 / 刪除會話屬性;在整個會話範圍內均可以對這些屬性進行操做。web
Shiro 提供的會話能夠用於 JavaSE/JavaEE 環境,不依賴於任何底層容器,能夠獨立使用,是完整的會話模塊。redis
會話管理器管理着應用中全部 Subject 的會話的建立、維護、刪除、失效、驗證等工做。是 Shiro 的核心組件,頂層組件 SecurityManager 直接繼承了SessionManager,且提供了SessionsSecurityManager 實現直接把會話管理委託給相應的 SessionManager,DefaultSecurityManager 及 DefaultWebSecurityManager 默認 SecurityManager 都繼承了 SessionsSecurityManager。spring
Shiro 提供了三個默認實現:數據庫
DefaultSessionManager:DefaultSecurityManager 使用的默認實現,用於 JavaSE 環境;apache
ServletContainerSessionManager:DefaultWebSecurityManager 使用的默認實現,用於 Web 環境,其直接使用 Servlet 容器的會話;json
DefaultWebSessionManager:用於 Web 環境的實現,能夠替代 ServletContainerSessionManager,本身維護着會話,直接廢棄了 Servlet 容器的會話管理。跨域
在spring中注入會話管理 spring-shiro.xml,具體注入方式上一節已經講過,完整代碼地址參考 https://gitee.com/jiansin/ssm
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="redisSessionDAO" class="com.plantform.shiro.commons.RedisSessionDao"/> <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> <!-- 設置session過時時間爲1小時(單位:毫秒),默認爲30分鐘 --> <property name="globalSessionTimeout" value="3600000"/> <property name="sessionValidationSchedulerEnabled" value="true"/> <property name="sessionDAO" ref="redisSessionDAO"/> </bean> <bean id="cacheManager" class="com.plantform.shiro.commons.RedisCacheManager"> <property name="redisTemplate" ref="redisTemplate"/> </bean> <!-- Shiro默認會使用Servlet容器的Session,可經過sessionMode屬性來指定使用Shiro原生Session --> <!-- 即<property name="sessionMode" value="native"/>,詳細說明見官方文檔 --> <!-- 這裏主要是設置自定義的單Realm應用,如有多個Realm,可以使用'realms'屬性代替 --> <!-- securityManager安全管理器 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realms"> <list> <ref bean="shiroRealm"/> </list> </property> <!-- 注入緩存管理器 --> <property name="cacheManager" ref="cacheManager"/> <!-- 注入session管理器 --> <property name="sessionManager" ref="sessionManager"/> <!-- 記住我 --> </bean> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <!-- 要求登陸時的連接(可根據項目的URL進行替換),非必須的屬性,默認會自動尋找Web工程根目錄下的"/login.html"頁面 --> <property name="loginUrl" value="/index.jsp"/> <!-- 用戶訪問未對其受權的資源時,所顯示的鏈接 --> <property name="unauthorizedUrl" value="/"/> <property name="filters"> <map> <entry key="authc" value-ref="authenticationFilter"/> </map> </property> <!-- Shiro鏈接約束配置,即過濾鏈的定義 --> <!-- 此處可配合個人這篇文章來理解各個過濾連的做用http://blog.csdn.net/jadyer/article/details/12172839 --> <!-- 下面value值的第一個'/'表明的路徑是相對於HttpServletRequest.getContextPath()的值來的 --> <!-- anon:它對應的過濾器裏面是空的,什麼都沒作,這裏.do和.jsp後面的*表示參數,比方說login.jsp?main這種 --> <!-- authc:該過濾器下的頁面必須驗證後才能訪問,它是Shiro內置的一個攔截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter --> <property name="filterChainDefinitions"> <value> /login.jsp=anon /system/captcha=anon /static/**=anon /system/logout = anon /system/login=anon /oauth/**=anon /error/**=anon /v2/**/=anon /webjars/**=anon /swagger-resources/**=anon /swagger-ui.html/**=anon /**=authc </value> </property> </bean> <bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"> <property name="hashAlgorithmName" value="md5"/> <property name="hashIterations" value="2"/> </bean> <bean id="shiroRealm" class="com.plantform.shiro.commons.ShiroRealm"> <property name="credentialsMatcher" ref="credentialsMatcher"/> </bean> <bean id="authenticationFilter" class="com.plantform.shiro.commons.ShiroAuthenticationFilter"/> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> <!-- AOP式方法級權限檢查 --> <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"> <property name="proxyTargetClass" value="true"/> </bean> <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> <property name="securityManager" ref="securityManager"/> </bean> </beans>
若是使用 ServletContainerSessionManager 進行會話管理,Session 的超時依賴於底層 Servlet 容器的超時時間,能夠在 web.xml 中配置其會話的超時時間(分鐘爲單位):
<session-config> <session-timeout>30</session-timeout> </session-config>
會話監聽器用於監聽會話建立、過時及中止事件:
public class MySessionListener1 implements SessionListener { @Override public void onStart(Session session) {//會話建立時觸發 System.out.println("會話建立:" + session.getId()); } @Override public void onExpiration(Session session) {//會話過時時觸發 System.out.println("會話過時:" + session.getId()); } @Override public void onStop(Session session) {//退出/會話過時時觸發 System.out.println("會話中止:" + session.getId()); } }
spring中注入shiro會話監聽器
<!-- shiroSessionListener 監聽類--> <bean id="shiroSessionListener" class="com.listener.ShiroSessionListener"></bean> <bean id="redisSessionDAO" class="com.plantform.shiro.commons.RedisSessionDao"/> <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> <!-- 設置session過時時間爲1小時(單位:毫秒),默認爲30分鐘 --> <property name="globalSessionTimeout" value="3600000"/> <property name="sessionValidationSchedulerEnabled" value="true"/> <property name="sessionDAO" ref="redisSessionDAO"/> <property name="sessionListeners"> <list> <ref bean="shiroSessionListener"></ref> </list> </property> </bean>
Shiro 提供 SessionDAO 用於會話的 CRUD,即 DAO(Data Access Object)模式實現:
//如DefaultSessionManager在建立完session後會調用該方法;如保存到關係數據庫/文件系統/NoSQL數據庫;便可以實現會話的持久化;返回會話ID;主要此處返回的ID.equals(session.getId()); Serializable create(Session session); //根據會話ID獲取會話 Session readSession(Serializable sessionId) throws UnknownSessionException; //更新會話;如更新會話最後訪問時間/中止會話/設置超時時間/設置移除屬性等會調用 void update(Session session) throws UnknownSessionException; //刪除會話;當會話過時/會話中止(如用戶退出時)會調用 void delete(Session session); //獲取當前全部活躍用戶,若是用戶量多此方法影響性能 Collection<Session> getActiveSessions();
spring-shiro.xml
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> <!-- 設置session過時時間爲1小時(單位:毫秒),默認爲30分鐘 --> <property name="globalSessionTimeout" value="3600000"/> <property name="sessionValidationSchedulerEnabled" value="true"/> <property name="sessionDAO" ref="redisSessionDAO"/> </bean>
public class RedisSessionDao extends AbstractSessionDAO { private static final String sessionIdPrefix = "shiro-session-"; private static final String sessionIdPrefix_keys = "shiro-session-*"; //設置過時時間爲1小時(單位:毫秒),默認爲30分鐘 --> private static final long timeout = 3600000; private transient static Logger log = LoggerFactory.getLogger(RedisSessionDao.class); @Autowired private transient RedisTemplate<Serializable, Session> redisTemplate; @Override protected Serializable doCreate(Session session) { Serializable sessionId = sessionIdPrefix + UUID.randomUUID().toString(); assignSessionId(session, sessionId); //操做字符串 redisTemplate.opsForValue().set(sessionId, session, timeout, TimeUnit.SECONDS); log.info("create shiro session ,sessionId is :{}", sessionId.toString()); return sessionId; } @Override protected Session doReadSession(Serializable sessionId) { log.info("read shiro session ,sessionId is :{}", sessionId.toString()); return redisTemplate.opsForValue().get(sessionId); } @Override public void update(Session session) throws UnknownSessionException { log.info("update shiro session ,sessionId is :{}", session.getId().toString()); redisTemplate.opsForValue().set(session.getId(), session, timeout, TimeUnit.SECONDS); } @Override public void delete(Session session) { log.info("delete shiro session ,sessionId is :{}", session.getId().toString()); redisTemplate.opsForValue().getOperations().delete(session.getId()); } @Override public Collection<Session> getActiveSessions() { Set<Serializable> keys = redisTemplate.keys(sessionIdPrefix_keys); if (keys.size() == 0) { return Collections.emptySet(); } List<Session> sessions = redisTemplate.opsForValue().multiGet(keys); return Collections.unmodifiableCollection(sessions); } }
用戶在登陸的時候把session信息存入到數據庫中
/** * 登陸 * * @param loginName 登陸名 * @param password 密碼 * @param platform 終端類型 * @return */ @ApiOperation(value = "登陸", httpMethod = "POST", produces = "application/json", response = Result.class) @ResponseBody @RequestMapping(value = "login", method = RequestMethod.POST) public Result login(@RequestParam String loginName, @RequestParam String password, @RequestParam int platform, HttpServletRequest request) throws Exception { SysUser user = sysUserService.selectByLoginName(loginName); if (user == null) { return Result.instance(ResponseCode.unknown_account.getCode(), ResponseCode.unknown_account.getMsg()); } if (user.getStatus() == 3) { return Result.instance(ResponseCode.forbidden_account.getCode(), ResponseCode.forbidden_account.getMsg()); } Subject subject = SecurityUtils.getSubject(); //這裏若是發生異常會拋出到繼承的類中去處理 subject.login(new UsernamePasswordToken(loginName, password)); //準備存入session信息到數據庫中 LoginInfo loginInfo = sysUserService.login(user, subject.getSession().getId(), platform); subject.getSession().setAttribute("loginInfo", loginInfo); log.debug("登陸成功"); return Result.success(loginInfo); }
服務層中的實現方法
@Override public LoginInfo login(SysUser user, Serializable id, int platform) { log.debug("sessionId is:{}", id.toString()); LoginInfo loginInfo = new LoginInfo(); BeanUtils.copyProperties(user, loginInfo); List<SysUserPermission> userPermissions = sysUserPermissionMapper.selectByUserId(user.getId()); List<SysPermission> permissions = new ArrayList<>(); for (SysUserPermission userPermission : userPermissions) { SysPermission sysPermission = sysPermissionMapper.selectById(userPermission.getSysPermissionId()); permissions.add(sysPermission); } List<SysUserRoleOrganization> userRoleOrganizations = sysUserRoleOrganizationMapper.selectByUserId(user.getId()); loginInfo.setJobs(userRoleOrganizations); SysLoginStatus newLoginStatus = new SysLoginStatus(); newLoginStatus.setSysUserId(user.getId()); newLoginStatus.setSysUserZhName(user.getZhName()); newLoginStatus.setSysUserLoginName(user.getLoginName()); newLoginStatus.setSessionId(id.toString()); newLoginStatus.setSessionExpires(new DateTime().plusDays(30).toDate()); newLoginStatus.setPlatform(platform); SysLoginStatus oldLoginStatus = sysLoginStatusMapper.selectByUserIdAndPlatform(user.getId(), platform); if (oldLoginStatus != null) { if (!oldLoginStatus.getSessionId().equals(id.toString())) { redisTemplate.opsForValue().getOperations().delete(oldLoginStatus.getSessionId()); } oldLoginStatus.setStatus(2); sysLoginStatusMapper.update(oldLoginStatus); newLoginStatus.setLastLoginTime(oldLoginStatus.getCreateTime()); } sysLoginStatusMapper.insert(newLoginStatus); return loginInfo; }
<!-- securityManager安全管理器 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realms"> <list> <ref bean="shiroRealm"/> </list> </property> <!-- 注入緩存管理器 --> <property name="cacheManager" ref="cacheManager"/> <!-- 注入session管理器 --> <property name="sessionManager" ref="sessionManager"/> <!-- 記住我 --> </bean> <bean id="cacheManager" class="com.hunt.system.security.shiro.RedisCacheManager"> <property name="redisTemplate" ref="redisTemplate"/> </bean>
package com.hunt.system.security.shiro; import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheException; import org.apache.shiro.cache.CacheManager; import org.apache.shiro.util.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.redis.core.RedisTemplate; import com.hunt.util.SystemConstant; import java.io.Serializable; /** * @Author ouyangan * @Date 2016/10/9/14:13 * @Description 接口實現 */ public class RedisCacheManager implements CacheManager, Serializable { private transient static Logger log = LoggerFactory.getLogger(RedisCacheManager.class); private transient RedisTemplate<Object, Object> redisTemplate; public RedisCacheManager() { } public RedisCacheManager(RedisTemplate<Object, Object> redisTemplate) { this.redisTemplate = redisTemplate; } @Override public <K, V> Cache<K, V> getCache(String name) throws CacheException { if (!StringUtils.hasText(name)) { throw new IllegalArgumentException("Cache name cannot be null or empty."); } log.debug("redis cache manager get cache name is :{}", name); Cache cache = (Cache) redisTemplate.opsForValue().get(name); if (cache == null) { cache = new RedisCache<>(redisTemplate); redisTemplate.opsForValue().set(SystemConstant.shiro_cache_prefix + name, cache); } return cache; } public void setRedisTemplate(RedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } } package com.hunt.system.security.shiro; import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.redis.core.RedisTemplate; import com.hunt.util.SystemConstant; import java.io.Serializable; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; /** * @Author ouyangan * @Date 2016/10/9/13:55 * @Description Cache redis實現 */ public class RedisCache<K, V> implements Cache<K, V>, Serializable { public static final String shiro_cache_prefix = "shiro-cache-"; public static final String shiro_cache_prefix_keys = "shiro-cache-*"; private static final long timeout = 2592000; private transient static Logger log = LoggerFactory.getLogger(RedisCache.class); private transient RedisTemplate<K, V> redisTemplate; public RedisCache(RedisTemplate<K, V> redisTemplate) { this.redisTemplate = redisTemplate; } public RedisCache() { } @Override public V get(K key) throws CacheException { log.debug("根據key:{}從redis獲取對象", key); log.debug("redisTemplate : {}", redisTemplate); return redisTemplate.opsForValue().get(shiro_cache_prefix + key); } @Override public V put(K key, V value) throws CacheException { log.debug("根據key:{}從redis刪除對象", key); redisTemplate.opsForValue().set((K) (shiro_cache_prefix + key), value, timeout, TimeUnit.SECONDS); return value; } @Override public V remove(K key) throws CacheException { log.debug("redis cache remove :{}", key.toString()); V value = redisTemplate.opsForValue().get(shiro_cache_prefix + key); redisTemplate.delete(key); return value; } @Override public void clear() throws CacheException { log.debug("清除redis全部緩存對象"); Set<K> keys = redisTemplate.keys((K) shiro_cache_prefix_keys); redisTemplate.delete(keys); } @Override public int size() { Set<K> keys = redisTemplate.keys((K) shiro_cache_prefix_keys); log.debug("獲取redis緩存對象數量:{}", keys.size()); return keys.size(); } @Override public Set<K> keys() { Set<K> keys = redisTemplate.keys((K)shiro_cache_prefix_keys); log.debug("獲取全部緩存對象的key"); if (keys.size() == 0) { return Collections.emptySet(); } return keys; } @Override public Collection<V> values() { Set<K> keys = redisTemplate.keys((K) shiro_cache_prefix_keys); log.debug("獲取全部緩存對象的value"); if (keys.size() == 0) { return Collections.emptySet(); } List<V> vs = redisTemplate.opsForValue().multiGet(keys); return Collections.unmodifiableCollection(vs); } public RedisTemplate<K, V> getRedisTemplate() { return redisTemplate; } public void setRedisTemplate(RedisTemplate<K, V> redisTemplate) { this.redisTemplate = redisTemplate; } }
安全性要求高的網站不建議有記住密碼功能,由於Cookie是保存在本機電腦瀏覽器中,不排除其餘用戶使用該電腦,複製走Cookie,導入其餘電腦繼續使用該帳號登陸。
spring-shiro.xml文件以下
<!-- securityManager安全管理器 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realms"> <list> <ref bean="shiroRealm"/> </list> </property> <!-- 注入緩存管理器 --> <property name="cacheManager" ref="cacheManager"/> <!-- 注入session管理器 --> <property name="sessionManager" ref="sessionManager"/> <!-- 記住我 --> <property name="rememberMeManager" ref="rememberMeManager"></property> </bean> <!-- 定義RememberMe功能的程序管理類 --> <bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager"> <!-- 定義在進行RememberMe功能實現的時候所須要使用到的Cookie的處理類 --> <property name="cookie" ref="rememberMeCookie"/> </bean> <!-- 配置須要向Cookie中保存數據的配置模版(RememberMe) --> <bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie"> <!-- 設置Cookie在瀏覽器中保存內容的名字,由用戶本身來設置 --> <constructor-arg value="MLDNJAVA-RememberMe"/> <!-- 保證該系統不會受到跨域的腳本操做供給 --> <property name="httpOnly" value="true"/> <!-- 定義Cookie的過時時間爲一天設置securityManager安全管理器的rememberMeManager,具體配置以下: ``` ```--> <property name="maxAge" value="86400"/> </bean>
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="filterChainDefinitions"> <value> /login.jsp = anon /authenticated.jsp = authc /logout = logout /** = user </value> </property> </bean>
注意:/authenticated.jsp = authc」表示訪問該地址用戶必須身份驗證經過(Subject. isAuthenticated()==true);而「/** = user」表示訪問該地址的用戶是身份驗證經過或RememberMe登陸的均可以進行任何操做的。
LoginAuthRealm.java
// 認證信息 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException { try { UsernamePasswordToken token = (UsernamePasswordToken) authcToken; String username = token.getUsername(); SysUsers user = userSv.getByName(token.getUsername()); if (!StringUtils.isBlank(username)) { if (user != null) { return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), getName()); } } } catch (Exception e) { e.printStackTrace(); } return null; }
注:return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), getName());其中把用戶信息放入SimpleAuthenticationInfo對象,不能把整個user對象放入,否則會出現錯誤數組下標越界,在項目中user對象信息過於龐大,不能所有存入Cookie,Cookie對長度有必定的限制。