1. 程式人生 > >CAS單點登入系列之極速入門與實戰教程(4.2.7)

CAS單點登入系列之極速入門與實戰教程(4.2.7)

@

目錄
  • 一、 SSO簡介
    • 1.1 單點登入定義
    • 1.2 單點登入角色
    • 1.3 單點登入分類
  • 二、 CAS簡介
    • 2.1 CAS簡單定義
    • 2.2 CAS體系結構
    • 2.3 CAS原理
  • 三、CAS服務端搭建
    • 3.1 CAS支援Http登入配置
    • 3.2 CAS服務端部署執行
  • 四、CAS客戶端接入
  • 五、客戶端極速接入

一、 SSO簡介

1.1 單點登入定義

單點登入(Single sign on),英文名稱縮寫SSO,SSO的意思就是在多系統的環境中,登入單方系統,就可以在不用再次登入的情況下訪問相關受信任的系統。也就是說只要登入一次單體系統就可以。

1.2 單點登入角色

單點登入一般包括下面三種角色:

①使用者(多個);

②認證中心(一個);

③Web應用(多個)。

PS:這裡所說的web應用可以理解為SSO Client,認證中心可以說是SSO Server。

1.3 單點登入分類

因為http協議是無狀態的協議,所以要保持登入狀態,必須要儲存登入資訊,按照儲存方式,單點登入實現方式主要可以分為兩種。

  • 一種是基於Cookie的,這種比較常見,比如下文介紹的CAS也是基於Cookie的;
  • 另外一種是基於Session的,其實理解起來就是會話共享,只有實現不同子系統之間的會話共享就能實現單點登入,詳情可以參考我之前的部落格,就是實現會話共享實現單點登入的,連結

二、 CAS簡介

2.1 CAS簡單定義

CAS(Center Authentication Service)是耶魯大學研究的一款開源的單點登入專案,主要為web專案提供單點登入實現,屬於Web SSO。

  • CAS Github連結
  • Apereo CAS官方網站

2.2 CAS體系結構

CAS體系結構分為CAS Server和CAS Client。

CAS Server就是Cas開源的,需要去github下載,然後進行修改;Cas Client
可以是App或者web端的或者PC端,CAS支援多種開發語言,java、php、C#等等


PS:圖來自官網,這裡簡單介紹一下,從圖可以看出,CAS支援多種方式的認證,一種是LDAP的、比較常見的資料庫Database的JDBC,還有Active Directory等等;支援的協議有Custom Protocol 、 CAS 、 OAuth 、 OpenID 、 RESTful API 、 SAML1.1 、 SAML2.0 等

2.3 CAS原理

下面給出一張來自CAS官方的圖片

CAS登入等系統分為CAS Server和CAS Client,下面,我根據我的理解稍微解釋一下:

1、使用者訪問CAS Client請求資源

2、客戶端程式做了重定向,重定向到CAS Server

