前言:
如果用java来写一个单点登录的CAS客户端,是很容易实现的,只要导入相关的jar包,再按要求配置就好了,关于java的单点登录的实现,在我前边的几篇文章中有重点描述,有兴趣的童鞋可以去看看。最近要做一个c++版的单点登录的客户端,在网上搜了一下,没有找到,所以只好自己想办法写了。
1.单点登录原理
为了了解单点登录的原理,我反编译了java版客户端jar包cas-client.jar代码,以下描述只是我的理解,如果有不当的地方,还望大家指出。
I.当用户第一次去访问客户端地址clientURL时,cas客户端检测到session中没有用户信息且浏览器中没有ticket,会重定向到casServer的地址serverURL,地址格式为serverURL/login?service=clientURL,service代表用户在server端成功登陆后页面跳转的地址,即客户端地址。
II.服务端接收到请求,判断到请求中没有ticket,会跳转到登录页面,用户在页面上填写正确的用户名密码完成登录后,页面跳转到客户端地址clientURL,格式为clientURL?ticket=XXX,并将ticket保存到浏览器缓存中(ticket的保存应该是由casServer完成的)。
III.此时地址跳回了客户端,cas客户端判断到此时session中没有用户信息,但请求中携带了ticket,因此向casServer发送ticket校验的请求,请求格式为serverURL/serviceValidate?ticket=XXX&service=clientURL,使用的方法是HTTPURLConnection.如果ticket验证成功,会返回如下xml文件格式的数据:
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
<cas:authenticationSuccess>
<cas:user>sa</cas:user>
</cas:authenticationSuccess>
</cas:serviceResponse>
IV.然后客户端通过解析该数据,获取用户名,并存储到session中。
当用户再次登录该系统时,判断到session中有用户信息,所以不再进行ticket验证;如果去登陆另外一个cas客户端,由于浏览器中存在ticket,所以会直接进行ticket验证的操作。
以上简单的对该流程进行了描述,下面的代码使用java是对ticket校验过程的简单模拟:
public class HttpUrlConnectionTest {
/**
* @param args
* @throws UnsupportedEncodingException
*/
public static void main(String[] args) throws Exception {
//cas server 校验地址
String serverValidateUrl="http://nagsh:8080/cas_server/serviceValidate";
//客户端地址
String service = "http://nagsh:8080/cas_cgi_client/cgi-bin/cas_client2.exe";
//ticket
String ticket ="ST-19-eBhtXb5cuIlAxHpqf5pA-cas01.example.org";
//组装url
String constructServiceUrl = serverValidateUrl+"?ticket="+ticket+"&service="+service;
URL url =new URL(constructServiceUrl);
HttpURLConnection conn = (HttpURLConnection)url.openConnection();
conn.setConnectTimeout(5*1000);
conn.setRequestMethod("GET");
InputStream inStream = conn.getInputStream();
final StringBuilder builder = new StringBuilder(255);
int byteRead;
while ((byteRead = inStream.read()) != -1) {
builder.append((char) byteRead);
}
String response = builder.toString();
}
}
获取到response后再使用XmlUtils去解析即可。
这里不对cas_client反编译的源码做过多的描述,有兴趣的童鞋可以去看一下源码,会有更多的收获
2.开发环境
开发前有很多准备工作,我也是一步步摸索过来的。
I.搭建CGI环境
c++一般是用来写后台的,一般编译后会形成exe文件,但我需要将其部署到tomcat下,所以需要配置tomcat使其支持c++,具体内容,请看我的另一篇文章:CGI编程–Tomcat下运行c++程序
II.编译libcurl,cgicc,tinyxml三个库
关于这三个库,在文章末尾的源码中都有,包括dll文件,需要将其中的dll文件拷贝到C:\Windows\SysWOW64目录下。
III.tomcat8
tomcat6不支持libcurl,在这个问题上我花费了两天时间,一直以为是代码的问题,后来换成tomcat8就好了。
IV.vs2008
我使用的开发工具是vs2008.
V.部署casServer.
在我的另外两篇文章中有casServer的配置方法,如果大家有现成的casServer,可以忽略此步骤。
CAS单点登录(二)—非SSL协议 CAS服务端部署及客户端配置
CAS单点登录(三)–服务端改造(登录页及登录方式的自定义)
3. 代码
其实,拿代码说话才是硬道理。
I.先来一张代码结构图:
cgicc、curl、tinyxml三个文件夹下是对应的头文件和cpp文件。
cas.client.h为核心,我把它写成了头文件。
config.xml为配置文件。
lib下为依赖的库文件,需要在vs中做依赖关联。
dll下为dll文件,需要添加到C:\Windows\SysWOW64下。
main.cpp是程序的入口,主要是调用cas_client.h。
II.config.xml:
<xml>
<CAS_SERVER_URL>http://127.0.0.1/cas_server</CAS_SERVER_URL>
<CAS_CLIENT_URL>http://nagsh:8080/cas_cgi_client/cgi-bin/cas_client.exe</CAS_CLIENT_URL>
</xml>
III.cas_client.h
#include <iostream>
#include <vector>
#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <stddef.h>
#include "cgicc/CgiDefs.h"
#include "cgicc/Cgicc.h"
#include "cgicc/HTTPHTMLHeader.h"
#include "cgicc/HTMLClasses.h"
#include "curl/curl.h"
#include "tinyxml/tinyxml.h"
using namespace std;
using namespace cgicc;
string CAS_SERVER_URL = ""; //CAS server 地址
string CAS_CLIENT_URL = ""; //CAS 客户端地址
string fileUrl = "config.xml";
//读取配置文件,获取配置信息
void loadInfo(){
const char * xmlUrl = fileUrl.c_str();
TiXmlDocument doc(xmlUrl);
bool loadOk = doc.LoadFile();
if (!loadOk)
{
cout << "could load:" << doc.ErrorDesc() << endl;
}
//获取配置信息
TiXmlElement * rootElement = doc.RootElement(); //serviceResponse元素
TiXmlElement * serverElement = rootElement->FirstChildElement(); // cas_server元素
TiXmlElement* clientElement = serverElement->NextSibling()->ToElement(); // cas_server元素
const char * cas_server = serverElement->GetText();
const char * cas_client = clientElement->GetText();
string serverStr(cas_server);
string clientStr(cas_client);
CAS_SERVER_URL = serverStr;
CAS_CLIENT_URL = clientStr;
}
//CAS客户端页面展示
void toHomePage(string info){
cout << "Content-type:text/html\r\n\r\n";
cout << "<html>\n";
cout << "<head>\n";
cout << "<title>CAS 客户端</title>\n";
cout << "<script type=\"text/javascript\">\n";
cout << "function logout(){";
cout << "document.cookie='userName=;';";
// cout << "window.location.href=encodeURI(\"http://127.0.0.1:8080/cas_server/logout?service=http://nagsh:8080/cas_cgi_client/cgi-bin/cas_client2.exe\");";//登出需要用IP 否则报错
//此处为注销的方法
cout <<"window.location.href=encodeURI(\""+CAS_SERVER_URL+"/logout?service="+CAS_CLIENT_URL+"\");" ;
cout <<"}";
cout << "</script>\n";
cout << "</head>\n";
cout << "<body>\n";
cout << "welcome:"+info+"";
cout << "<a href='javascript:;' onclick='logout()'>注销</a>";
cout << "</body>\n";
cout << "</html>\n";
}
//重定向到CAS 客户端
void rediretToClient(string info){
//保存cookie后重定向
string cookies = "document.cookie='userName="+info+"';";
cout << "Content-type:text/html\r\n\r\n";
cout << "<html>\n";
cout << "<head>\n";
cout << "<script type=\"text/javascript\">\n";
cout << cookies;
cout << "window.location.href=encodeURI(\""+CAS_CLIENT_URL+"\")\n";
cout << "</script>\n";
cout << "</head>\n";
cout << "</html>\n";
}
//没有ticket 重定向到CAS 服务端
void toServerPage(){
cout << "Content-type:text/html\r\n\r\n";
cout << "<html>\n";
cout << "<head>\n";
cout << "<script type=\"text/javascript\">\n";
//cout << "window.location.href=encodeURI(\"http://nagsh:8080/cas_server/login?service=http://nagsh:8080/cas_cgi_client/cgi-bin/cas_client2.exe\")\n";
cout << "window.location.href=encodeURI(\""+CAS_SERVER_URL+"/login?service="+CAS_CLIENT_URL+"\")\n";
cout << "</script>\n";
cout << "</head>\n";
cout << "</html>\n";
}
//解析xml数据
string tinyXml(string xml){
const char * xmlString = xml.c_str();
TiXmlDocument *doc = new TiXmlDocument();
doc->Parse(xmlString);
TiXmlElement * rootElement = doc->RootElement(); //serviceResponse元素
TiXmlElement* authenElement = rootElement->FirstChildElement(); // authenticationSuccess元素
TiXmlElement* userElement = authenElement->FirstChildElement(); // user元素
const char * userName = userElement->GetText();
string s(userName);
return s;
}
//获取cookie
string geCookie(){
Cgicc cgi;
const_cookie_iterator cci;
// 获取环境变量
const CgiEnvironment& env = cgi.getEnvironment();
string userName = "";
for( cci = env.getCookieList().begin();
cci != env.getCookieList().end();
++cci )
{
if(cci->getName()=="userName"){
userName = cci->getValue();
}
}
return userName;
}
//ticket校验的回调函数
size_t http_data_writer(void* data, size_t size, size_t nmemb, void* content)
{
long totalSize = size*nmemb;
std::string* symbolBuffer = (std::string*)content;
if(symbolBuffer)
{
symbolBuffer->append((char *)data, ((char*)data)+totalSize);
}
return totalSize;
}
//ticket校验
int ticketViladate(string ticket)
{
CURL *curl; //定义CURL类型的指针
CURLcode code; //定义CURLcode类型的变量,保存返回状态码
curl = curl_easy_init(); //初始化一个CURL类型的指针
string userName = geCookie();
if(curl!=NULL)
{
// string url = "http://127.0.0.1:8080/cas_server/serviceValidate?ticket="+ticket+"&service=http://nagsh:8080/cas_cgi_client/cgi-bin/cas_client2.exe";
string url = CAS_SERVER_URL+"/serviceValidate?ticket="+ticket+"&service="+CAS_CLIENT_URL;
curl_easy_setopt(curl, CURLOPT_URL,url.data());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, http_data_writer); //获取Response数据回调
std::string strData; //获取的数据 正常代表的即为登录用户的用户名
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&strData); //设置写数据
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, FALSE); //如果数字证书不完整时可设置为FALSE,不进行验证
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, TRUE); //捕获重定向URL的信息
curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 20);
//调用curl_easy_perform 执行我们的设置.并进行相关的操作. 在这 里只在屏幕上显示出来.
code = curl_easy_perform(curl);
if(code == CURLcode::CURLE_OK)
{
long responseCode = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &responseCode);
if (responseCode < 200 || responseCode >= 300 || strData.empty())
{
toHomePage("响应码不正确");
return 0;
}
//跳转到客户端
//解析出用户名
strData = tinyXml(strData);
//保存cookie然后重定向
rediretToClient(strData);
}else{
//
toHomePage("解析失败,错误码:");
cout <<code;
}
//清除curl操作.
curl_easy_cleanup(curl);
}
return 0;
}
//检验请求中是否包含ticket
int checkTicket(){
Cgicc cgi;
string userName = geCookie();
//cookie中有用户名,直接跳转到客户端页面
if(userName!=""){
toHomePage(userName);
return 0;
}
//参数不为空 0:有参数 1:没有参数
if(cgi.getElements().empty()==0){
form_iterator ticket = cgi.getElement("ticket");
if( !ticket->isEmpty() && ticket != (*cgi).end()) {
//ticket有值
//验证ticket
ticketViladate(**ticket);
}else{
//tickt没有值
//跳转到CAS Server
toServerPage();
}
}else{
//没有ticket
//判断cookie中JSESSIONID与session中1是否一致
//是 进入页面
//否 跳转到CAS Server
toServerPage();
}
return 0;
}
int cas_client_init()
{
loadInfo();
checkTicket();
system("PAUSE");
return 0;
}
IV.main.cpp
#include "cas_client.h"
int main(){
cas_client_init();
return 0;
}
4.代码解释
我按照代码的流程来解释上边的代码。
1.main.cpp文件调用的方法是cas_client_init,首先会执行loadInfo方法。–>2
2.loadInfo方法主要是使用tinyXml读取config.xml文件,获取cas_server地址和客户端的地址,写这步的原因是方便正式使用后修改地址,而不必每次都重新编译。–>3
3.接下来执行checkTicket方法,会使用cgicc从cookie中获取用户信息(c++貌似不支持session,只好使用cookie来存储用户信息),如果userName不为空,即用户已登录,则直接调用toHomePage方法跳转到客户端页面,否则进行下一步–>4
4.cookie中没有用户信息,在判断是否存在ticket,如果不存在,则跳转到cas_server–>5,否则进行ticket验证–>6.
5.在server端登录成功后,地址跳转回客户端,重新从1开始执行,因为请求中有ticket,所以跳转到6进行ticket验证。
6.在ticketViladate方法中使用libcurl的向server发送请求进行ticket验证,并获取返回结果,使用tinyXml库进行解析,获取用户名。–>7
7.获取到用户名后,调用rediretToClient重定向到client地址,将用户信息写入cookie中(同时,重定向后,地址栏将不再显示ticket)
关于代码中的具体介绍,我都写在了注释中,大家可以仔细阅读。
尾言:
这是我的第一个c++程序,竟然不是HelloWorld。
这次开发,遇到了很多问题,比如libcurl在tomcat6下不兼容,弄了一天多,后来走投无路换了个tomcat就好了。又比如ticket验证的过程及获取用户名的方式,还是在耐心的看了源码后茅塞顿开。总之,自己又进步了一点点。
源码地址:源码