1. 程式人生 > >Shiro 整合SpringMVC 並且實現許可權管理,登入和登出

Shiro 整合SpringMVC 並且實現許可權管理,登入和登出

Apache Shiro是Java的一個安全框架。目前,使用Apache Shiro的人越來越多,因為它相當簡單,對比Spring Security,可能沒有Spring Security做的功能強大,但是在實際工作時可能並不需要那麼複雜的東西,所以使用小而簡單的Shiro就足夠了。

因為我總結的是使用SpringMVC和Apache Shiro整合,注重的是整合和使用,至於基礎,我這裡就不細說了。我使用的是maven進行專案的構建,對於非maven的專案只要把這些JAR包下載下來放到相應的位置即可。因為這個專案是整合Spring的,所以除了Apache shiro的JAR之外,我們還需要shiro-web和shiro-spring的的JAR,下面是所需要的所有shiro架包,至於其他的架包,像快取的架包,Spring和SpringMVC的架包等等還是平時那些通用JAR,沒有多餘的。

<dependency>  
        <groupId>org.apache.shiro</groupId>  
        <artifactId>shiro-core</artifactId>  
        <version>1.2.3</version>  
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-ehcache</artifactId>
        <version>1.2.3</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-web</artifactId>
        <version>1.2.3</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.2.3</version>
    </dependency>

將JAR都準備好了之後,我們就可以開始正式搭建了。下面就分步驟來建立

步驟1.

一:首先建立spring的配置檔案,位置都在resource中(非maven的專案可以放到classpath或者是WEB-INF下面,只要保證最後編譯之後能在classpath下即可),配置檔案為spring-context.xml.

二:建立Apache Shiro的配置檔案,名字是spring-context-shiro.xml,我們只需要和spring的配置檔案放在同一級就可以了。

三:還有一個配置檔案是springmvc的,配置檔案是spring-mvc。前面兩個檔案都是以spring-context*開頭是有原因的,因為這樣我們就可以在web.xml中設定配置檔案的時候,直接使用萬用字元掃描前兩個但是又可以不掃描springmvc的配置檔案

這是在web.xml裡面配置:

複製程式碼

contextConfigLocation classpath*:/spring-context-*.xml org.springframework.web.context.ContextLoaderListener 複製程式碼 除了spring的配置,還有一個配置是非常重要的:shiroFilter。對於初次配置shiro的同學經常遇到一個問題:問題大概講的是shiroFilter找不到,但是我們明明在web.xml和spring-context-shiro配置檔案裡面配置了呀,怎麼回事?這是因為這個shiroFilter名字兩邊需要一致!!!(是不是很坑,但是其實是可以配置的,只是一般人不知道,這個後面講)

