进阶之路:第三方系统整合SSO(非侵入式)

起源

最近有一个需求:

自己做一个SSO系统,要求登录之后第三方产品(如gitlab、jira)也实现免登陆的效果。

我听到之后第一反应就是:人家的认证系统走的不是我们的逻辑,除了改源码好像没别的实现方式了。。

但是经过几天的测试之后,最终还是实现了不修改源码实现第三方系统免登陆的效果。这个倒是提醒了我以后遇到事情不能太早下定论,多做些测试和调研,就算不行也得用充分的测试结果或官方的论证依据来印证说法。


原理

由于HTTP是无状态协议,所以web应用无法辨认请求发送者的身份,因此就有了各种各样帮助web应用记录用户身份信息的方法,解决本次问题的就是其中之一——Cookie。

gitlab的登陆原理如下:1.用户访问gitlab登录页面并输入用户名密码 → 2.浏览器携带参数发送请求 → 3.gitlab服务器发放session → 4.服务器将session设置到cookie中 → 5.重定向到欢迎页


测试过程

查看请求

登录请求
分析一下这次登录请求:

  1. 用户输入的用户名和密码分别放在了body里的user[login]user[password],同时body里还有一些额外的参数,如utf8、authenticity_token、user[remember_me];
  2. 请求中带上了_gitlab_session
  3. 校验成功后服务器返回了一个新的_gitlab_session,将其设置到cookie中,然后重定向到了http://gitlab.test.com这个url;

通用代码

关键依赖
<!-- http client -->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.3</version>
</dependency>
创建执行器
CloseableHttpClient httpClient = HttpClients.custom().build();

流程图

SSO登录流程


模拟请求

获取参数

登录请求的body参数中一共有5个参数,其中utf8、user[remember_me]为定值写死即可,user[login]user[password]可直接让用户通过表单输入,而authenticity_token则需要爬取页面信息获取。这里着重讲一下authenticity_token的获取方式。

查看登录页面源代码可以发现页面头有一个name为csrf-token的标签:
authenticity_token
这个就是我们需要的authenticity_token,其目的是用于防止跨站请求伪造,这里我们只需要用爬虫将其抓取后作为参数传递给gitlab服务器即可。

// 爬取页面内容
HttpGet getCsrfToken = new HttpGet("http://gitlab.test.com");
CloseableHttpResponse getCsrfTokenResponse = httpClient.execute(getCsrfToken);
// 匹配authenticity_token
Matcher csrfTokenMatcher = Pattern.compile("(?<=<meta name=\"csrf-token\" content=\").*(?=\")").matcher(EntityUtils.toString(getCsrfTokenResponse.getEntity(), "UTF-8"));
if (csrfTokenMatcher.find()) {
	String csrfToken = csrfTokenMatcher.group();
}
获取请求中的_gitlab_session

在前面的请求分析中可以看到gitlab在登录时会随请求发送一个_gitlab_session。虽然这个_gitlab_session和请求返回的_gitlab_session同名,但是此时的session并不能使我们实现免登陆的效果,推测是_gitlab_session中保存了用户的登录状态等信息。直接在上一步的响应头中就可以找到未登录状态的_gitlab_session

// 匹配登录前的_gitlab_session
Matcher logoutSessionMatcher = Pattern.compile("(?<=_gitlab_session=).+?(?=;)").matcher(getCsrfTokenResponse.getFirstHeader("Set-Cookie").getValue());
if (logoutSessionMatcher.find()) {
	String logoutSession = logoutSessionMatcher.group();
}
模拟登录

万事俱备,所有的登录必备参数我们已经获取完成,接下来开始模拟发送登录请求。