3、CAS Server會對請求做認證,驗證是否有TGC(Ticket Granted Cookie,有TGC說明已經登入過,不需要再登入,沒有就返回登入頁面

4、認證通過後會生成一個Service Ticket返回Cas Client,客戶端進行Ticket快取,一般放在Cookie裡,我們稱之為TGC(Ticket Granted Cookie)

5、然後Cas Client就帶著Ticket再次訪問Cas Server,CAS Server進行Ticket驗證

6、CAS Server對Ticket進行驗證,通過就返回使用者資訊,使用者拿到資訊後就可以登入

看到這個過程,我們大概就能理解CAS是怎麼實現的,看起來過程挺多的,不過這些過程都是CAS在後臺做的。CAS Service和CAS Client通訊基於HttpUrlConnection

注意要點:

  • TGT(Ticket Granded Ticket),就是儲存認證憑據的Cookie,有TGT說明已經通過認證
  • ST(Service Ticket),是由CAS認證中心生成的一個唯一的不可偽裝的票據,用於認證的
  • 沒登入過的或者TGT失效的,訪問時候也跳轉到認證中心,發現沒有TGT,說明沒有通過認證,直接重定向登入頁面,輸入賬號密碼後,再次重定向到認證中心,驗證通過後,生成ST,返回客戶端儲存到TGC
  • 登入過的而且TGT沒有失效的,直接帶著去認證中心認證,認證中心發現有TGT,重定向到客戶端,並且帶上ST,客戶端再帶ST去認證中心驗證

三、CAS服務端搭建

3.1 CAS支援Http登入配置

CAS預設是要https的連結才能登入的,不過學習的話是可以先驅動https限制,本部落格介紹的是基於Cas4.2.7的,之前改過4.0的,詳情見https://blog.csdn.net/u014427391/article/details/82083995

Cas4.2.7和4.0的修改是不一樣的,Cas4.2.7版本需要自己編譯,是基於Gradle的,不是基於Maven的,覺得麻煩可以下載4.0,因為4.0版本有提供war包,不需要自己編譯,下面介紹一下4.2.7版本,怎麼支援http登入

需要修改cas4.2.7的cas-server-webapp/WEB-INF/cas.properties,全都改為非安全的

tgc.secure=false
warn.cookie.secure=false

cas-server-webapp/resources/service/HTTPSandIMAPS-10000001.json原來的

"serviceId" : "^(https|imaps)://.*"

加上http

"serviceId" : "^(https|imaps|http)://.*"

註釋cas-server-webapp/WEB-INF/view/jsp/default/ui/casLoginView.jsp頁面中校驗是否是HTTPS協議的標籤塊

<c:if test="${not pageContext.request.secure}">
    <div id="msg" class="errors">
        <h2><spring:message code="screen.nonsecure.title" /></h2>
        <p><spring:message code="screen.nonsecure.message" /></p>
    </div>
</c:if>

然後登入就沒非安全提示了

3.2 CAS服務端部署執行

然後將war包丟在Tomcat的webapp裡,部署啟動,預設賬號密碼casuser/Mellon,cas4.2.7的賬號密碼是寫在cas.properties裡的,這個和4.0的不一樣

accept.authn.users=casuser::Mellon

登入成功,當然在專案中,肯定不能這樣做,這個需要我們配置jdbc或者加上許可權校驗等等

單點登出,連結是http://127.0.0.1:8080/cas/logout

四、CAS客戶端接入

本部落格介紹一下基於SpringBoot的Cas客戶端接入,資料庫採用mysql,許可權控制採用Shiro

maven配置,加上Shiro和CAS的相關jar:

CAS和Shiro的相關版本

<properties>
        <shiro.version>1.2.3</shiro.version>
        <shiro.spring.version>1.2.4</shiro.spring.version>
        <shiro.encache.version>1.2.4</shiro.encache.version>
        <cas.version>3.2.0</cas.version>
        <shiro.cas.version>1.2.4</shiro.cas.version>
    </properties>

加上Shiro和cas相關jar

 <!-- Shiro -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>${shiro.spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-ehcache</artifactId>
            <version>${shiro.encache.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-cas</artifactId>
            <version>${shiro.cas.version}</version>
        </dependency>

		<!-- cas -->
        <dependency>
            <groupId>org.jasig.cas.client</groupId>
            <artifactId>cas-client-core</artifactId>
            <version>${cas.version}</version>
        </dependency>

新建一個環境類,儲存cas的一些配置連結:

package org.muses.jeeplatform.core;

/**
 * <pre>
 *  CAS配置環境類
 * </pre>
 *
 * @author nicky.ma
 * <pre>
 * 修改記錄
 *    修改後版本:     修改人:  修改日期: 2019年05月25日  修改內容:
 * </pre>
 */
public class CASConsts {

    /* CAS單點登入配置 */
    //Cas server地址
    public static final String CAS_SERVER_URL_PREFIX = "http://localhost:8080/cas";
    //Cas單點登入地址
    public static final String CAS_LOGIN_URL = CAS_SERVER_URL_PREFIX +"/login";
    //CAS單點登出地址
    public static final String CAS_LOGOUT_URL = CAS_SERVER_URL_PREFIX + "/logout";
    //對外提供的服務地址
    public static final String SERVER_URL_PREFIX = "http://localhost:8081";
    //Cas過濾器的urlPattern
    public static final String CAS_FILTER_URL_PATTERN = "/jeeplatform";
    //CAS客戶端單點登入跳轉地址
    public static final String CAS_CLIENT_LOGIN_URL = CAS_LOGIN_URL + "?service="+SERVER_URL_PREFIX+CAS_FILTER_URL_PATTERN;
    //CAS客戶端單點登出
    public static final String CAS_CLIENT_LOGOUT_URL = CAS_LOGOUT_URL + "?service="+SERVER_URL_PREFIX+CAS_FILTER_URL_PATTERN;
    //登入成功地址
    public static final String LOGIN_SUCCESS_URL = "/index";
    //無權訪問頁面403
    public static final String LOGIN_UNAUTHORIZED_URL = "/403";

}

ShiroCas配置類:

package org.muses.jeeplatform.config;

import org.apache.shiro.cas.CasFilter;
import org.apache.shiro.cas.CasSubjectFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.jasig.cas.client.authentication.AuthenticationFilter;
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.session.SingleSignOutHttpSessionListener;
import org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter;
import org.muses.jeeplatform.core.shiro.ShiroRealm;
import org.muses.jeeplatform.web.filter.SysAccessControllerFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

import static org.muses.jeeplatform.core.CASConsts.*;

/**
 * @author nicky.ma
 */
@Configuration
public class ShiroConfig {

    private static final Logger LOG = LoggerFactory.getLogger(ShiroConfig.class);

    /**
     *  單點登出監聽器
     * @return
     */
    @Bean
    public ServletListenerRegistrationBean singleSignOutHttpSeessionListener(){
        ServletListenerRegistrationBean bean = new ServletListenerRegistrationBean();
        bean.setListener(new SingleSignOutHttpSessionListener());
        bean.setEnabled(true);
        return bean;
    }

    /**
     * 註冊單點登出的過濾器
     * @return
     */
    @Bean
    public FilterRegistrationBean singleSignOutFilter(){
        FilterRegistrationBean bean = new FilterRegistrationBean();
        bean.setName("singleSignOutFilter");
        bean.setFilter(new SingleSignOutFilter());
        bean.addUrlPatterns("/*");
        bean.setEnabled(true);
        return bean;
    }

    @Bean
    public FilterRegistrationBean authenticationFilter(){
        FilterRegistrationBean bean = new FilterRegistrationBean();
        bean.setFilter(new AuthenticationFilter());
        bean.addUrlPatterns("/*");
        bean.setName("CAS AuthenticationFilter");
        bean.addInitParameter("casServerLoginUrl",CAS_SERVER_URL_PREFIX);
        bean.addInitParameter("serverName",SERVER_URL_PREFIX);
        return bean;
    }

    /**
     * 單點登入校驗
     * @return
     */
    @Bean
    public FilterRegistrationBean validationFilter(){
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(new Cas20ProxyReceivingTicketValidationFilter());
        registrationBean.addUrlPatterns("/*");
        registrationBean.setName("CAS Validation Filter");
        registrationBean.addInitParameter("casServerUrlPrefix", CAS_SERVER_URL_PREFIX );
        registrationBean.addInitParameter("serverName", SERVER_URL_PREFIX );
        return registrationBean;
    }


    /**
     * CAS過濾器
     * @return
     */
    @Bean
    public CasFilter getCasFilter(){
        CasFilter casFilter = new CasFilter();
        casFilter.setName("casFilter");
        casFilter.setEnabled(true);
        casFilter.setFailureUrl(CAS_CLIENT_LOGIN_URL);
        casFilter.setSuccessUrl(LOGIN_SUCCESS_URL);
        return casFilter;
    }

    /**
     * 定義ShrioRealm
     * @return
     */
    @Bean
    public ShiroRealm myShiroRealm(){
        ShiroRealm myShiroRealm = new ShiroRealm();
        return myShiroRealm;
    }

    /**
     * Shiro Security Manager
     * @return
     */
    @Bean
    public SecurityManager securityManager(){
        DefaultWebSecurityManager securityManager =  new DefaultWebSecurityManager();
        //securityManager.setRealm(myShiroRealm());
        securityManager.setSubjectFactory(new CasSubjectFactory());
        return securityManager;
    }

    /**
     * ShiroFilterFactoryBean
     * @param securityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager,CasFilter casFilter) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //註冊Shrio Security Manager
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        shiroFilterFactoryBean.setLoginUrl(CAS_CLIENT_LOGIN_URL);
        shiroFilterFactoryBean.setSuccessUrl(LOGIN_SUCCESS_URL);
        shiroFilterFactoryBean.setUnauthorizedUrl(LOGIN_UNAUTHORIZED_URL);

        //新增CasFilter到ShiroFilter
        Map<String,Filter> filters = new HashMap<String,Filter>();
        filters.put("casFilter",casFilter);
        shiroFilterFactoryBean.setFilters(filters);

        //攔截器.
        Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>();
        //Shiro整合CAS後需要新增該規則
        filterChainDefinitionMap.put(CAS_FILTER_URL_PATTERN,"casFilter");
        // 配置不會被攔截的連結 順序判斷
        filterChainDefinitionMap.put("/static/**", "anon");
        filterChainDefinitionMap.put("/upload/**", "anon");
        filterChainDefinitionMap.put("/plugins/**", "anon");
        filterChainDefinitionMap.put("/code", "anon");
        //filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/403", "anon");
        //filterChainDefinitionMap.put("/logincheck", "anon");
        filterChainDefinitionMap.put("/logout","anon");
        filterChainDefinitionMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        return shiroFilterFactoryBean;
    }

   
}

自定義的CasRealm:

package org.muses.jeeplatform.core.shiro;

import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.cas.CasRealm;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.muses.jeeplatform.core.entity.admin.User;
import org.muses.jeeplatform.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;

import static org.muses.jeeplatform.core.CASConsts.CAS_FILTER_URL_PATTERN;
import static org.muses.jeeplatform.core.CASConsts.CAS_SERVER_URL_PREFIX;

/**
 * @description 基於Shiro框架的許可權安全認證和授權
 * @author Nicky
 * @date 2017年3月12日
 */
public class ShiroRealm extends CasRealm {

	Logger LOG = LoggerFactory.getLogger(ShiroRealm.class);

	/**註解引入業務類**/
	@Resource
	UserService userService;

	@PostConstruct
	public void initProperty(){
		setCasServerUrlPrefix(CAS_SERVER_URL_PREFIX);
		//客戶端回撥地址
		setCasService(CAS_SERVER_URL_PREFIX + CAS_FILTER_URL_PATTERN);
	}
	
	/**
	 * 登入資訊和使用者驗證資訊驗證(non-Javadoc)
	 * @see org.apache.shiro.realm.AuthenticatingRealm#doGetAuthenticationInfo(AuthenticationToken)
	 */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

		if(LOG.isInfoEnabled()) {
			LOG.info("=>執行Shiro許可權認證");
		}

		String username = (String)token.getPrincipal();  				//得到使用者名稱
		String password = new String((char[])token.getCredentials()); 	//得到密碼
	     
		User user = userService.findByUsername(username);

		/* 檢測是否有此使用者 */
		if(user == null){
			throw new UnknownAccountException();//沒有找到賬號異常
		}
		/* 檢驗賬號是否被鎖定 */
		if(Boolean.TRUE.equals(user.getLocked())){
			throw new LockedAccountException();//丟擲賬號鎖定異常
		}
		/* AuthenticatingRealm使用CredentialsMatcher進行密碼匹配*/
		if(null != username && null != password){
			return new SimpleAuthenticationInfo(username, password, getName());
		}else{
	    	 return null;
		}

	}
	
	/**
	 * 授權查詢回撥函式, 進行鑑權但快取中無使用者的授權資訊時呼叫,負責在應用程式中決定使用者的訪問控制的方法(non-Javadoc)
	 * @see AuthorizingRealm#doGetAuthorizationInfo(PrincipalCollection)
	 */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection pc) {
		if(LOG.isInfoEnabled()) {
			LOG.info("=>執行Shiro授權");
		}
		String username = (String)pc.getPrimaryPrincipal();
		SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
	    authorizationInfo.setRoles(userService.getRoles(username));
	    authorizationInfo.setStringPermissions(userService.getPermissions(username));
	    return authorizationInfo;
	}
	
	 @Override
	 public void clearCachedAuthorizationInfo(PrincipalCollection principals) {
		 super.clearCachedAuthorizationInfo(principals);
	 }

	 @Override
	 public void clearCachedAuthenticationInfo(PrincipalCollection principals) {
	     super.clearCachedAuthenticationInfo(principals);
	 }

	 @Override
	 public void clearCache(PrincipalCollection principals) {
	      super.clearCache(principals);
	 }

}

五、客戶端極速接入

上面例子是比較麻煩的,我們要接入客戶端可以常用第三方的starter lib來對接

 <!-- CAS依賴包 -->
        <dependency>
            <groupId>net.unicon.cas</groupId>
            <artifactId>cas-client-autoconfig-support</artifactId>
            <version>1.5.0-GA</version>
        </dependency>

yaml配置:

cas:
  server-login-url: http://127.0.0.1:8080/cas/login
  server-url-prefix: http://127.0.0.1:8080/cas
  client-host-url: http://127.0.0.1:8081
  validation-type: cas
#  use-session: true

加個Springboot配置類:

package org.muses.jeeplatform.config;

import net.unicon.cas.client.configuration.CasClientConfigurerAdapter;
import net.unicon.cas.client.configuration.EnableCasClient;
import org.jasig.cas.client.authentication.AuthenticationFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

@Configuration
@EnableCasClient
public class CASConfig extends CasClientConfigurerAdapter {

    private static final String CAS_SERVER_URL_LOGIN = "http://localhost:8080/cas/login";
    private static final String SERVER_NAME = "http://localhost:8081/";


//    @Override
//    public void configureAuthenticationFilter(FilterRegistrationBean authenticationFilter) {
//        super.configureAuthenticationFilter(authenticationFilter);
//        //authenticationFilter.getInitParameters().put("authenticationRedirectStrategyClass","com.test.CustomAuthRedirectStrategy");
//    }

    @Bean
    public FilterRegistrationBean filterRegistrationBean(){
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(new AuthenticationFilter());
        registrationBean.addUrlPatterns("/*");
        Map<String, String> initParameters = new HashMap<String,String>(16);
        initParameters.put("casServerLoginUrl",CAS_SERVER_URL_LOGIN);
        initParameters.put("serverName",SERVER_NAME);
        initParameters.put("ignorePattern","/logoutSuccess/*");
        registrationBean.setOrder(1);
        return registrationBean;
    }


}