# CAS标准版接入

# 4.1 背景

  • 项目名称:统一身份认证1.0
  • 项目开发:杭州半云科技有限公司 Auth007.png

# 4.2 对接说明

本文档详细的描述子应用系统通过接口接入“统一身份认证1.0”,解决传统认证机制中存在的一些问题,实现单点登陆(Singe Sign - On)。 本系统基于Apereo CAS的单点登录协议,并针对原有系统缺点进行了改进,使得认证和授权分离,并且减轻了认证服务器的负担,减少了网络传输,方便用户快速访问资源。本文档适用于应用系统研发人员阅读。
平台基于CAS6.6开发,只在原协议基础上扩展,完全兼容原协议。
开源项目地址:https://github.com/apereo/cas
官方在线文档:https://apereo.github.io/cas/6.6.x/index.html


官方实现客户端接入参考文档:https://apereo.atlassian.net/wiki/spaces/CASC/pages/103252551/Official+Clients
对接地址:http://server/cas (请向管理员索要)

# 4.3 协议说明 to do(文档接口完善)

协议标准文档:https://apereo.github.io/cas/6.6.x/protocol/CAS-Protocol-Specification.html#cas-protocol-30-specification

# 4.4 SDK下载

# 4.5 JAVA EE,JSP应用对接

# 4.5.1 拷贝统一身份认证客户端所需jar包到应用中

将CAS客户端所需的jar包casclient.jar、cas-client-core-3.2.1.jar拷贝到 projectName\WebRoot\WEB-INF\lib目录内。(注:projectName为应用的项目名) 各个jar包参考代码示例。