複製程式碼 shiroFilter org.springframework.web.filter.DelegatingFilterProxy shiroFilter /* 複製程式碼 3.

除了在web.xml中設定spring和spring-shiro配置檔案位置之外,我們還需要在web.xml中設定spring-mvc的位置:

複製程式碼

springServlet org.springframework.web.servlet.DispatcherServlet contextConfigLocation classpath:springmvc.xml 1 springServlet / 複製程式碼 4

在spring-context配置檔案中,還有一個是需要配置-cacheManager,因為shiro的session是自己實現的,所以我們還需要一個快取框架,所以在spring的配置檔案一定要注意配置哦,用的是ehcache

<!-- 快取 -->
<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
    <property name="configLocation" value="classpath:${ehcache.file}"></property>
</bean>

Ehcache的maven地址:

複製程式碼 net.sf.ehcache ehcache-core 2.6.9 org.apache.shiro shiro-ehcache 1.2.3 複製程式碼 5

在專案中重點還是配置spring-context-shiro.xml:先把配置的貼出來,然後講一下這幾個配置的意義:

複製程式碼

<?xml version="1.0" encoding="UTF-8"?>
<description>Shiro Configuration</description>

<!-- 載入配置屬性檔案 -->
<context:property-placeholder ignore-unresolvable="true" location="classpath:yonyou.properties" />

<!-- Shiro許可權過濾過濾器定義 -->
<bean name="shiroFilterChainDefinitions" class="java.lang.String">
    <constructor-arg>
        <value>
            /static/** = anon
            /userfiles/** = anon
            ${adminPath}/login = authc
            ${adminPath}/logout = logout
            ${adminPath}/** = user
        </value>
    </constructor-arg>
</bean>

<!-- 安全認證過濾器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <property name="securityManager" ref="securityManager" /><!--
    <property name="loginUrl" value="${adminPath}/login" />
    <property name="successUrl" value="${adminPath}?login" />
    <property name="filters">
        <map>
            <entry key="cas" value-ref="casFilter"/>
            <entry key="authc" value-ref="formAuthenticationFilter"/>
        </map>
    </property>
    <property name="filterChainDefinitions">
        <ref bean="shiroFilterChainDefinitions"/>
    </property>
</bean>


<!-- 定義Shiro安全管理配置 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="realm" ref="systemAuthorizingRealm" />
    <property name="sessionManager" ref="sessionManager" />
    <property name="cacheManager" ref="shiroCacheManager" />
</bean>

<!-- 自定義會話管理配置 -->
<bean id="sessionManager" class="com.yonyou.hotusm.common.security.session.SessionManager"> 
    <property name="sessionDAO" ref="sessionDAO"/>
    
    <!-- 會話超時時間,單位:毫秒  -->
    <property name="globalSessionTimeout" value="${session.sessionTimeout}"/>
    
    <!-- 定時清理失效會話, 清理使用者直接關閉瀏覽器造成的孤立會話   -->
    <property name="sessionValidationInterval" value="${session.sessionTimeoutClean}"/>
     <property name="sessionValidationSchedulerEnabled" value="true"/>
     
    <property name="sessionIdCookie" ref="sessionIdCookie"/>
    <property name="sessionIdCookieEnabled" value="true"/>
</bean>

<!-- 指定本系統SESSIONID, 預設為: JSESSIONID 問題: 與SERVLET容器名衝突, 如JETTY, TOMCAT 等預設JSESSIONID,
    當跳出SHIRO SERVLET時如ERROR-PAGE容器會為JSESSIONID重新分配值導致登入會話丟失! -->
<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
    <constructor-arg name="name" value="hotusm.session.id"/>
</bean>

<bean id="sessionDAO" class="com.yonyou.hotusm.common.security.session.CacheSessionDAO">
    <property name="sessionIdGenerator" ref="idGen" />
    <property name="activeSessionsCacheName" value="activeSessionsCache" />
    <property name="cacheManager" ref="shiroCacheManager" />
</bean>

<!-- 定義授權快取管理器 -->
<bean id="shiroCacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
    <property name="cacheManager" ref="cacheManager"/>
</bean>

<!-- 保證實現了Shiro內部lifecycle函式的bean執行 -->
<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>
複製程式碼 這裡從上往下進行解釋: 1.shiroFilterChainDefinitions

可以看到型別是String,String內部的各個字串是使用"\n\t"進行換行。這裡的每一行代表了一個路由,而後面的anno,user等等,也就是相對應的Filter(這塊我們是可以自己定義的,後面會講,${adminPath} 是我在配置檔案裡面配置的路徑而已,完全可以根據自己的路由進行設定。shiroFilterChainDefinitions最主要是在shiroFilter中作為一個引數注入。

=許可權過濾器及配置釋義=========

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 anon org.apache.shiro.web.filter.authc.AnonymousFilter

authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter

authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter

perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter

port org.apache.shiro.web.filter.authz.PortFilter

rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter

roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter

ssl org.apache.shiro.web.filter.authz.SslFilter

user org.apache.shiro.web.filter.authc.UserFilter

logout org.apache.shiro.web.filter.authc.LogoutFilter anon:例子/admins/**=anon 沒有引數,表示可以匿名使用。

authc:例如/admins/user/**=authc表示需要認證(登入)才能使用,沒有引數

roles:例子/admins/user/=roles[admin],引數可以寫多個,多個時必須加上引號,並且引數之間用逗號分割,當有多個引數時,例如admins/user/=roles[“admin,guest”],每個引數通過才算通過,相當於hasAllRoles()方法。

perms:例子/admins/user/=perms[user:add:*],引數可以寫多個,多個時必須加上引號,並且引數之間用逗號分割,例如/admins/user/=perms[“user:add:,user:modify:”],當有多個引數時必須每個引數都通過才通過,想當於isPermitedAll()方法。

rest:例子/admins/user/=rest[user],根據請求的方法,相當於/admins/user/=perms[user:method] ,其中method為post,get,delete等。

port:例子/admins/user/**=port[8081],當請求的url的埠不是8081是跳轉到schemal://serverName:8081?queryString,其中schmal是協議http或https等,serverName是你訪問的host,8081是url配置裡port的埠,queryString

是你訪問的url裡的?後面的引數。

authcBasic:例如/admins/user/**=authcBasic沒有引數表示httpBasic認證

ssl:例子/admins/user/**=ssl沒有引數,表示安全的url請求,協議為https

user:例如/admins/user/**=user沒有引數表示必須存在使用者,當登入操作時不做檢查

2.重點來了:shiroFilter(ShiroFilterFactoryBean),這裡要非常小心!! 這裡的bean的名字一定要和web.xml裡面的那個Filter名字相同,具體可以見下面的原始碼:

複製程式碼 DelegatingFilterProxy.java: @Override protected void initFilterBean() throws ServletException { synchronized (this.delegateMonitor) { if (this.delegate == null) { // If no target bean name specified, use filter name. if (this.targetBeanName == null) { this.targetBeanName = getFilterName(); } // Fetch Spring root application context and initialize the delegate early, // if possible. If the root application context will be started after this // filter proxy, we’ll have to resort to lazy initialization. WebApplicationContext wac = findWebApplicationContext(); if (wac != null) { this.delegate = initDelegate(wac); } } } } 複製程式碼 還記得我們web.xml裡面配置的那個Filter嗎, 其實我們配置的Filter只不過是起到一個代理的作用,那麼它代理誰呢? 它也不能知道,它所能做的就是根據targetBeanName去容器中獲取bean(這個bean是實現了Filter介面的),其中的targetBeanName就是bean的名稱,如果沒有設定的話,那麼就預設使用的Filter名稱。所以說前面說過的必須相同是不正確的,你只需要在Filter中設定targetBeanName和spring-context-shiro配置檔案中ShiroFilterFactoryBean的bean名稱一樣即可。

除了上面需要注意的幾個點之外,ShiroFilterFactoryBean還有一些屬性:unauthorizedUrl,系統未認證時跳轉的頁面,loginUrl登入頁面,successUrl登入成功的頁面,filter屬性就是和前面的shiroFilterChainDefinitions對應的。同時支援自定義,並且配置路由:像這樣的。最底層是過濾器,下面是我實現的一個filter:

複製程式碼 package com.yonyou.kms.common.security.shiro.session;

import java.io.PrintWriter;

import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;

import org.apache.shiro.web.servlet.AdviceFilter;

import com.yonyou.kms.modules.sys.security.SystemAuthorizingRealm.Principal; import com.yonyou.kms.modules.sys.utils.UserUtils;

/** *

  • 自定義filter
  • @author Hotusm

*/ public class SessionOutDateFilter extends AdviceFilter{

private String redirectUrl="http://url/portal";//session 失效之後需要跳轉的頁面
private String platformUrl="http://url/kms/a/login";
//排除這個連結 其他的連結都會進行攔截
private String loginUrl="/kms/a/login";
private String frontUrl="cms/f";
private String uploadUrl="cms/article/plupload";
private String appUrl="a/app";

protected boolean preHandle(ServletRequest request, ServletResponse response){
    Principal principal = UserUtils.getPrincipal();
    HttpServletRequest req=(HttpServletRequest) request;
    String uri=req.getRequestURI();
    if(checkUrl(uri, loginUrl,frontUrl,uploadUrl,appUrl)|(principal!=null&&!principal.isMobileLogin())){
        
        return true;
    }
    
    try {
        issueRedirect(request,response,redirectUrl);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return false;
}


  protected void issueRedirect(ServletRequest request, ServletResponse response, String redirectUrl)
     throws Exception
  {      
      
      String url="<a href="+redirectUrl+" target=\"_blank\" onclick=\"custom_close()\">重新登入<a/> ";
      String platform="<a href="+platformUrl+" target=\"_blank\" onclick=\"custom_close()\">直接登入<a/> ";
      
      HttpServletResponse resp=(HttpServletResponse) response;
      HttpServletRequest req=(HttpServletRequest) request;
      response.setContentType("text/html;charset=UTF-8");
      PrintWriter out=resp.getWriter();
      out.print("<script language='javascript'>");
      out.print("function custom_close(){" +
                  "self.opener=null;" +
                  "self.close();}");
      out.print("</script>");
      out.print("沒有許可權或者驗證資訊過期,請點選"+url+"登入portal<br/>");
      out.print("直接登入"+platform);
  }


  public String getRedirectUrl() {
    return redirectUrl;
}


public void setRedirectUrl(String redirectUrl) {
    this.redirectUrl = redirectUrl;
}


public String getLoginUrl() {
    return loginUrl;
}


public void setLoginUrl(String loginUrl) {
    this.loginUrl = loginUrl;
}

/**
 * 排除一些url不進行攔截
 * @param targetUrl
 * @param urls
 * @return
 */
private boolean checkUrl(String targetUrl,String ...urls){
    for(int i=0;i<urls.length;i++){
        if(targetUrl.contains(urls[i])){
            return true;
        }
    }
    
    return false;
}

}


這個和springmvc的攔截器是相同的用法,返回true則表示驗證通過(後面的邏輯繼續執行),返回false就表示驗證不通過。

最後在shiroFilter的filters進行配置我們自定義的bean:

<property name="filters">
            <map>
                <entry key="outdate" value-ref="sessionOutDateFilter"/>
            </map>
        </property>
這個sessionOutDateFilter我們需要注入(這裡省略)。最後我們就將可以將這些東西加到shiroFilterChainDefinitions中去:
複製程式碼
    <bean name="shiroFilterChainDefinitions" class="java.lang.String">
        <constructor-arg>
            <value>
                              ......
                ${adminPath}/** = outdate
                              .....
            </value>
        </constructor-arg>
    </bean>        
複製程式碼
這樣我們自己定義的叫做outdata的路由會攔截${adminPath}下的所以路徑,並且進行驗證。

3.

SecurityManager

它和我們前面講的ShiroFilterFactoryBean的關係形象的將就是ShiroFilterFactoryBean是一個路由規則配置倉庫和代理類,其實真正的邏輯都是在SecurityManager中進行的,下面來進行詳講SecurityManager的依賴類。

一:realm:域,Shiro從從Realm獲取安全資料(如使用者、角色、許可權),就是說SecurityManager要驗證使用者身份,那麼它需要從Realm獲取相應的使用者進行比較以確定使用者身份是否合法;也需要從Realm得到使用者相應的角色/許可權進行驗證使用者是否能進行操作;可以把Realm看成DataSource,即安全資料來源,下面是我重寫的realm:

package com.yonyou.hotusm.module.sys.security;

import com.yonyou.hotusm.common.utils.Encodes; import com.yonyou.hotusm.module.sys.dao.UserDao; import com.yonyou.hotusm.module.sys.entity.User; import com.yonyou.hotusm.module.sys.service.UserService; import com.yonyou.hotusm.module.sys.util.UserUtils; import org.apache.shiro.authc.*; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.authz.UnauthenticatedException; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service;

import java.io.Serializable;

@Service(“systemAuthorizingRealm”) public class SystemAuthorizingRealm extends AuthorizingRealm implements InitializingBean{

@Autowired
private UserDao userDao;

@Override
protected AuthorizationInfo doGetAuthorizationInfo(
        PrincipalCollection principals) {
    SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
    info.addStringPermission("sys:manager");
    info.addStringPermission("user");
    System.out.println("開始授權");
    return info;
}

@Override
protected AuthenticationInfo doGetAuthenticationInfo(
        AuthenticationToken token) throws AuthenticationException {
    UsernamePasswordToken upToken=(UsernamePasswordToken) token; 
    String username=upToken.getUsername();
    User user=new User();
    user.setLoginName(username);
    user=userDao.get(user);
    
    if(user!=null){
        byte[] salt = Encodes.decodeHex(user.getPassword().substring(0,16));
        return new SimpleAuthenticationInfo(username, 
            user.getPassword().substring(16), ByteSource.Util.bytes(salt), getName());
    }else{
        throw new UnauthenticatedException();
    }
}

public static class Principal implements Serializable {

    private static final long serialVersionUID = 1L;
    
    private String id; // 編號
    private String loginName; // 登入名
    private String name; // 姓名
    

    public Principal(User user) {
        this.id = user.getId();
        this.loginName = user.getLoginName();
        this.name = user.getName();
    }

    public String getId() {
        return id;
    }

    public String getLoginName() {
        return loginName;
    }

    public String getName() {
        return name;
    }


    /**
     * 獲取SESSIONID
     */
    public String getSessionid() {
        try{
            return (String) UserUtils.getSession().getId();
        }catch (Exception e) {
            return "";
        }
    }
    
    @Override
    public String toString() {
        return id;
    }

}
//在bean初始化完成以後  設定校驗的規則
public void afterPropertiesSet() throws Exception {
    HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(UserService.HASH_ALGORITHM);
    matcher.setHashIterations(UserService.HASH_INTERATIONS);
    setCredentialsMatcher(matcher);
    
}

}

