背景
需要用 Java 访问一个被 SSO 保护的特殊接口获取信息。
方案设计
假如我们用浏览器来实现这个功能,步骤简单如下:
- 输入目标 API 地址
- (浏览器自动重定向到 SSO 登陆页面)
- 输入用户名密码登陆
- (浏览器重定向回到 API 地址,并附带认证信息)
- 获取目标 API 的资源信息
其中需要用户操作的是步骤 1 和步骤 3。
但要求是用 Java 来实现上述功能,关键点在于如何获取认证信息。有了认证信息,我们便能直接 call 目标 API。
所以我们需要模拟浏览器的登陆行为,此时的初步想法是 [HttpClient (v4.5)](https://hc.apache.org/httpcomponents-client-4.5.x/quickstart.html)
+ CookieStore
:HttpClient
来发送 Http 请求,CookieStore
来缓存 Cookie 信息,相当于保存上下文(context)。
方案实施与问题描述
模拟浏览器登陆实现起来稍显复杂:
- 创建
CookieStore
,并添加到HttpClient
来保存上下文信息。 HttpClient
访问目标 API,因为需要 SSO 登陆,所以得到的 response 是一个 html 页面,即登陆页面。- 解析步骤 2 中返回的登陆页面,得到登陆表单提交的地址。
- 构建登陆请求,填入用户名密码信息,并用
HttpClient
提交登陆请求。 - 获取步骤 4 的 response。此时认证信息已经被添加到
CookieStore
中,HttpClient
可以直接访问目标 API。
简化后的代码如下:
// 1. 创建 CookieStore 以及 HttpClient
CookieStore cookieStore = new BasicCookieStore();
HttpClient httpClient = HttpClientBuilder.create()
.setRedirectStrategy(new LaxRedirectStrategy()) // follow redirect,即当遇到 302 时直接处理跳转
.setDefaultCookieStore(cookieStore)
.build();
// 2. 获取登陆表单 url
HttpResponse response = httpClient.execute(new HttpGet("{apiPath}"));
String result = EntityUtils.toString(response.getEntity());
Pattern pattern = Pattern.compile("<form id=\"login-form\" method=\"post\" name=\"login-form\" action=\"(.+?)\">"); // 表单正则
Matcher matcher = pattern.matcher(result);
if (!matcher.find()) {
// handle error
}
String loginUrl = matcher.group(1);
// 3. 提交登陆表单
HttpPost httpPost = new HttpPost(loginUrl);
List<NameValuePair> nameValuePairs = new ArrayList<>();
nameValuePairs.add(new BasicNameValuePair("username", "{username}"));
nameValuePairs.