# 4.5.2 配置文件WEB.XML

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
   <!-- 统一退出过滤器 -->
   <filter>
      <filter-name>SSO Single Sign Out Filter</filter-name>
      <filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
   </filter>

   <!-- 该过滤器负责用户的认证工作,必须启用它 -->
   <filter>
      <filter-name>SSOFilter</filter-name>
      <filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
      <init-param>
         <param-name>casServerLoginUrl</param-name>
         <param-value>http://cas.server:port/cas/login</param-value>
      </init-param>
      <init-param>
         <!-- 本系统的UIA -->
         <param-name>serverName</param-name>
         <param-value>http://localhost:8088</param-value>
      </init-param>
      <init-param>
         <param-name>renew</param-name>
         <param-value>false</param-value>
      </init-param>
      <init-param>
         <param-name>gateway</param-name>
         <param-value>false</param-value>
      </init-param>
   </filter>

   <!-- 该过滤器负责对Ticket的校验工作,必须启用它 -->
   <filter>
      <filter-name>CAS Validation Filter</filter-name>
      <filter-class>org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter</filter-class>
      <init-param>
         <param-name>casServerUrlPrefix</param-name>
         <param-value>http://cas.server:8080/cas</param-value>
      </init-param>
      <init-param>  
          <param-name>redirectAfterValidation</param-name>  
          <param-value>true</param-value>  
      </init-param>
      <init-param>
         <!-- 本系统的UIA -->
         <param-name>serverName</param-name>
         <param-value>http://localhost:8088</param-value>
      </init-param>
      <init-param>
         <param-name>acceptAnyProxy</param-name>
         <param-value>true</param-value>
      </init-param>
      <init-param>
         <param-name>proxyReceptorUrl</param-name>
         <param-value>/proxyCallback</param-value>
      </init-param>
      <init-param>
         <param-name>proxyCallbackUrl</param-name>
         <param-value>http://localhost:8088/proxyCallback</param-value>
      </init-param>

   </filter>

   <!--该过滤器负责实现HttpServletRequest请求的包裹,
   比如允许开发者通过HttpServletRequest的getRemoteUser()方法获得SSO登录用户的登录名,可选配置。-->
   <filter>
      <filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
      <filter-class>org.jasig.cas.client.util.HttpServletRequestWrapperFilter</filter-class>
   </filter>

   <filter>
      <filter-name>CAS Assertion Thread Local Filter</filter-name>
      <filter-class>org.jasig.cas.client.util.AssertionThreadLocalFilter</filter-class>
   </filter>

   <!-- cas mapping s -->
   <filter-mapping>
      <filter-name>SSO Single Sign Out Filter</filter-name>
      <url-pattern>/*</url-pattern>
   </filter-mapping>
   <filter-mapping>
      <filter-name>CAS Validation Filter</filter-name>
      <url-pattern>/proxyCallback</url-pattern>
   </filter-mapping>
   <filter-mapping>
      <filter-name>SSOFilter</filter-name>
      <url-pattern>*.jsp</url-pattern>
   </filter-mapping>
   <filter-mapping>
      <!-- 要拦截的本系统登录界面 -->
      <filter-name>SSOFilter</filter-name>
      <url-pattern>/*</url-pattern>
   </filter-mapping>
   <filter-mapping>
      <filter-name>CAS Validation Filter</filter-name>
      <url-pattern>/*</url-pattern>
   </filter-mapping>
   <filter-mapping>
      <filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
      <url-pattern>/*</url-pattern>
   </filter-mapping>
   <filter-mapping>
      <filter-name>CAS Assertion Thread Local Filter</filter-name>
      <url-pattern>/*</url-pattern>
   </filter-mapping>

   <listener>
      <listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
   </listener>
</web-app>

# 4.5.3 代码示例

点击下载
index.jsp

<%@page import="java.net.URLDecoder"%>
<%@page import="java.util.*"%>
<%@page  import="org.jasig.cas.client.util.AbstractCasFilter"%>
<%@page  import="org.jasig.cas.client.validation.Assertion"%>
<%@page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<html>
<head>
<style type="text/css">
	td{
		border: 1px solid;
	}
</style>
</head>
<body>
	<%Assertion assertion=
	    (Assertion)
	    session.getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION);
	/*获取用户扩展信息
	 *扩展信息由UIA的SSO配置决定
	*/
	Map<String, Object> map = assertion.getPrincipal().getAttributes();
	
	
	%>
	<table style="border: 1px solid;width: 100%;">
		<tr>
			<td style="width: 10%;">唯一标识(工号)</td>
			<td><%= assertion.getPrincipal().getName()%></td>
		</tr>
		<tr>
			<td >用户昵称</td>
			<td><%= decode((String)map.get("nickName")) %></td>
		</tr>
		<tr>
			<td >邮箱</td>
			<td><%= decode((String)map.get("email")) %></td>
		</tr>
		<tr>
			<td >手机号</td>
			<td><%= decode((String)map.get("phonenumber")) %></td>
		</tr>
		<tr>
		<tr>
			<td><a href="localhost:8080/cas/logout?service=http://localhost:8088/logout.action">退出</a>
		</tr>
	</table>
	<%! 
	private List<Map<String,String>> parseStringToList(String str) throws Exception {
        List<Map<String,String>> list = new ArrayList<Map<String,String>>();
        if(str == null || str.equals("")){
           return list;
        }
	    str = decode(str);
        String[] array = str.split("-");
        for (String subArray : array) {
            String[] keyResult = subArray.split(",");
            Map<String,String> map = new HashMap<String, String>();
            for (String subResult : keyResult) {
                String[] value = subResult.split(":");
                map.put(value[0], value[1]);
            }
            list.add(map);
        }
        return list;
    } 
	private String decode(String str) throws Exception{
	    if(str != null){
		    str = URLDecoder.decode(str,"UTF-8");
		}
	    return str;
	}
    %>
</body>
</html>

# 4.6 SpringBoot前后端对接方式

# 4.6.1 拷贝Maven依赖配置

<dependencies>
   <!-- security starter Poms -->
   <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
   </dependency>
   <!-- security 对CAS支持 -->
   <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-cas</artifactId>
   </dependency>
   <!-- security taglibs -->
   <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-taglibs</artifactId>
   </dependency>
</dependencies>

# 4.6.2 配置文件application.properties

app.server.host.url=http://localhost:8081
app.login.url=/login
app.logout.url=/logout

cas.server.host.url=http://cas-server:port/cas
cas.server.host.login_url=${cas.server.host.url}/login
cas.server.host.logout_url=${cas.server.host.url}/logout?service=${app.server.host.url}

# 4.6.3 代码示例

点击下载

SecurityConfig.java

package cas.client.springboot.demo.security;

import cas.client.springboot.demo.custom.CustomUserDetailsService;
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.validation.Cas20ServiceTicketValidator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken;
import org.springframework.security.cas.authentication.CasAuthenticationProvider;
import org.springframework.security.cas.web.CasAuthenticationEntryPoint;
import org.springframework.security.cas.web.CasAuthenticationFilter;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;

import cas.client.springboot.demo.properties.CasProperties;