其他的幾個類在這裡不是重點,重要的是看這裡面的邏輯。其中最重要的是doGetAuthorizationInfo和doGetAuthenticationInfo以及afterPropertiesSet這三個方法,doGetAuthorizationInfo是對當前的使用者進行授權的,至於授權的時期,就是當用戶需要驗證的時候(框架進行回撥),我這裡只是簡單的寫死了,但是在實際專案開發中,我們一般會將許可權存放在資料表中,所以真實情況是先到資料庫中查出一個集合,然後迭代授權。

    doGetAuthenticationInfo對於的是對使用者驗證,主要的一個點在於我們最後返回的那個SimpleAuthenticationInfo,這個是加密的策略,這裡的密碼是密文的(根據loginName資料中取得),下面是密碼的加密策略:

//為明文密碼加密
    public String encryptionPassword(String plainPassword){
        byte[] salt = Digests.generateSalt(SALT_SIZE);  //SALT_SIZE=8
        byte[] hashPassword = Digests.sha1(plainPassword.getBytes(), salt, HASH_INTERATIONS);  //HASH_INTERATIONS=1024
return Encodes.encodeHex(salt)+Encodes.encodeHex(hashPassword); }
我這裡是生成了了16位的salt,然後用來加密明文,最後兩個加起來存入到資料中。根據上面說的,所以看到我們doGetAuthenticationInfo返回的是分開的兩部分。這裡也需要注意,這個密碼最後的校驗我們做的,而是框架!我們只是提供了校驗類供它回撥(下面我們使用的是預設的校驗類,我們也可以自定義):

    //在bean初始化完成以後  設定校驗的規則
    public void afterPropertiesSet() throws Exception {
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(UserService.HASH_ALGORITHM);
        matcher.setHashIterations(UserService.HASH_INTERATIONS);
        setCredentialsMatcher(matcher);
        
,那麼在密碼進行驗證的時候,就會呼叫HashedCredentialsMatcher的

    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        Object tokenHashedCredentials = hashProvidedCredentials(token, info);
        Object accountCredentials = getCredentials(info);
        return equals(tokenHashedCredentials, accountCredentials);
    }
