1. 程式人生 > >jeesite整合cas認證

jeesite整合cas認證

cas和shiro整合,很好的解決了登入及許可權問題。本人最近第一次使用,框架使用的是jeesite開源框架,本身已經集成了shiro,現在將cas整合到專案中。

折騰了三天,終於把cas整合到jeesite中。現將整合過程寫下,供朋友參考。

本專案整合cas的同時還留有登入入口,此時需要多種認證方式,步驟6、7的設定就是針對這個功能的,如不需要可直接跳過。

不做技術好多年了,專案時間緊只能親自上陣,寫的不周全的請多包涵。有問題望指教。

1、新增cas的maven依賴。

  <!-- CAS -->
  <dependency>
      <groupId>org.jasig.cas.client</groupId>
      <artifactId>cas-client-core</artifactId>
      <version>3.2.1</version>
  </dependency>

2、web.xml新增內容。

<!-- 該過濾器對HttpServletRequest請求包裝, 可通過HttpServletRequest的getRemoteUser()方法獲得登入使用者的登入名,可選 -->   
   

 <filter>    
        <filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>    
        <filter-class>    
            org.jasig.cas.client.util.HttpServletRequestWrapperFilter    
        </filter-class>    
    </filter>    
    <filter-mapping>    
        <filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>    
        <url-pattern>/*</url-pattern>    
    </filter-mapping>    
    <!-- 該過濾器使得可以通過org.jasig.cas.client.util.AssertionHolder來獲取使用者的登入名。    
         比如AssertionHolder.getAssertion().getPrincipal().getName()。     
         這個類把Assertion資訊放在ThreadLocal變數中,這樣應用程式不在web層也能夠獲取到當前登入資訊 -->    
    <filter>    
        <filter-name>CAS Assertion Thread Local Filter</filter-name>    
        <filter-class>org.jasig.cas.client.util.AssertionThreadLocalFilter</filter-class>   
    </filter>    
    <filter-mapping>    
        <filter-name>CAS Assertion Thread Local Filter</filter-name>    
        <url-pattern>/*</url-pattern>    
    </filter-mapping> 

   

3、配置spring-content-shiro.xml。

整體xml內容如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="
  http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans-4.1.xsd
  http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd"
 default-lazy-init="true">
 <description>Shiro Configuration</description>
    <!-- 載入配置屬性檔案 -->
 <context:property-placeholder ignore-unresolvable="true" location="classpath:project.properties" />
 
 <!-- Shiro許可權過濾過濾器定義 -->
 <bean name="shiroFilterChainDefinitions" class="java.lang.String">
  <constructor-arg>
   <value>    
    /static/** = anon
                /userfiles/** = anon
                ${adminPath}/login = authc
                ${adminPath}/logout = logout
                ${adminPath}/cas = cas
                ${adminPath}/** = user
   </value>
  </constructor-arg>
 </bean>
 
 <bean id="logout" class="org.apache.shiro.web.filter.authc.LogoutFilter">
          <property name="redirectUrl" 
        value="${cas.logout.url}"/>
 </bean>
 
 <!-- 安全認證過濾器 -->
 <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
  <property name="securityManager" ref="securityManager" /> 
  <property name="loginUrl" value="${cas.server.url}/login?service=${cas.project.url}${adminPath}/cas" />
  <!--<property name="loginUrl" value="${adminPath}/login" /> -->
  <property name="successUrl" value="${adminPath}" />
  <property name="filters">
            <map>
                <entry key="cas" value-ref="casFilter"/>
                <entry key="authc" value-ref="formAuthenticationFilter"/>
                <entry key="logout" value-ref="logout" />
            </map>
        </property>
  <property name="filterChainDefinitions">
   <ref bean="shiroFilterChainDefinitions"/>
  </property>
 </bean>
 
 <!-- cas和shiro結合  -->
 <bean id="casRealm" class="com.sinosoft.modules.sys.security.CasLoginRealm">
     <property name="casServerUrlPrefix" value="${cas.server.url}"></property>
     <property name="casService" value="${cas.project.url}${adminPath}/cas"></property>
   </bean>
   <!-- CAS認證過濾器 -->  
 <bean id="casFilter" class="org.apache.shiro.cas.CasFilter">  
  <property name="failureUrl" value="${adminPath}/login"/>
 </bean>
 <!-- 定義Shiro安全管理配置 -->
 <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
  <property name="realms">
   <list>
    <ref bean="casRealm"/>
    <ref bean="systemAuthorizingRealm"/>
   </list>
  </property>
  <property name="sessionManager" ref="sessionManager" />
  <property name="cacheManager" ref="shiroCacheManager" />
 </bean>
 
 <!-- 配置使用自定義認證器,可以實現多Realm認證,並且可以指定特定Realm處理特定型別的驗證 -->
    <bean id="authenticator" class="com.sinosoft.modules.sys.security.CustomizedModularRealmAuthenticator">
        <!-- 配置認證策略,只要有一個Realm認證成功即可,並且返回所有認證成功資訊 -->
        <property name="authenticationStrategy">
            <bean class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy"></bean>
        </property>
    </bean>
 
 <!-- 自定義會話管理配置 -->
 <bean id="sessionManager" class="com.sinosoft.common.security.shiro.session.SessionManager">
  <property name="sessionDAO" ref="sessionDAO"/>
  
  <!-- 會話超時時間,單位:毫秒  -->
  <property name="globalSessionTimeout" value="${session.sessionTimeout}"/>
  
  <!-- 定時清理失效會話, 清理使用者直接關閉瀏覽器造成的孤立會話   -->
  <property name="sessionValidationInterval" value="${session.sessionTimeoutClean}"/>
<!--    <property name="sessionValidationSchedulerEnabled" value="false"/> -->
   <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="jeesite.session.id"/>
 </bean>
 <!-- 自定義Session儲存容器 -->
<!--  <bean id="sessionDAO" class="com.sinosoft.common.security.shiro.session.JedisSessionDAO"> -->
<!--   <property name="sessionIdGenerator" ref="idGen" /> -->
<!--   <property name="sessionKeyPrefix" value="${redis.keyPrefix}_session_" /> -->
<!--  </bean> -->
 <bean id="sessionDAO" class="com.sinosoft.common.security.shiro.session.CacheSessionDAO">
  <property name="sessionIdGenerator" ref="idGen" />
  <property name="activeSessionsCacheName" value="activeSessionsCache" />
  <property name="cacheManager" ref="shiroCacheManager" />
 </bean>
 
 <!-- 自定義系統快取管理器-->
<!--  <bean id="shiroCacheManager" class="com.sinosoft.common.security.shiro.cache.JedisCacheManager"> -->
<!--   <property name="cacheKeyPrefix" value="${redis.keyPrefix}_cache_" /> -->
<!--  </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>
 
</beans>


4、project.properties中增加屬性url。

cas.server.url=http://localhost:8080/cas
cas.project.url=http://1ocalhost:8080/infoService
cas.logout.url=http://localhost:8080/cas/logout?server=http://localhost:8080/infoService

5、建立CasLoginRealm類,繼承CasRealm。

package com.sinosoft.modules.sys.security;
import java.util.Collection;  
import java.util.List;
import org.apache.commons.lang3.StringUtils;  
import org.apache.shiro.authc.AuthenticationException;  
import org.apache.shiro.authc.AuthenticationInfo;  
import org.apache.shiro.authc.AuthenticationToken;  
import org.apache.shiro.authc.SimpleAuthenticationInfo;  
import org.apache.shiro.authz.AuthorizationInfo;  
import org.apache.shiro.authz.SimpleAuthorizationInfo;  
import org.apache.shiro.cas.CasAuthenticationException;  
import org.apache.shiro.cas.CasRealm;  
import org.apache.shiro.cas.CasToken;  
import org.apache.shiro.session.Session;  
import org.apache.shiro.subject.PrincipalCollection;  
import org.apache.shiro.subject.SimplePrincipalCollection;  
import org.jasig.cas.client.authentication.AttributePrincipal;  
import org.jasig.cas.client.validation.Assertion;  
import org.jasig.cas.client.validation.TicketValidationException;  
import org.jasig.cas.client.validation.TicketValidator;  
import org.slf4j.Logger;  
import org.slf4j.LoggerFactory;  
import com.sinosoft.common.config.Global;  
import com.sinosoft.common.utils.SpringContextHolder;  
import com.sinosoft.common.web.Servlets;  
import com.sinosoft.modules.sys.entity.Menu;  
import com.sinosoft.modules.sys.entity.Role;  
import com.sinosoft.modules.sys.entity.User;  
import com.sinosoft.modules.sys.security.SystemAuthorizingRealm.Principal;  
import com.sinosoft.modules.sys.service.SystemService;  
import com.sinosoft.modules.sys.utils.LogUtils;  
import com.sinosoft.modules.sys.utils.UserUtils;  
/**
 * <p>Title: cas認證類</p>
 * <p>Description: cas認證操作類</p>
 * <p>Copyright: Copyright (c) 2017</p>
 * <p>Company: </p>
 * <p>Date: 2017-7-5</p>
 * @author holyspirit
 * @version 1.0
 */