@Configuration
@EnableWebSecurity //启用web权限
@EnableGlobalMethodSecurity(prePostEnabled = true) //启用方法验证
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Autowired
	private CasProperties casProperties;
	
	/**定义认证用户信息获取来源,密码校验规则等*/
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		super.configure(auth);
		auth.authenticationProvider(casAuthenticationProvider());
		//inMemoryAuthentication 从内存中获取
		//auth.inMemoryAuthentication().withUser("chengli").password("123456").roles("USER")
		//.and().withUser("admin").password("123456").roles("ADMIN");
		
		//jdbcAuthentication从数据库中获取,但是默认是以security提供的表结构
		//usersByUsernameQuery 指定查询用户SQL
		//authoritiesByUsernameQuery 指定查询权限SQL
		//auth.jdbcAuthentication().dataSource(dataSource).usersByUsernameQuery(query).authoritiesByUsernameQuery(query);
		
		//注入userDetailsService,需要实现userDetailsService接口
		//auth.userDetailsService(userDetailsService);
	}
	
	/**定义安全策略*/
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.authorizeRequests()//配置安全策略
			//.antMatchers("/","/hello").permitAll()//定义/请求不需要验证
			.anyRequest().authenticated()//其余的所有请求都需要验证
			.and()
		.logout()
			.permitAll()//定义logout不需要验证
			.and()
		.formLogin();//使用form表单登录
		
		http.exceptionHandling().authenticationEntryPoint(casAuthenticationEntryPoint())
			.and()
			.addFilter(casAuthenticationFilter())
			.addFilterBefore(casLogoutFilter(), LogoutFilter.class)
			.addFilterBefore(singleSignOutFilter(), CasAuthenticationFilter.class);
		
		//http.csrf().disable();
	}
	
	/**认证的入口*/
	@Bean
	public CasAuthenticationEntryPoint casAuthenticationEntryPoint() {
		CasAuthenticationEntryPoint casAuthenticationEntryPoint = new CasAuthenticationEntryPoint();
		casAuthenticationEntryPoint.setLoginUrl(casProperties.getCasServerLoginUrl());
		casAuthenticationEntryPoint.setServiceProperties(serviceProperties());
		return casAuthenticationEntryPoint;
	}
	
	/**指定service相关信息*/
	@Bean
	public ServiceProperties serviceProperties() {
		ServiceProperties serviceProperties = new ServiceProperties();
		serviceProperties.setService(casProperties.getAppServerUrl() + casProperties.getAppLoginUrl());
		serviceProperties.setAuthenticateAllArtifacts(true);
		return serviceProperties;
	}
	
	/**CAS认证过滤器*/
	@Bean
	public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
		CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
		casAuthenticationFilter.setAuthenticationManager(authenticationManager());
		casAuthenticationFilter.setFilterProcessesUrl(casProperties.getAppLoginUrl());
		return casAuthenticationFilter;
	}
	
	/**cas 认证 Provider*/
	@Bean
	public CasAuthenticationProvider casAuthenticationProvider() {
		CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider();
		casAuthenticationProvider.setAuthenticationUserDetailsService(customUserDetailsService());
		//casAuthenticationProvider.setUserDetailsService(customUserDetailsService()); //这里只是接口类型,实现的接口不一样,都可以的。
		casAuthenticationProvider.setServiceProperties(serviceProperties());
		casAuthenticationProvider.setTicketValidator(cas20ServiceTicketValidator());
		casAuthenticationProvider.setKey("casAuthenticationProviderKey");
		return casAuthenticationProvider;
	}
	
	/*@Bean
	public UserDetailsService customUserDetailsService(){
		return new CustomUserDetailsService();
	}*/
	
	/**用户自定义的AuthenticationUserDetailsService*/
	@Bean
	public AuthenticationUserDetailsService<CasAssertionAuthenticationToken> customUserDetailsService(){
		return new CustomUserDetailsService();
	}
	
	@Bean
	public Cas20ServiceTicketValidator cas20ServiceTicketValidator() {
		return new Cas20ServiceTicketValidator(casProperties.getCasServerUrl());
	}
	
	/**单点登出过滤器*/
	@Bean
	public SingleSignOutFilter singleSignOutFilter() {
		SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
		singleSignOutFilter.setCasServerUrlPrefix(casProperties.getCasServerUrl());
		singleSignOutFilter.setIgnoreInitConfiguration(true);
		return singleSignOutFilter;
	}
	
	/**请求单点退出过滤器*/
	@Bean
	public LogoutFilter casLogoutFilter() {
		LogoutFilter logoutFilter = new LogoutFilter(casProperties.getCasServerLogoutUrl(), new SecurityContextLogoutHandler());
		logoutFilter.setFilterProcessesUrl(casProperties.getAppLogoutUrl());
		return logoutFilter;
	}
}