方法,這個Info就是我們前面方法doGetAuthenticationInfo提供的,至於另外的一個Token,後面會講(也是一個方法提供的)。

4

下面就是講解SessionManager,因為Shiro有自己的一套session體系,有sessionManager就不奇怪了,sessionManager主要職責是管理session的建立和刪除,特別提一下,sessionManager對session的操作,其實只是呼叫了sessionDAO,然再加上自己的一些操作。

看原始碼:

public class DefaultSessionManager extends AbstractValidatingSessionManager implements CacheManagerAware {

//TODO - complete JavaDoc

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

private SessionFactory sessionFactory;

protected SessionDAO sessionDAO;  //todo - move SessionDAO up to AbstractValidatingSessionManager?

private CacheManager cacheManager;

private boolean deleteInvalidSessions;

public DefaultSessionManager() {
    this.deleteInvalidSessions = true;
    this.sessionFactory = new SimpleSessionFactory();
    this.sessionDAO = new MemorySessionDAO();
}

… protected void create(Session session) { if (log.isDebugEnabled()) { log.debug(“Creating new EIS record for new session instance [” + session + “]”); } sessionDAO.create(session); }


複製程式碼


這上面的原始碼中就知道SessionManager就是對SessionDAO進行了代理的作用。

我們就明白了sessionManager依賴sessionDAO(後面實現自己的SessionDAO需要注入到SessionManager中),下面是自己實現的sessionManager:

package com.yonyou.hotusm.common.security.session;

import org.apache.shiro.session.InvalidSessionException; import org.apache.shiro.session.Session; import org.apache.shiro.session.UnknownSessionException; import org.apache.shiro.session.mgt.SessionContext; import org.apache.shiro.session.mgt.SessionKey; import org.apache.shiro.session.mgt.SimpleSession; import org.apache.shiro.web.servlet.Cookie; import org.apache.shiro.web.servlet.ShiroHttpServletRequest; import org.apache.shiro.web.servlet.SimpleCookie; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.apache.shiro.web.util.WebUtils;

import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.Serializable; import java.util.Collection; import java.util.Date;

/*** *

  • @author Hotusm
  • v2015-11-04 */ public class SessionManager extends DefaultWebSessionManager{
/*
 *DefaultWebSessionManager 實現了DefaultSessionManager的功能 並在其上實現了web的功能
 * 也就是在上面實現了將SessionId 存到了Cookie中 
 * */
@Override
protected Serializable getSessionId(ServletRequest request,
        ServletResponse response) {
    
   
    String sid=request.getParameter("_sid");
    if(org.apache.commons.lang.StringUtils.isNotBlank(sid)){
        if(WebUtils.isTrue(request, "_cookie")){
            HttpServletRequest req=(HttpServletRequest) request;
            HttpServletResponse resp=(HttpServletResponse) response;
            Cookie template=getSessionIdCookie();
            Cookie cookie=new SimpleCookie(template);
            cookie.setValue(sid);
            cookie.saveTo(req, resp);
        }
        request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,ShiroHttpServletRequest.URL_SESSION_ID_SOURCE);
        request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sid);
        request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
        return sid;
    }
    
