一、前言
CAS是一个旨在为应用系统提供单点登录方案的企业级的开源项目,它为第三方应用提供了基于REST的操作接口。
为后续表达准确,对相关术语作简单说明:
- Web应用系统:准备集成CAS单点登录功能的各类Web应用;
- CAS Server:本文中特指cas-server-webapp的war文件,需要独立部署,有时也称为认证系统、认证中心;
- CAS Client:本文中特指cas-client-core-5.3.0.jar,需与应用系统一起部署。
- CAS版本:除非特别声明,CAS各组件的版本均为5.3.0;
- TGT(Ticket Grangting Ticket):TGT是CAS为用户签发的登录票据,拥有了TGT,用户就可以证明自己在CAS成功登录过。TGT封装了Cookie值以及此Cookie值对应的用户信息。用户在CAS认证成功后,CAS生成cookie(叫TGC),写入浏览器,同时生成一个TGT对象,放入自己的缓存,TGT对象的ID就是cookie的值。当HTTP再次请求到来时,如果传过来的有CAS生成的cookie,则CAS以此cookie值为key查询缓存中有无TGT ,如果有的话,则说明用户之前登录过,如果没有,则用户需要重新登录;
- ST(Service Ticket):ST是CAS为用户签发的访问某一service的票据。用户访问service时,service发现用户没有ST,则要求用户去CAS获取ST。用户向CAS发出获取ST的请求,如果用户的请求中包含cookie,则CAS会以此cookie值为key查询缓存中有无TGT,如果存在TGT,则用此TGT签发一个ST,返回给用户。用户凭借ST去访问service,service拿ST去CAS验证,验证通过后,允许用户访问资源。
二、CAS REST协议接口说明
1、获取TGT
请求方式 | POST | |
路径 | localhost:8080/cas/v1/tickets | |
http协议 | HTTP/1.0 | |
请求参数 | username=battags&password=password&additionalParam1=paramvalue | |
请求响应: | 201 Created | |
2、获取ST
请求方式 | POST | |
路径 | localhost:8080/cas/v1/tickets/{TGT id} | |
http协议 | HTTP/1.0 | |
请求参数 | service={form encoded parameter for the service url} | |
请求响应 | 200 OK | |
3、校验ST
请求方式 | GET |
路径 | localhost:8080/cas/p3/serviceValidate |
http协议 | HTTP/1.0 |
请求参数 | service={service url}&ticket={service ticket} |
请求响应 | 状态码,200成功;200请求失败;415不支持的媒体类型; |
4、登出
请求方式 | DELETE |
路径 | http://172.25.51.1:8080/cas/v1/tickets/{TGT} |
http协议 | HTTP/1.0 |
请求响应 | 返回注销的TGT |
三、示例代码(Java版本)
package cas_service;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.methods.DeleteMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
...
public class CasService
{
static private final Logger LOG = LoggerFactory.getLogger(CasService.class);
private static String serverAddr;
private static String serverPort;
private static String serverConnString;
static
{
String fileName = "cas-service.ini";
try
{
ProjProperties props = newProjProperties();
props.load(new FileInputStream(fileName));
serverAddr = props.getProperty("SSO_SVR_ADDRESS", "localhost");
serverPort = props.getProperty("SSO_SVR_PORT", "8443");
serverConnString = "https://" + serverAddr + ":" + serverPort + "/cas";
}
catch (Exception e)
{
LOG.warn("load application server configuration ({}) failed. {}", fileName, e.getMessage());
}
}
//获取TGT
public String getTicketGrantingTicket(String username, String password)
{
if (serverConnString==null || serverConnString.equals(""))
throw new Exception("Invalid parameter: CAS Server");
HttpClient client = new HttpClient();
PostMethod method = new PostMethod(serverConnString + "/v1/tickets");
method.setRequestBody(new NameValuePair[]
{ new NameValuePair("username", username), new NameValuePair("password", password) });
try
{
client.executeMethod(method);
String response = method.getResponseBodyAsString();
int status = method.getStatusCode();
switch (status)
{
case HttpStatus.SC_CREATED: // Created
{
Matcher matcher = Pattern.compile(".*action=\".*/(.*?)\".*").matcher(response);
if (matcher.matches())
return matcher.group(1);
break;
}
default:
throw new Exception("Invalid Response code " + status + " from CAS Server!");
}
}
catch (IOException e)
{
LOG.error("some exception happened during apply for a TGT " + e.getMessage());
}
finally
{
method.releaseConnection();
}
return null;
}
//根据TGT获得ST
public String getServiceTicket(String ticketGrantingTicket, String moduleName)
{
if (serverConnString==null || serverConnString.equals(""))
throw new Exception("Invalid parameter: CAS Server");
if (moduleName==null || moduleName.equals(""))
throw new Exception("Invalid parameter: no module name within request.");
if (ticketGrantingTicket==null || ticketGrantingTicket.equals(""))
throw new Exception("Invalid TGT.");
HttpClient client = new HttpClient();
PostMethod method = new PostMethod(serverConnString + "/v1/tickets/" + ticketGrantingTicket);
String service1 = buildModuleServiceName(moduleName);
method.setRequestBody(new NameValuePair[]
{ new NameValuePair("service", service1) });
try
{
client.executeMethod(method);
String response = method.getResponseBodyAsString();
int status = method.getStatusCode();
switch (status)
{
case HttpStatus.SC_OK: // Accepted
return response;
default:
throw new Exception("Invalid Response code " + status + " from CAS Server!");
}
}
catch (IOException e)
{
LOG.error("some exception occured during apply for a service ticket. " + e.getMessage());
}
finally
{
method.releaseConnection();
}
return null;
}
//检验ST是否有效
public String verifyServiceTicket(String serviceTicket, String moduleName)
{
if (serverConnString==null || serverConnString.equals(""))
throw new Exception("Invalid parameter: CAS Server");
if (moduleName==null || moduleName.equals(""))
throw new Exception("Invalid parameter: module name");
if (ABISHelper.isEmpty(serviceTicket))
return null;
HttpClient client = new HttpClient();
GetMethod method = null;
String service1 = buildModuleServiceName(moduleName);
try
{
method = new GetMethod(serverConnString + "/p3/serviceValidate?ticket="
+ URLEncoder.encode(serviceTicket, "utf-8") + "&service=" + URLEncoder.encode(service1, "utf-8"));
client.executeMethod(method);
String response = method.getResponseBodyAsString();
// 对有转发的访问请求,GetMethod才返回SC_OK,PostMethod返回的是302
int status = method.getStatusLine().getStatusCode();
switch (status)
{
case HttpStatus.SC_OK: // Accepted
int begin = response.indexOf("<cas:user>");
if (begin < 0)
return null;
int end = response.indexOf("</cas:user>");
return response.substring(begin + 10, end);
default:
throw new Exception("Invalid Response code " + status + " from CAS Server!");
}
}
catch (IOException e)
{
LOG.error("some exception occured during verify a service ticket. " + e.getMessage());
}
finally
{
method.releaseConnection();
}
return null;
}
//删除TGT
public boolean deleteTicketGrantingTicket(String ticketGrantingTicket)
{
if (serverConnString==null || serverConnString.equals(""))
throw new Exception("Invalid parameter: CAS Server");
if (ticketGrantingTicket==null || ticketGrantingTicket.equals(""))
return false;
HttpClient client = new HttpClient();
DeleteMethod method = new DeleteMethod(serverConnString + "/v1/tickets/" + ticketGrantingTicket);
try
{
client.executeMethod(method);
int status = method.getStatusCode();
switch (status)
{
case HttpStatus.SC_OK:
return true;
default:
throw new Exception("Invalid Response code " + status + " from CAS Server!");
}
}
catch (IOException e)
{
LOG.error("some exception occured during verifing a service ticket" + e.getMessage());
}
finally
{
method.releaseConnection();
}
return false;
}
private String buildModuleServiceName(String moduleName)
{
return "https://" + serverAddr + ":" + serverPort + "/" + moduleName;
}
public String getCasUserName ()
{
return SessionUtils. getAttribute("_COST_CAS_USER_NAME");
}
}
四、典型的调用流程
以下是应用系统使用cas_service包接口的典型流程:
- 某用户登录应用A,因为是首次登录,需提供用户名、密码;
- 应用A根据用户名、密码,调用getTicketGrantingTicket接口获取TGT;
- TGT多次使用,需保存在session或其它存储对象中;
- 应用A使用TGT,调用getServiceTicket接口获取am服务的ST;
- 应用A可使用刚获取的ST,作为参数访问am服务;
- ST因有效期短暂且使用次数有限制,一般是一次性使用,不必保存;
- 用户欲访问应用B的bn服务,先从session或其它存储对象中查找到TGT;
- 应用A(或应用B)TGT,调用getServiceTicket接口获取bn服务的ST;
- 应用B接收ST,调用verifySeviceTicket接口,返回不为null则该ST有效;
- 验证通过后,应用B使用该ST访问bn服务;
- 应用B可调用接口getCasUserName和getCasAttributes,获取登录用户及相关属性;
- 欲根据ST查找当前登录用户,调用getUsernameSeviceTicket接口,返回值即是;
- 用户从某应用注销时,需调用deleteTicketGrantingTicket接口从Cas Server删除TGT。