# 4.7 ASP.NET

# 4.7.1 拷贝Net CasClient客户端到应用中

将CAS客户端所需的文件DotNetCasClient.dll、DotNetCasClient.pdb和DotNetCasClient.xml拷贝到 projectName\bin目录内(请使用附件中的Net CasClien客户端文件)。
并添加引用 net-cas-quote.png

# 4.7.2 配置文件web.config

<?xml version="1.0"?>
<configuration>
  <!-- 定义casClientConfig配置 -->
  <configSections>
    <section name="casClientConfig" type="DotNetCasClient.Configuration.CasClientConfiguration, DotNetCasClient"/>
  </configSections>
  <!--
    配置退出Cas参数
    caslogoutUrl - Cas退出登录地址
    serverName - 退出后返回到指定的地址 
  -->
  <appSettings>
    <add key="caslogoutUrl" value="http://ip:port/cas/logout"/>
    <add key="serverName" value="http://127.0.0.1:4602"/>
  </appSettings>
  <connectionStrings/>
  <!-- 
      配置Cas客户端参数
      casServerLoginUrl - CAS服务器登录表单的URL。
      casServerUrlPrefix - CAS服务器应用程序的根
      serverName - 登陆成功后返回到执行地址
      ticketValidatorName - 验证使用协议名称。有效值为Cas10,Cas20和Saml11
      singleSignOut - 允许应用程序接收时,用户的SSO会话结束发送CAS单点登录了消息。这将导致在该应用中,用户的会话被破坏。默认值是true
      serviceTicketManager - 服务票据管理器使用存储在CAS服务器进行验证,撤销和单点登录的全力支持返回的票。如果没有配置的一票管理,这些功能将被禁用
      renew - 强制用户访问该应用程序之前重新验证到CAS。这提供了额外的安全性,在使用性的成本,因为它可以有效地禁用SSO这种应用。默认值是false。
  -->
  <casClientConfig
    casServerLoginUrl="http://ip:port/cas/login"
    casServerUrlPrefix="http://ip:port/cas/"
    serverName="http://127.0.0.1:4602"
    ticketValidatorName="Cas20"
    singleSignOut="true"
    serviceTicketManager="CacheServiceTicketManager"
    redirectAfterValidation="true"  />
  
  <system.web>
    <!-- 
        如果出现其他单点登录应用退出,当前应用不退出的情况
        当前应用使用的是.net ifreamwork框架环境是4.0,
        第一种情况:在<system.web>中加入 <httpRuntime requestValidationMode="2.0"/>
        第二种情况:如果你的网站程序本身是net 2.0环境开发的,但放到了VS2010软件里运行,也会出现这种情况,你可以把运行解决方案切换成net2.0即可
        -->
    <httpRuntime requestValidationMode="2.0"/>
  
    <compilation debug="true"></compilation>
    <!--
      配置ASP.NET表单验证部分,以便它指向的casClientConfig节casServerLoginUrl属性定义的CAS服务器的登录网址。这是非常重要的中科院登录URL是在这两个位置是一样的。
      注意:这里请勿配置path参数‍
    -->
    <authentication mode="Forms">
      <forms
        loginUrl="http://ip:port/cas/login"
        timeout="30"
        defaultUrl="~/Default.aspx"
        cookieless="UseCookies"
        slidingExpiration="true" />
    </authentication>
    
    <!--禁止匿名登录-->
    <authorization>
      <deny users="?" />
    </authorization>
 
    <httpModules>
      <add name="DotNetCasClient" type="DotNetCasClient.CasAuthenticationModule,DotNetCasClient"/>
    </httpModules>
  </system.web>
  <system.webServer>
    <validation validateIntegratedModeConfiguration="false"/>
    <modules>
      <remove name="DotNetCasClient"/>
      <add name="DotNetCasClient" type="DotNetCasClient.CasAuthenticationModule,DotNetCasClient"/>
    </modules>
  </system.webServer>
</configuration>

# 4.7.3 代码示例

点击下载

# 4.8 PHP

# 4.8.1 整合CAS PHP客户端代码到项目中

