URLConnection自动登录深大CAS认证平台:提交表单实现方案

本次实验使用HttpsURLConnection实现深大统一身份认证平台的自动登录。使用这个工具可以对包括Blackboard、OA、图书馆、财务查询、缴纳学费、公文通、校务信箱、故障报修、研究生选课和场馆订票等一切需要使用深大统一认证平台的站点实现一键登录甚至免登录的功能。

CAS验证基本原理

深大统一身份认证平台使用了Jasig Central Authentication Service 3.5.2.1这个 CAS 平台。CAS,全称 Central Authentication Service,是一种比较不错的的单点登录服务框架。使用像 CAS 这样单点登录(Single Sign On, 简称 SSO)方案时,用户只需要登录一次即可访问所有相互信任的系统(比如深大内的各种站点)。

CAS的基本验证原理如图所示:
CAS Protocol

通过原理图可知,浏览器访问一个使用了 CAS 服务的页面(CAS Client,如BlackBoard)后,将会被重定向至 CAS 验证页面(CAS Server,如深大的统一身份认证平台)进行用户认证。当用户验证成功后,浏览器将获得一条唯一且不可伪造的 Ticket。然后 CAS Server 页面将浏览器重定向回 CAS Client 页面,浏览器紧接提交获得的Ticket给 Client页面,Client 页面接收到Ticket后,从后台验证该Ticket的合法性。验证通过后将自动跳转到登录后的页面,并返回相关的身份信息。

实现结果

请求 BlackBoard 校园卡用户页面
http://elearning.szu.edu.cn/webapps/cbb-sdgxtyM-BBLEARN/checksession.jsp,成功打印登录后的 BlackBoard 页面内容:

实现代码

实现有两部分代码组成。第一个是主逻辑代码HTTPSPoster.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
package cn.kavel.httpsposter;