    return super.getSessionId(request, response);
}

@Override
protected Session doCreateSession(SessionContext context) {
    try {
        return super.doCreateSession(context);
    } catch (Exception e) {
        return null;
    }
    
}

@Override
protected Session newSessionInstance(SessionContext context) {
    
    Session session=super.newSessionInstance(context);
    session.setTimeout(getGlobalSessionTimeout());
    return session;
}

@Override
protected Session retrieveSession(SessionKey sessionKey)
        throws UnknownSessionException {
    
    try {
        return super.retrieveSession(sessionKey);
    } catch (Exception e) {
        //獲取不到SESSION不報錯
        return null;
    }
    
}

@Override
public void validateSessions() {
    super.validateSessions();
}

@Override
public Session start(SessionContext context) {
    try {
        return super.start(context);
    } catch (Exception e) {
        SimpleSession session=new SimpleSession();
        session.setId(0);
        return session;
    }
}

@Override
public Date getStartTimestamp(SessionKey key) {
    try {
        return super.getStartTimestamp(key);
    } catch (Exception e) {
        return null;
    }
    
}

@Override
public Date getLastAccessTime(SessionKey key) {
    try {
        return super.getLastAccessTime(key);
    } catch (Exception e) {
        return null;
    }
}

@Override
public long getTimeout(SessionKey key) throws InvalidSessionException {
    try {
        return super.getTimeout(key);
    } catch (Exception e) {
        return 0;
    }
    
}

