近期有个WEB项目需要改造。业主找第三方搞了一个集成站点,将多个应用站点的链接集中放在一个导航页面。由于进入集成站点时已经登录过了,业主要求点击这些应用站点的链接时就不必再登录。

以前做过类似项目,用的是单点登录。大家都用同一个登录系统,一次登录,到处同行,不亦快哉。不过也有一些缺点,一是单点登录比较复杂,不好搞。之前我们用过一个开源的单点登录系统cas,代码一大堆,部署也很复杂,然后每个使用它的应用都要有个客户端,总之非常复杂。出了问题也不知道是哪里的毛病。最常见的现象就是多次重定向,用户登录信息在客户端和服务器之间无限被踢皮球。所以每次想到要用这个东东都心烦意乱,甚至吓得面无人色。(详见拙作:《21世纪应用开源单点登录项目CAS之集大成者》)

这还不是最坏的。最大的问题是,使用单点登录,势必要维护共同的用户信息,要么是所有系统都使用同一个用户表,要么是大家同步用户信息。不同的应用系统通常由不同的公司开发,每个都有自己的设计,现在要集成在一起,改造工作量可想而知。有些年代久远,早就过维护期,想改造都不可能。

一、自动登录方案

但这次没有使用单点登录。第三方公司给出了一个方案:
1)在集成站点的导航页面点击应用站点链接时,系统分配一个token;
2)应用站点可以访问集成站点的接口对token进行验证,并获取对应的用户信息。这些用户信息是集成站点的,应用系统不一定有;
3)token验证通过后,应用站点就可以自己决定是否让他登录本系统了。

这种思想,有点类似auth。auth是第三方验证通过后就自动放行了,而这里的方案是,接下来还要应用系统自己做一些处理,即如何在本系统里放行。

这个方案的好处是,不一定要拥有相同的用户信息。如果要求不严格,应用系统验证带过来的token后就可以用一个默认的账号自动登录;如果非要是同一个账号,那么因为验证token的时候会返回用户信息,应用系统完全可以之同步到自己的库里。所以这个方案比较灵活,应用系统的主动权较大,修改工作量比较小,难度也较小。人家有高手啊。

二、应用系统的实现

我们系统采用java开发,安全框架是Spring Security。我的思路是:
1)访问本应用系统时,检查有无带上第三方token,有则执行第2步,无则转向本系统的登录页面
2)验证第三方token合法,则系统自动登录,否则转向本系统的登录页面

关键是如何自动登录。

我从前端的登录页面,按图索骥,发现登录按钮点击后,会提交到后端的“/auth/token”。但是我找来找去,都找不到对应的代码。后端用的这个框架我不熟悉,一问才知,/auth/token是Spring Security自己的实现。这个接口访问后,会返回一个json对象,里面有个关键元素,叫“access_token”,我们前端就是凭这个来认定是否已经登录了本系统的。

很自然地,我要在系统里实现自动登录,那我应该创建并返回这个access_token给前端。问题是,这个创建过程是黑箱,我搞来搞去,都生成不了类似的令牌。最后放弃了,何必自己去搞,系统模拟前端提交,访问一下自己这个/auth/token不就好了吗?代码如下:

@GetMapping(value = "/autoLogin")
	public String autoLogin(@RequestParam(value="token3") String token3) {
		return autoLoginService.autoLogin(token3);
	}
@Override
 public String autoLogin(String token3) {//token3,第三方token
     String re = null;

     if(checkToken3(token3)) {
         re = login();
     } else {
         re = "token未经授权";
     }

     return re;
 }

 private boolean checkToken3(String token) {
     boolean ok = false;

	 //checkUrl,验证第三方token网址
     String url = String.format("%s?token=%s", checkUrl,token);
     try {
         String re = HttpUtils.get(url);
         JSONObject jobj = JSONObject.parseObject(re);
         ok = jobj.get("code").toString().equals("200");
     } catch (Exception ex) {
         System.err.println(ex.getMessage());
     }

     return ok;
 }

 private String login() {
     String re = null;

	 //参照前端提交的参数
     Map<String, String> params = new HashMap<>();
     params.put("tenantCode", "10001");
     params.put("username", account);
     params.put("password", password);
     params.put("type", "account");
     params.put("scope", "ui");
     params.put("grant_type", "password");
     params.put("client_id", "browser");

     Map<String, String> heads = new HashMap<>();
     heads.put("Authorization", "巴拉巴拉巴拉");
     heads.put("Content-Type", "application/x-www-form-urlencoded");

     try {
     	 //提交给自己的接口,登录并返回access_token等。HttpUtils是自己写的静态类
         re = HttpUtils.post(String.format("http://localhost:%s/api/uaa/oauth/token", port), params, heads);
     } catch (Exception e) {
         re = e.getMessage();
     }

     return re;
 }