提要:
登录流程、所需参数可见之前博客(http://www.cnblogs.com/sei-cxt/p/8429069.html)。
1. 注意点一:验证码。
2. 注意点二:Cookie。
3. 示例代码和输出结果。
4. 以后要考虑的问题。
1. 注意点一:验证码。
首先是获取验证码,可知发送GET验证码请求后,12306服务端返回的是一张验证码图片,可以临时存在根目录下。同时,上篇博客(http://www.cnblogs.com/sei-cxt/p/8435921.html)写的GET方法是没有区分返回文件格式是image还是text的,这个判断要加上。
得到的验证码图片如下,关于自动验证验证码的技术暂时不讨论,需要学习图像识别技术。我们现在只说如何自己选择验证码并向服务端发送我们选择的验证码。
如上图,我们发送的答案其实是以图片左上顶点为原点的坐标,这一点通过fiddler抓包也可以发现,其中%2C是经过编码的“,”。
编码原因是:
Web设计人员面对的挑战之一是,要处理操作系统之间的区别。这些不同会导致URL的问题:例如有些操作系统允许文件中有空格,而有些不允许。多数操作系统不反对文件名中出现#号,但在URL中#号表示文件名的结束,后面是片段标识符。其他特殊字符、非字母数字字符等等,在URL或另一种操作系统中有特殊的意义,这也会产生类似的问题。为解决这些问题,URL使用的字符必须来自ASCII固定的子集,确切地讲,包括: ·大写字母A~Z ·小写字母a~z ·数字0~9 ·标点符号字符-_.!~*’和, …… 编码方式非常简单。除了前面指定的ASCII数字、字母和标点符号外,所有字符都要转换为字节,每个字节要写为百分号后面加两个十六进制数字。空格是一种特殊情况,因为它太普通了。空格不是编码为%20,而是编码为加号(+)。加号本身编码为%2B。/ # = &和?字符在用于名时应当编码,而用于URL各部分的分隔符时不用编码。 ——《Java网络编程》第七章 Elliotte Rusty Harold著 朱涛江 林剑 译
因此我们可以固定写死坐标,只用输入第几张图即可。
// 每个图的位置,直接写死,以后可改 String[] pos = {"40,75", "112,75", "183,75", "255,75", "40,150", "112,150", "183,150", "255,150"};
2. 注意点二:Cookie。
没有Cookie,就没有办法实现登录。因为登录有三步,不同步骤之间要告知信息,比如“已经成功验证验证码,可以验证登录信息了”。
要是没有验证验证码,直接向登录的网站发送用户名和密码,会返回:{"result_message":"验证码校验失败","result_code":"5"}
通过网上查询资料和核对fiddler请求头和响应头的数据得知,前一次的相应头里“Set-Cookie”字段可以用于下一步骤请求头的“Cookie”,这里有几个坑要注意一下。
1) 12306的“Set-Cookie”会发好几个,得到的数据是List<String>,直接用conn.getHeaderField("Set-Cookie")获得的cookie不全。因为getHeaderField(String name)这个方法只返回最后一次设置的值,返回类型是String(查API),必须用getHeaderFields()方法,从中找到key为"Set-Cookie"的数据然后用“; ”连接起来构成一个String字符串(尝试知有重复的数值对是允许的)。
2) 不能直接不连接获得的List<String>,而是多次用conn.setRequestProperty("Cookie", cookie)发送。因为setRequestProperty(String param1, String param2)方法中,param1若是有重复,存的值会被param2覆盖(看源码)。
3. 示例代码和输出结果。
1) 工具类(HttpsRequest):
相较之前加入了POST方法,其与GET方法相似度很高,之后考虑合为一个方法。
GET方法里新加了区分返回类型的代码。
新添对Cookie的处理。
import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.net.URL; import java.net.URLEncoder; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Scanner; import java.util.Map.Entry; import javax.net.ssl.HttpsURLConnection; public class HttpsRequest { public static HttpsRequestInfo methodGet(String urlStr, Map<String, Object> params, String cookie) { String realUrl = urlStr; StringBuffer responce = new StringBuffer(); StringBuffer set_cookie = new StringBuffer(); // GET方法需要将查询字符串拼接在url后面 if(params != null && params.size() != 0) { realUrl += encodeParam(params, "GET"); } try { // 连接url URL url = new URL(realUrl); HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(); conn.setRequestMethod("GET"); // URL连接可用于输入和/或输出 // 如果打算使用 URL连接进行输出,则将DoOutput标志设置为true,默认值为false // 如果打算使用 URL连接进行输入,则将DoInput标志设置为true,默认值为true // conn.setDoOutput(false); // conn.setDoInput(true); // 允许连接使用任何可用的缓存,默认值为true // conn.setUseCaches(true); conn.setSSLSocketFactory(MyX509TrustManager.getSSLSocketFactory()); // 设置一般请求属性 conn.setRequestProperty("accept", "*/*"); // 客户端可以处理哪些数据类型 conn.setRequestProperty("connection", "Keep-Alive"); // 设置长连接,无过期时间 conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)"); // 使用的何种浏览器 if(cookie != null && !cookie.equals("")) { conn.setRequestProperty("Cookie", cookie); // 设置cookie,保证登录 } // 建立通信链接 conn.connect(); // 获取cookie Map<String, List<String>> headerMap = conn.getHeaderFields(); for(Entry<String, List<String>> entry : headerMap.entrySet()) { if(entry.getKey() != null && entry.getKey().equals("Set-Cookie")) { for(String str : entry.getValue()) { set_cookie.append(str + "; "); } } } // 判断返回格式 // getHeaderField(String name)只返回最后一次设置的值,所以找cookie的时候不能用 String type = conn.getHeaderField("Content-Type"); if(type.startsWith("image")) { // 保存验证码图片 InputStream is = conn.getInputStream(); FileOutputStream fos = new FileOutputStream("CAPTCHA.jpg"); int b = 0; while((b = is.read()) != -1) { fos.write(b); } is.close(); fos.close(); } else { // 读取response BufferedReader br = new BufferedReader( new InputStreamReader(conn.getInputStream(), "utf-8")); String res = null; while((res = br.readLine()) != null) { responce.append(res); } br.close(); } } catch (IOException e) { e.printStackTrace(); } catch (KeyManagementException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (NoSuchProviderException e) { e.printStackTrace(); } return new HttpsRequestInfo(responce.toString(), set_cookie.toString()); } public static HttpsRequestInfo methodPost(String urlStr, Map<String, Object> params, String cookie) { StringBuffer responce = new StringBuffer(); StringBuffer set_cookie = new StringBuffer(); try { // 连接url URL url = new URL(urlStr); HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(); conn.setRequestMethod("POST"); // URL连接可用于输入和/或输出 // 如果打算使用 URL连接进行输出,则将DoOutput标志设置为true,默认值为false // 如果打算使用 URL连接进行输入,则将DoInput标志设置为true,默认值为true conn.setDoOutput(true); // conn.setDoInput(true); // 允许连接使用任何可用的缓存,默认值为true // conn.setUseCaches(true); conn.setSSLSocketFactory(MyX509TrustManager.getSSLSocketFactory()); // 设置一般请求属性 conn.setRequestProperty("accept", "*/*"); // 客户端可以处理哪些数据类型 conn.setRequestProperty("connection", "Keep-Alive"); // 设置长连接,无过期时间 conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)"); // 使用的何种浏览器 if(cookie != null && !cookie.equals("")) { conn.setRequestProperty("Cookie", cookie); // 设置cookie,保证登录 } // 建立通信链接 conn.connect(); // POST需要发送请求的数据 if(params != null && params.size() != 0) { BufferedWriter bw = new BufferedWriter( new OutputStreamWriter(conn.getOutputStream(), "utf-8")); bw.write(encodeParam(params, "POST")); bw.flush(); bw.close(); } // 获取cookie Map<String, List<String>> headerMap = conn.getHeaderFields(); for(Entry<String, List<String>> entry : headerMap.entrySet()) { if(entry.getKey() != null && entry.getKey().equals("Set-Cookie")) { for(String str : entry.getValue()) { set_cookie.append(str + "; "); } } } // 读取response BufferedReader br = new BufferedReader( new InputStreamReader(conn.getInputStream(), "utf-8")); String res = null; while((res = br.readLine()) != null) { responce.append(res); } br.close(); } catch (IOException e) { e.printStackTrace(); } catch (KeyManagementException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (NoSuchProviderException e) { e.printStackTrace(); } return new HttpsRequestInfo(responce.toString(), set_cookie.toString()); } /** * 将查询的map形式的参数转码并拼成查询字符串 * @param params * @return */ private static String encodeParam(Map<String, Object> params, String method) { if(params == null || params.size() == 0) { return ""; } StringBuffer sb = null; if (method.equalsIgnoreCase("GET")) { sb = new StringBuffer("?"); } else if (method.equalsIgnoreCase("POST")) { sb = new StringBuffer(""); } for(Entry<String, Object> para : params.entrySet()) { try { sb.append(URLEncoder.encode(para.getKey(), "UTF-8")); sb.append("="); sb.append(URLEncoder.encode(para.getValue().toString(), "UTF-8")); sb.append("&"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } String query = sb.toString(); // 去掉最后一个& return query.substring(0, query.length() - 1); } /** * 选择得到验证码答案坐标 * @param choose * @return */ private static String getCaptchaAnswer(String choose) { // 每个图的位置,直接写死,以后可改 String[] pos = {"40,75", "112,75", "183,75", "255,75", "40,150", "112,150", "183,150", "255,150"}; String[] chooses = choose.split(","); StringBuffer answer = new StringBuffer(); for(String cho : chooses) { answer.append(pos[Integer.parseInt(cho) - 1] + ","); } String ans = answer.toString(); return ans.substring(0, ans.length() - 1); } }
2) 证书相关类(MyX509TrustManager):与之前没有变化,不贴。
3) 网络信息类(HttpsRequestInfo):用来保存相应返回的信息和Cookie,原先的设计不能返回两个字符串。
public class HttpsRequestInfo { private String responce; private String cookie; public HttpsRequestInfo() { } public HttpsRequestInfo(String responce, String cookie) { super(); this.responce = responce; this.cookie = cookie; } public String getResponce() { return responce; } public void setResponce(String responce) { this.responce = responce; } public String getCookie() { return cookie; } public void setCookie(String cookie) { this.cookie = cookie; } }
4) main函数:验证验证码的响应头传回来的Cookie为空,因此登录传用户名密码的时候只能用发验证码图片的时候传回来的Cookie,具体原因以后考虑。
public static void main(String[] args) { Map<String, Object> params1 = new LinkedHashMap<String, Object>(); params1.put("login_site", "E"); params1.put("module", "login"); params1.put("rand", "sjrand"); HttpsRequestInfo info1 = HttpsRequest.methodGet("https://kyfw.12306.cn/passport/captcha/captcha-image", params1, null); System.out.println(info1.getResponce()); System.out.println(info1.getCookie()); Scanner in = new Scanner(System.in); String line = in.nextLine(); in.close(); Map<String, Object> params2 = new LinkedHashMap<String, Object>(); params2.put("answer", HttpsRequest.getCaptchaAnswer(line)); params2.put("login_site", "E"); params2.put("rand", "sjrand"); HttpsRequestInfo info2 = HttpsRequest.methodPost("https://kyfw.12306.cn/passport/captcha/captcha-check", params2, info1.getCookie()); System.out.println(info2.getResponce()); System.out.println(info2.getCookie()); Map<String, Object> params3 = new LinkedHashMap<String, Object>(); params3.put("username", "******"); params3.put("password", "******"); params3.put("appid", "otn"); HttpsRequestInfo info3 = HttpsRequest.methodPost("https://kyfw.12306.cn/passport/web/login", params3, info1.getCookie()); System.out.println(info3.getResponce()); System.out.println(info3.getCookie()); }
5) 输出结果:
BIGipServerpool_passport=300745226.50215.0000; path=/; _passport_ct=3a6f7d3cb29b4bd7b9ac3da2c41a8588t5927; Path=/passport; _passport_session=5a55bcfaa5644450b24d4f2ed8f1e7840821; Path=/passport; 2,7 {"result_message":"验证码校验成功","result_code":"4"} {"result_message":"登录成功","result_code":0,"uamtk":"nrsMZEFcdXx8EK1B_n9ODPC7-ErURBJGrANQivMxnVc091210"} uamtk=nrsMZEFcdXx8EK1B_n9ODPC7-ErURBJGrANQivMxnVc091210; Path=/passport; _passport_ct=; Max-Age=0; Expires=Thu, 01-Jan-1970 00:00:10 GMT; Path=/passport;
4. 以后要考虑的问题。
返回信息以后要用JSON解析,并且要判断result_code来决定之后的操作。