HttpPost getSession = new HttpPost("http://gitlab.test.com/users/sign_in");
// 设置body
List<NameValuePair> paramList = new ArrayList<>();
paramList.add(new BasicNameValuePair("utf8", "✓"));
paramList.add(new BasicNameValuePair("authenticity_token", csrfTokenMatcher.group()));
paramList.add(new BasicNameValuePair("user[login]", username));
paramList.add(new BasicNameValuePair("user[password]", password));
paramList.add(new BasicNameValuePair("user[remember_me]", "0"));
getSession.setEntity(new UrlEncodedFormEntity(paramList, "utf-8"));
Matcher logoutSessionMatcher = Pattern.compile("(?<=_gitlab_session=).+?(?=;)").matcher(getCsrfTokenResponse.getFirstHeader("Set-Cookie").getValue());
if (logoutSessionMatcher.find()) {
	// 设置Header
    getSession.setHeader("Cookie", "_gitlab_session=" + logoutSessionMatcher.group());
}
// 获取登录后的session
CloseableHttpResponse getSessionResponse = httpClient.execute(getSession);
// 匹配登录后的_gitlab_session
Matcher loginSessionMatcher = Pattern.compile("(?<=_gitlab_session=).+?(?=;)").matcher(getSessionResponse.getFirstHeader("Set-Cookie").getValue());
if (loginSessionMatcher.find()) {
	String loginSession = loginSessionMatcher.group();
}
设置cookie

不出意外的话,在上一步中我们已经拿到了免登陆的关键信息:登录后的_gitlab_session,接下来我们只需要将session信息设置到cookie中就可以完成终极目标了,不过这里还有一个隐藏的坑——cookie的跨域问题。

一般而言,cookie只能跨子域共享(例如a.test.comb.test.com共享cookie),可以通过以下代码实现:

Cookie cookie = new Cookie("name", "value");
cookie.setDomain("test.com");
response.addCookie(cookie);

而无法跨顶级域名共享cookie。(设想以下情景:钓鱼网站诱导用户输入账号密码拿到session,然后将session跨域设置到官方网站的cookie实现免登陆。可怕不?)

此处我们采用一个取巧的方式绕过跨域问题:

在gitlab服务器下放一个专门用于设置cookie的服务(称之为session适配器)。我们通过之前的步骤获取到_gitlab_session后,通过调用API的形式将_gitlab_session作为参数传递给session适配器,然后由session适配器将cookie设置到浏览器中。由于session适配器和gitlab域名相同,gitlab可以读取到session中的身份信息,所以此时再访问gitlab便实现了免登陆的效果。

@GetMapping("/gitlab")
@CrossOrigin(origins = "*", allowedHeaders = "*",
        methods = {RequestMethod.GET, RequestMethod.POST}, maxAge = 3600L)
public void gitlab(@RequestParam String session, HttpServletResponse response) {
    // 设置cookie
    Cookie gitlabSession = new Cookie("_gitlab_session", session);
    gitlabSession.setPath("/");
    gitlabSession.setHttpOnly(true);
    response.addCookie(gitlabSession);
}

到这里我们已经实现了gitlab免登陆,用户无感知,并且对gitlab本身没有任何侵入式修改。


难点

流程中一共有三个坑需要注意:

  1. authenticity_token的获取。因为我司开发的主要是企业项目,办公环境在内网(相对安全),所以虽然知道CSRF的基本原理,但是并不需要防范,也就没有机会接触到它的配置。这次还好大佬见多识广,一句话就指明了这个参数可以从页面代码获取,不然这个坑估计得踩很久。
  2. body的传递。由于现在Restful风格占了主流,请求体一般都用json进行传递,反映在header里就是Content-Type:application/json。但是gitlab的请求是form表单传递的,header中使用的是Content-Type:x-www-form-urlencoded,所以应该使用以下方式设置请求体:
    HttpPost getSession = new HttpPost("http://gitlab.test.com/users/sign_in");
    // 设置body
    List<NameValuePair> paramList = new ArrayList<>();
    paramList.add(new BasicNameValuePair("utf8", "✓"));
    paramList.add(new BasicNameValuePair("authenticity_token", csrfTokenMatcher.group()));
    paramList.add(new BasicNameValuePair("user[login]", username));
    paramList.add(new BasicNameValuePair("user[password]", password));
    paramList.add(new BasicNameValuePair("user[remember_me]", "0"));
    getSession.setEntity(new UrlEncodedFormEntity(paramList, "utf-8"));
    
  3. cookie跨域。这个在上文中已经详细描述了,此处不再赘述。

优化

关于跨域设置cookie的问题后来又找到一个更简便的解决方案:

session适配器中的方法整合到登录中心的登录方法,然后通过反向代理将该方法设置为和对应的第三方服务同域。(可以和登录中心下的其他方法不同域)

这样可以省略session适配器服务,减少代码量,优化部署过程。

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值