public class CasLoginRealm extends CasRealm {
 private Logger logger = LoggerFactory.getLogger(getClass());
 private SystemService systemService;
 @Override
 protected AuthenticationInfo doGetAuthenticationInfo(
   AuthenticationToken token) throws AuthenticationException {
  // return super.doGetAuthenticationInfo(token);
  CasToken casToken = (CasToken) token;
  if (token == null) {
   return null;
  }
  // 獲取ticket
  String ticket = (String) casToken.getCredentials();
  if (!org.apache.shiro.util.StringUtils.hasText(ticket)) {
   return null;
  }
  TicketValidator ticketValidator = ensureTicketValidator();
  try {
   // 回傳ticket到服務端驗證,驗證通過就進入下一行,可以獲取登入後的相關資訊,否則直接拋異常,即驗證不通過
   Assertion casAssertion = ticketValidator.validate(ticket,
     getCasService());
   AttributePrincipal casPrincipal = casAssertion.getPrincipal();
   String userId = casPrincipal.getName();
   User user = getSystemService().getUserByLoginName(userId);
   if (user != null) {
    Principal p = new Principal(user, false);
    PrincipalCollection principalCollection = new SimplePrincipalCollection(
      p, getName());
    return new SimpleAuthenticationInfo(principalCollection, ticket);
   } else {
    return null;
   }
  } catch (TicketValidationException e) {
   logger.error("票據認證失敗", e);
   throw new CasAuthenticationException("Unable to validate ticket ["
     + ticket + "]", e);
  }
 }
 @Override
 protected AuthorizationInfo doGetAuthorizationInfo(
   PrincipalCollection principals) {
  Principal principal = (Principal) getAvailablePrincipal(principals);
  // 獲取當前已登入的使用者
  if (!Global.TRUE.equals(Global.getConfig("user.multiAccountLogin"))) {
   Collection<Session> sessions = getSystemService().getSessionDao()
     .getActiveSessions(true, principal, UserUtils.getSession());
   if (sessions.size() > 0) {
    // 如果是登入進來的,則踢出已線上使用者
    if (UserUtils.getSubject().isAuthenticated()) {
     for (Session session : sessions) {
      getSystemService().getSessionDao().delete(session);
     }
    }
    // 記住我進來的,並且當前使用者已登入,則退出當前使用者提示資訊。
    else {
     UserUtils.getSubject().logout();
     throw new AuthenticationException("msg:賬號已在其它地方登入,請重新登入。");
    }
   }
  }
  User user = getSystemService().getUserByLoginName(
    principal.getLoginName());
  if (user != null) {
   SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
   List<Menu> list = UserUtils.getMenuList();
   for (Menu menu : list) {
    if (StringUtils.isNotBlank(menu.getPermission())) {
     // 新增基於Permission的許可權資訊
     for (String permission : StringUtils.split(
       menu.getPermission(), ",")) {
      info.addStringPermission(permission);
     }
    }
   }
   // 新增使用者許可權
   info.addStringPermission("user");
   // 新增使用者角色資訊
   for (Role role : user.getRoleList()) {
    info.addRole(role.getEnname());
   }
   // 更新登入IP和時間
   getSystemService().updateUserLoginInfo(user);
   // 記錄登入日誌
   LogUtils.saveLog(Servlets.getRequest(), "系統登入");
   return info;
  } else {
   return null;
  }
 }
    /**
     * 
     * Title:獲取系統業務物件
     * Description:獲取系統業務物件
     * @param 
     * @return 
     * @Author:holyspirit
     * @Create Date: 2017-7-6
     * @Modifier:
     * @Modify Date:
     */
 public SystemService getSystemService() {
  if (systemService == null) {
   systemService = SpringContextHolder.getBean(SystemService.class);
  }
  return systemService;
 }
}