@Override
public void setTimeout(SessionKey key, long maxIdleTimeInMillis)
        throws InvalidSessionException {
    try {
        super.setTimeout(key, maxIdleTimeInMillis);
    } catch (Exception e) {
        
    }
}

@Override
public void touch(SessionKey key) throws InvalidSessionException {
    try {
        super.touch(key);
    } catch (Exception e) {
    }
    
}

@Override
public String getHost(SessionKey key) {
    try {
        return super.getHost(key);
    } catch (Exception e) {
        return null;
    }
    
}

@Override
public Collection<Object> getAttributeKeys(SessionKey key) {
    try {
        return super.getAttributeKeys(key);
    } catch (Exception e) {
        return null;
    }
    
}

@Override
public Object getAttribute(SessionKey sessionKey, Object attributeKey)
        throws InvalidSessionException {
    try {
        return super.getAttribute(sessionKey, attributeKey);
    } catch (Exception e) {
        return null;
    }
    
}

@Override
public Object removeAttribute(SessionKey sessionKey, Object attributeKey)
        throws InvalidSessionException {
    try {
        return super.removeAttribute(sessionKey, attributeKey);
    } catch (Exception e) {
        return null;
    }

// }

@Override
public void stop(SessionKey key) throws InvalidSessionException {
    try {
        super.stop(key);
    } catch (Exception e) {
        
    }
    
}

@Override
public void checkValid(SessionKey key) throws InvalidSessionException {
    try {
        super.checkValid(key);
    } catch (Exception e) {
    }
} 

}


上面就是對session的操作.

步驟5

還有就是sessionDAO了,這個sessionDAO才是真正對session操作的bean:

package com.yonyou.hotusm.common.security.session;

import com.google.common.collect.Sets; import com.yonyou.hotusm.common.config.Global; import com.yonyou.hotusm.common.web.Servlets; import org.apache.shiro.session.Session; import org.apache.shiro.session.UnknownSessionException; import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;

import javax.servlet.http.HttpServletRequest; import java.io.Serializable; import java.util.Collection; import java.util.Set;

/**

  • @author Hotusm
  •     v-2015-10-28
    

*/ public class CacheSessionDAO extends EnterpriseCacheSessionDAO implements SessionDAO {

@Override
protected Serializable doCreate(Session session) {
    HttpServletRequest request = Servlets.getRequest();
    if (request != null) {
        String uri = request.getRequestURI();
        if (Servlets.isStaticFile(uri)) {
            return null;
        }
    }
    super.doCreate(session);
    //System.out.println("doCreate:"+" sessionId"+session.getId());
    return session.getId();
}

@Override
public Session readSession(Serializable sessionId) throws UnknownSessionException {
    //System.out.println("readSession:"+" sessionId"+sessionId);
    //System.out.println();
    try {
        Session s = null;
        HttpServletRequest request = Servlets.getRequest();
        if (request != null) {
            String uri = request.getRequestURI();
            if (Servlets.isStaticFile(uri)) {
                return null;
            }
            s = (Session) request.getAttribute("session_" + sessionId);
        }

        if (s != null) {
            return s;
        }
        Session session = super.readSession(sessionId);
        if (request != null && session != null) {
            request.setAttribute("session_" + sessionId, session);
        }
        return session;
    } catch (Exception e) {

        return null;
    }

}

@Override
protected Session doReadSession(Serializable sessionId) {
    //System.out.println("doReadSession:"+" sessionId"+sessionId);
    return super.doReadSession(sessionId);
}


@Override
protected void doUpdate(Session session) {

// System.out.println(“doUpdate”+" sessionId"+session.getId()); if (session == null || session.getId() == null) { return; } HttpServletRequest request = Servlets.getRequest(); if (request != null) { String uri = request.getRequestURI(); if (Servlets.isStaticFile(uri)) { return; } if (org.apache.commons.lang.StringUtils.startsWith(uri, Global.getConfig(“web.view.prefix”)) && org.apache.commons.lang.StringUtils.endsWith(uri, Global.getConfig(“web.view.suffix”))) { return; } //手動控制不更新session String updateSession = request.getParameter(“updateSession”); if (Global.FALSE.equals(updateSession) || Global.NO.equals(updateSession)) { return; } } super.doUpdate(session); }

@Override
protected void doDelete(Session session) {
    //System.out.println("doDelete");
    if (session == null || session.getId() == null) {
        return;
    }
    super.doUpdate(session);
}


public Collection<Session> getActiveSessions(boolean includeLeave) {

    return null;
}

public Collection<Session> getActiveSessions(boolean includeLeave,
                                             Object principal, Session filterSession) {

    if (includeLeave && principal == null) {
        return this.getActiveSessions();
    }
    Set<Session> sessions = Sets.newHashSet();
    for (Session session : getActiveSessions()) {
        boolean isActiveSession = true;

    }
    return null;
}

}


6.

看sessionDAO還有一個idGen依賴bean,指的是sessionId值的生成策略,這個bean也是自己定義的,但是需要繼承SessionIdGenerator:

public class IdGen implements SessionIdGenerator{

private static SecureRandom secureRandom;

/**
 * 封裝JDK自帶的UUID,通過random生成
 */
public static String uuid(){
    return UUID.randomUUID().toString().replace("-", "");
}

public static long randomLong(){
    return Math.abs(secureRandom.nextLong());
}

public Serializable generateId(Session session) {
    return IdGen.uuid();
}

}

返回的就是session的值,至於shiroCacheManager就是session快取的儲存位置(它依賴的是我們在spring-context定義的cacheManager)。

3.需要注意一點是formAuthenticationFilter是登陸以後,身份驗證的入口,但是隻攔截POST方式的loginUrl,就是我們前面配置的那個url,成功以後會跳到我們配置的那個成功頁面,一般我們都是設定一個虛擬路徑,然後在controller跳轉頁面:

/** * 登入成功,進入管理首頁 */ @RequiresPermissions(“user”) @RequestMapping(value = “${adminPath}”) public String index(HttpServletRequest request, HttpServletResponse response) { Principal principal = UserUtils.getPrincipal(); List str=commentService.commentList(null); //System.out.println(JsonMapper.toJsonString(str)); // 登入成功後,驗證碼計算器清零 isValidateCodeLogin(principal.getLoginName(), false, true);

    if (logger.isDebugEnabled()){
        
        logger.debug("show index, active session size: {}", sessionDAO.getActiveSessions(false).size());
    }
    
    // 如果已登入,再次訪問主頁,則退出原賬號。
    if (Global.TRUE.equals(Global.getConfig("notAllowRefreshIndex"))){
        
        String logined = CookieUtils.getCookie(request, "LOGINED");
        if (org.apache.commons.lang3.StringUtils.isBlank(logined) || "false".equals(logined)){
            
            CookieUtils.setCookie(response, "LOGINED", "true");
        }else if (org.apache.commons.lang3.StringUtils.equals(logined, "true")){
            UserUtils.getSubject().logout();
            
            return "redirect:" + adminPath + "/login";
        }
    }

/ return “modules/sys/sysIndex”; }


 

下面是authc對應的那個filter的程式碼,
@Service
public class FormAuthenticationFilter extends org.apache.shiro.web.filter.authc.FormAuthenticationFilter {

    public static final String DEFAULT_CAPTCHA_PARAM = "validateCode";
    public static final String DEFAULT_MOBILE_PARAM = "mobileLogin";
    public static final String DEFAULT_MESSAGE_PARAM = "message";

    private String captchaParam = DEFAULT_CAPTCHA_PARAM;
    private String mobileLoginParam = DEFAULT_MOBILE_PARAM;
    private String messageParam = DEFAULT_MESSAGE_PARAM;
    

    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
        
        String username = getUsername(request);
        String password = getPassword(request);boolean rememberMe = isRememberMe(request);

        String host = StringUtils.getRemoteAddr((HttpServletRequest)request);
        boolean mobile = isMobileLogin(request);
        return new UsernamePasswordToken(username, password.toCharArray(), rememberMe, host, mobile);
        //end
    }

    public String getCaptchaParam() {
        return captchaParam;
    }

    protected String getCaptcha(ServletRequest request) {
        return WebUtils.getCleanParam(request, getCaptchaParam());
    }

    public String getMobileLoginParam() {
        return mobileLoginParam;
    }
    
    protected boolean isMobileLogin(ServletRequest request) {
        return WebUtils.isTrue(request, getMobileLoginParam());
    }
    
    public String getMessageParam() {
        return messageParam;
    }
    
    /**
     * 登入成功之後跳轉URL
     */
    @Override
    public String getSuccessUrl() {
        return super.getSuccessUrl();
    }
    
    @Override
    protected void issueSuccessRedirect(ServletRequest request,
            ServletResponse response) throws Exception {
//        Principal p = UserUtils.getPrincipal();
//        if (p != null && !p.isMobileLogin()){
             WebUtils.issueRedirect(request, response, getSuccessUrl(), null, true);
//        }else{
//            super.issueSuccessRedirect(request, response);
//        }
    }

    /**
     * 登入失敗呼叫事件
     */
    @Override
    protected boolean onLoginFailure(AuthenticationToken token,
            AuthenticationException e, ServletRequest request, ServletResponse response) {
        String className = e.getClass().getName(), message = "";
        if (IncorrectCredentialsException.class.getName().equals(className)
                || UnknownAccountException.class.getName().equals(className)){
            message = "使用者或密碼錯誤, 請重試.";
        }
        else if (e.getMessage() != null && org.apache.commons.lang3.StringUtils.startsWith(e.getMessage(), "msg:")){
            message = org.apache.commons.lang3.StringUtils.replace(e.getMessage(), "msg:", "");
        }
        else{
            message = "系統出現點問題,請稍後再試!";
            e.printStackTrace(); // 輸出到控制檯
        }
        request.setAttribute(getFailureKeyAttribute(), className);
        request.setAttribute(getMessageParam(), message);
        return true;
    }
    
}

這裡的Token就是我們前面所講的Info一起來做明文和密文進行校驗的。

經過上面的一些操作,shiro登入和授權就可以做好了,對於退出,我們只要設定退出按鈕的連結地址是我們前面filterChainDefinitions配置的路徑就可以了,我的是: ${adminPath}/logout = logout;