import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import java.io.*;
import java.net.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Created by wjw_w on 2017/4/13.
*/
public class HTTPSPoster {

private URL mHttpsURLHost;
private StringBuilder mPOSTQuery;
private StringBuilder mCOOKIEChain;
private String action;
private String cookie;
private String username;
private String password;

/**
* 构造方法
*
* @param host 需要登录的目标地址
* @param username 登录用户名
* @param password 登录密码
*/
HTTPSPoster(String host, String username, String password) {
try {
this.username = username;
this.password = password;
mHttpsURLHost = new URL(host);
mPOSTQuery = new StringBuilder();
mCOOKIEChain = new StringBuilder();
} catch (MalformedURLException e) {
e.printStackTrace();
}
}

/**
* 添加POST请求键值对
*
* @param key POST键
* @param value POST值
*/
private void addPOSTQuery(String key, String value) {
mPOSTQuery.append("&");
try {
mPOSTQuery.append(URLEncoder.encode(key, "UTF-8"));
mPOSTQuery.append("=");
mPOSTQuery.append(URLEncoder.encode(value, "UTF-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}

/**
* 为302跳转请求添加Cookie链条
*
* @param cookie 该次跳转返回的响应头文件内的Set-Cookie值
*/
private void addCookieChain(String cookie) {
if (mCOOKIEChain.toString().isEmpty())
mCOOKIEChain.append(cookie);
else
mCOOKIEChain.append(";" + cookie);
System.out.println(mCOOKIEChain.toString());
}

/**
* 处理网页并打印最终网页的结果
*
* @param connection 需要处理的URLConnection
* @return 返回String类型的网页内容
* @throws IOException 连接出错时抛出该异常
*/
private String getHTML(URLConnection connection) throws IOException {

if (connection instanceof HttpsURLConnection) {
HttpsURLConnection httpsURLConnection = (HttpsURLConnection) connection;
httpsURLConnection.setSSLSocketFactory(initCustomizedSSLSocketFactory());
}
connection = handleRedirect(connection);
String cookies = connection.getHeaderField("Set-Cookie");
cookie = cookies;
BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), "GBK"));
StringBuilder sb = new StringBuilder();
String s;
while ((s = br.readLine()) != null) {
sb.append(s);
System.out.println(s);
}
return sb.toString();
}

/**
* 递归处理跳转请求
*
* @param connection 当前请求
* @return 返回最终状态码不为302的请求
* @throws IOException 连接出错时抛出该异常
*/
private URLConnection handleRedirect(URLConnection connection) throws IOException {
System.out.println("respond:" + ((HttpURLConnection) connection).getResponseCode());
for (Map.Entry<String, List<String>> entry : connection.getHeaderFields().entrySet()) {
System.out.println("Key : " + entry.getKey() +
" ,Value : " + entry.getValue());
}
if (((HttpURLConnection) connection).getResponseCode() == HttpURLConnection.HTTP_MOVED_TEMP) {
String cookies = connection.getHeaderField("Set-Cookie");
String location = connection.getHeaderField("Location");
connection = new URL(location).openConnection();
if (connection instanceof HttpsURLConnection) {
HttpsURLConnection httpsURLConnection = (HttpsURLConnection) connection;
httpsURLConnection.setSSLSocketFactory(initCustomizedSSLSocketFactory());
}
addCookieChain(cookies);
connection.setRequestProperty("Cookie", mCOOKIEChain.toString());
connection = handleRedirect(connection);
}
mHttpsURLHost = connection.getURL();
return connection;
}

/**
* 分析登录验证页面上的元素,获取到表单提交地址和隐藏元素
*
* @param HTTP String类型的网页
* @return 返回表单提交地址
*/
private String analyseAuthPage(String HTTP) {
String HIDDEN_REG = "<input type=\"hidden\" (.*?)/>";
String FORM_REG = "<form (.*?)/>";

List<String> hiddenList = getTag(HTTP, HIDDEN_REG);
for (String tag : hiddenList) {
String[] splited = tag.split("\"");
String name = splited[3];
String value = splited[5];
addPOSTQuery(name, value); //添加隐藏键值对到mPOSTQuery中
}
String formtag = getTag(HTTP, FORM_REG).get(0);
String[] splited = formtag.split("\"");
return "https://" + mHttpsURLHost.getHost() + splited[3];
}

/**
* 根据正则表达式匹配内容
*
* @param HTML 待匹配文本内容
* @param regex 正则表达式
* @return 返回存放有匹配内容的列表
*/
private List<String> getTag(String HTML, String regex) {
Matcher matcher = Pattern.compile(regex).matcher(HTML);
List<String> list = new ArrayList<>();
while (matcher.find()) {
list.add(matcher.group());
}
return list;
}

/**
* 主要逻辑
*
* STEP1: 裸GET传入的网页,会因为没有身份验证信息“ticket”而被跳转到CAS页面进行身份验证。
* 跳转到实际的CAS页面后,分析页面中的HTML元素,从而获取到提交表单所需要的必要信息。(包括POST地址和hidden属性的值)
* 还有从响应头中获取表示当前身份的Cookie(Cookie非常重要,前几个小时因为没有正确存入Cookie而遭遇了循环跳转CAS页面)
*
* STEP2: 获取到必要信息后,open一个connection并将必要信息进行组装,最后加入username与password,进行POST操作。
* 在这些信息中,组装正确的Cookie值非常重要。前几个小时的实验因为没有存入正确的Cookie而被判断没有登录,一直被重定向回CAS页面。
*
* STEP3: 如果一切正常,connection将会非常舒服地通过验证并跳转到所希望的页面上去。
*/
private void post() {
try {
System.out.println("STEP1");
//STEP1: GET and Analyse CAS Page (in order to get HIDDEN INFORMATION and COOKIE)
String http = getHTML(mHttpsURLHost.openConnection());
String action = analyseAuthPage(http);

System.out.println("STEP2");
//STEP2: POST data and HIDDEN INFORMATION and COOKIE
URL newPOST = new URL(action);
HttpsURLConnection newPOSTConnection = (HttpsURLConnection) newPOST.openConnection();
newPOSTConnection.setSSLSocketFactory(initCustomizedSSLSocketFactory());
newPOSTConnection.setDoOutput(true);
newPOSTConnection.setRequestMethod("POST");
newPOSTConnection.setRequestProperty("Cookie", cookie);
addPOSTQuery("username", username);
addPOSTQuery("password", password);
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(newPOSTConnection.getOutputStream()));
bufferedWriter.write(mPOSTQuery.toString());
bufferedWriter.flush();

System.out.println("STEP3");
//STEP3: GET RESULT
getHTML(newPOSTConnection);


} catch (IOException e) {
e.printStackTrace();
}
}

/**
* 组装自定义SSLSocket工厂
* 在默认的情况下建立HTTPS链接时,HttpsURLConnection会根据默认的设定先去验证服务器的证书信息。
* 而由于一些未知的原因,客户端获取不到深大CAS页面的证书信息,
* 于是转向客户端TrustStore中寻找证书。而默认TrustStore是空的,因为没有手工添加过证书。
* 所以这种情况下进行连接将会抛出SSLHandshakeException异常。
* 所以需要传入一个自定义的SSLSocketFactory用于绕过证书验证。
* 这个自定义的SSLSocket被传入一个假TrustManager,这个manager遇到证书验证失败不会有任何警报。当然,这种做法非常危险。
*
* @return 返回自定义的SSLSocketFactory
*/
private SSLSocketFactory initCustomizedSSLSocketFactory() {
try {
TrustManager[] trustManagers = {new MyX509TrustManager()};
SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
sslContext.init(null, trustManagers, new java.security.SecureRandom());
return sslContext.getSocketFactory();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

public static void main(String[] args) {
// write your code here
String bb = "http://elearning.szu.edu.cn/webapps/cbb-sdgxtyM-BBLEARN/checksession.jsp";
String username = "";
String password = "";
HTTPSPoster poster = new HTTPSPoster(bb, username, password);
poster.post();
}


}

第二个是用于绕过证书验证的自定义TrustMamagerMyX509TrustManager.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
package cn.kavel.httpsposter;

import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import java.security.KeyStore;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

/**
* Created by wjw_w on 2017/4/13.
*/
public class MyX509TrustManager implements X509TrustManager {
/*
* The default X509TrustManager returned by SunX509. We'll delegate
* decisions to it, and fall back to the logic in this class if the
* default X509TrustManager doesn't trust it.
*/
X509TrustManager sunJSSEX509TrustManager;

MyX509TrustManager() throws Exception {
// create a "default" JSSE X509TrustManager.
KeyStore ks = KeyStore.getInstance("JKS");
//不需要load,因为这个是假的,是特技
//ks.load(new FileInputStream("trustedCerts"),
// "passphrase".toCharArray());
TrustManagerFactory tmf =
TrustManagerFactory.getInstance("SunX509", "SunJSSE");
tmf.init(ks);
TrustManager tms[] = tmf.getTrustManagers();
/*
* Iterate over the returned trustmanagers, look
* for an instance of X509TrustManager. If found,
* use that as our "default" trust manager.
*/
for (int i = 0; i < tms.length; i++) {
if (tms[i] instanceof X509TrustManager) {
sunJSSEX509TrustManager = (X509TrustManager) tms[i];
return;
}
}
/*
* Find some other way to initialize, or else we have to fail the
* constructor.
*/
throw new Exception("Couldn't initialize");
}

/*
* Delegate to the default trust manager.
*/
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
try {
sunJSSEX509TrustManager.checkClientTrusted(chain, authType);
} catch (CertificateException excep) {
// do any special handling here, or rethrow exception.
}
}

/*
* Delegate to the default trust manager.
*/
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
try {
sunJSSEX509TrustManager.checkServerTrusted(chain, authType);
} catch (CertificateException excep) {
/*
* Possibly pop up a dialog box asking whether to trust the
* cert chain.
*/
}
}

/*
* Merely pass this through.
*/
public X509Certificate[] getAcceptedIssuers() {
return sunJSSEX509TrustManager.getAcceptedIssuers();
}
}
#

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×