6、FormAuthenticationFilter類增加程式碼片段,重寫redirectToLogin方法。

@Override
 protected void redirectToLogin(ServletRequest request,
   ServletResponse response) throws IOException {
  setLoginUrl("/info/login");
  WebUtils.issueRedirect(request, response, getLoginUrl());
 }


7、新增CustomizedModularRealmAuthenticator類。

package com.sinosoft.modules.sys.security;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import java.util.Collection;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.cas.CasToken;
import org.apache.shiro.realm.Realm;
/**
 * <p>Title: 自定義Authenticator</p>
 * <p>Description:分別定義處理不同登入方式驗證的Realm</p>
 * <p>Copyright: Copyright (c) 2017</p>
 * <p>Company: </p>
 * <p>Date: 2017-7-6</p>
 * @author holyspirit
 * @version 1.0
 */
public class CustomizedModularRealmAuthenticator extends
  ModularRealmAuthenticator {
 @Override
 protected AuthenticationInfo doAuthenticate(
   AuthenticationToken authenticationToken)
   throws AuthenticationException {
  //獲取所有Realm
  Collection<Realm> realms = getRealms();
  Realm realm = null;
  //判斷當前token是castoken還是UsernamePasswordToken
  //強轉對應型別token
  //根據token型別獲得對應的Realm
  if(authenticationToken instanceof CasToken){
   authenticationToken = (CasToken)authenticationToken;
   realm = getRealm(realms, authenticationToken);
  }
  if(authenticationToken instanceof UsernamePasswordToken){
   authenticationToken = (UsernamePasswordToken)authenticationToken;
   realm = getRealm(realms, authenticationToken);
  }
  //執行響應Realm
  return doSingleRealmAuthentication(realm, authenticationToken);
 }
 
 //判斷當前token對應的Realm
 public Realm getRealm(Collection<Realm> realms, AuthenticationToken token){
  for (Realm realm : realms) {
   if(realm.supports(token)){
    return realm;
   }
  }
  return null;
 }
}


8、設定登出事件,退出cas認證同時清除系統session

<a href="${ctx}/logout" title="退出登入">退出</a>
問題集合:
1。中文亂碼問題,所有提交的請求中文變成亂碼,這是過濾器順序所致。cas過濾器和shiro過濾器要放在編碼過濾器後面。