近期有个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);}
@Overridepublic String autoLogin(String token3) {//token3,第三方tokenString 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;}