把CAS PHP Client的源码copy到/includes下,如下图结构: 图中红色框标注文件和目录为CAS PHP Client的全部源码。 修改CAS/client.php,将其中的https改为http

文件参考下文 代码示例

# 4.8.2 配置文件config.php

<?php
   //CAS Server 主机名 
   define('CAS_SERVER_HOSTNAME', 'cas-ip'); 
   //CAS Server 端口号 
   define('CAS_SERVER_PORT', cas-port); 
   //CAS Server应用名 
   define('CAS_SERVER_APP_NAME', '/cas');
   //退出登录后返回地址
   define('LOGOUT_ADDRESS', 'http://xxx.xxx.xxx.xxx');
?>

# 4.8.3 Client端的设置示例

如果出现其他单点登录应用退出,当前应用不退出的情况。请按照以下方式处理:

  • 第一种情况:当前应用使用的是.net ifreamwork框架环境是4.0在<system.web>中加入 <httpRuntime requestValidationMode="2.0"/>
  • 第二种情况:如果你的网站程序本身是net 2.0环境开发的,但放到了VS2010软件里运行,也会出现这种情况,你可以把运行解决方案切换成net2.0即可。

# 4.8.4 代码示例

点击下载

index.php

<?php
    // 引入配置文件
    include 'config.php';
    // 引入CAS Client项目
    include '/includes/CAS.php'; 

    // 初始化CAS客户端参数
    phpCAS::client(CAS_VERSION_2_0,CAS_SERVER_HOSTNAME,CAS_SERVER_PORT,CAS_SERVER_APP_NAME,true); 
    // 不使用SSL服务校验 
    phpCAS::setNoCasServerValidation();   
    // 这里会检测服务器端的退出的通知,就能实现php和其他语言平台间同步登出了
    phpCAS::handleLogoutRequests();
    // 判断是否已经访问CAS验证,true-获取用户信息,false-访问CAS验证
    if(phpCAS::checkAuthentication()){  
        /**
        *获取用户的唯一标识信息
        *由UIA的配置不同可分为两种:
        *(1)学生:学号;教工:身份证号
        *(2)学生:学号;教工:教工号
        **/
        $userid=phpCAS::getUser();

        // 获取登录用户的扩展信息  
        // 用户姓名
        $name = phpCAS::getAttribute("nickName");
        // 电话号码
        $phone = phpCAS::getAttribute("phonenumber");
        // 邮件
        $email = phpCAS::getAttribute("email");

        // 获取所有扩展参数信息
        $attribute = phpCAS::getAttributes();
    }else{  
        // 访问CAS的验证
        phpCAS::forceAuthentication();  
    }  

    // 退出登录
    if(isset($_REQUEST['a'])){
        if($_REQUEST['a'] == "logout"){
            $param=array("service"=>LOGOUT_ADDRESS);  
            phpCAS::logout($param);
        }
    }


    function toArray($data){
      $result = array();
      if(isset($data)){
        $arrays = explode("-",$data);
        foreach ($arrays as $temp) {
              $_array = explode(",",$temp);
              $arrayName = array();
              if(count($_array)>0){
                foreach ($_array as $_temp) {
                    $_array1 = explode(":",$_temp);  
                    if(count($_array1)>0&&isset($_array1[0])&&isset($_array1[1])){
                      $arrayName[$_array1[0]]  = $_array1[1];
                    }
                }
                if(count($arrayName)>0){
                  array_push($result,  $arrayName);
                }
              } 
          }
      }
      return $result;
    }
?>
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>测试单点登录</title>
    <style type="text/css">
        table td{
          padding-left: 10px;
          height:30px;
        }
        body{
          font-family: "微软雅黑";
        }
    </style>
</head>
<body>
<table border="1" style="width:80%;margin-left:auto;margin-right:auto;margin-top:40px;margin-bottom:40px;">
    <tr>
        <th style="width:20%;">参数名称:</th>
        <th style="widht:80%;">参数值</th>
    </tr>
    <tr>
        <td>登陆唯一ID:</td><td><?php echo $userid;?></td>
    </tr>
    <tr>
         <td>用户名称:</td><td><?php echo $name;?></td>
    </tr>
    <tr>
        <td>用户电话:</td><td><?php echo $phone;?></td>
    </tr>
    <tr>
        <td>用户邮箱:</td><td><?php echo $email;?></td>
    </tr>
    <tr>
        <td colspan="2"><a href="http://ip:port/?a=logout">退出登录</a></td>
    </tr>
</table>

</body>
</html>