java实现 SSO 单点登录(最终版)
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.****.net/zhangjingao/article/details/89052764
前言
前面我写了一篇文章,java实现完全跨域SSO单点登录,最后我会比较两种方案。
那篇文章主要说明完全跨域SSO单点登录的实现,但是我最终并没有使用那篇,当然,那篇完全可以实现SSO跨域,但是那篇有一些不太优雅的地方,我综合我的场景等各方面考虑,最终选择了我下面的这个方案。因为那篇并没有被选用,所以代码大家可以随意看,但是下面这个方案因为代码已经在使用,所以不太方便分享代码,见谅。大家有什么问题,欢迎留言或者qq私我,如果是qq(1763608200)私我,建议加验证信息:sso探讨(内心os: 不加验证信息我不会通过的,最近信息泄露太严重了,好多头像是杂七杂八的不知道是卖茶还是干啥的老加我 )
同父域单点登录
什么是SSO
SSO(Single Sign On)单点登录是实现多个系统之间统一登录的验证系统,简单来说就是:有A,B,C三个系统,在A处登录过后,再访问B系统,B系统就已经处于了登录状态,C系统也是一样。举个生活中栗子:你同时打开天猫和淘宝,都进入login界面,都要求你登录的,现在你在淘宝处登录后,直接在天猫处刷新,你会发现,你已经登录了,而且就是你在淘宝上登录的用户。说明他们实现了SSO,并且持有相同的信息。
当然这个特性意味着它的使用场景是:同一公司下的不同子系统,因为对于SSO来说,每一个子系统拥有的信息都一样,是用户的全部信息,如果是不同公司,那这肯定不合适。
这套方案我们要实现的效果
假设此时有A,B,C三套系统,他们有相同的*域名,就是说他们是同一个域名的子域名,此处使用a.xxx.com,b.xxx.com,c.xxx.com来表示,有一个独立的验证中心,使用sso.xxx.com表示。当我们在a.xxx.com下登录后,我当前的浏览器访问b.xxx.com依然能直接访问,并且已经处于登录状态。当我在A系统中注销登录时,B系统也会被注销登录。但是如果是b.yyy.com在该方案下就不支持了。
具体流程
初始A,B,C全部处于未登录状态,没有令牌(tokenid)。
1、用户访问A系统,没有令牌,A系统验证未登录,直接返回A的登录界面;
2、A输入账号密码进行登录请求到SSO中(附带重定向url);
3、然后SSO验证用户信息正确,生成身份令牌,将身份令牌转为jwt,将用户信息使用AES加密,将jwt身份令牌和用户信息作为键值对存进redis,时间设为30分钟,将身份令牌种进cookie,重定向回携带的url(到达了子系统);
4、子系统发现有身份令牌,发送请求到达SSO验证令牌真伪;
5、SSO收到验证请求,验证通过,返回AES加密的用户信息;
6、子系统收到通过信息,将用户信息解密,存进session一分钟(token),跳转页面至首页或用户请求页;
7、一分钟失效后或者子系统访问时,发现有令牌但session中无信息,再次重复4,5,6。
解释几个疑问点
1、sso中redis存储的是全局会话,而每个子系统中session存储的是临时会话。
2、为什么临时会话定义一分钟?
定义一分钟,就是为了当A系统退出后,注销了本地临时会话,再到sso注销全局会话,一分钟的话即使不去注销B系统的临时会话,也很快就会消失,这个时间可以忽略,再者一分钟可以保证子系统和sso实时保证用户信息的统一,假设此时该用户修改了个人信息,其他系统也能保证尽快得到最新的信息。
3、为什么sso中要用redis存储身份令牌呢?为什么不用session呢?
首先各个子系统验证身份令牌时是由各个子系统发出的,不一定是登录的那个域名了,所以无法保证session还能跟登录的那个session一致,所以使用redis保存,这样身份令牌和sso的redis之间也模拟出来一个全局会话关系出来。
4、为什么要用JWT呢?
是为了实现数字签名,如果黑客窃取到身份令牌,暴力枚举令牌,是有机会泄露的,使用JWT后,sso可以验证这是否被篡改。
5、为什么要重定向回子系统?
首先A系统登录时是在自己的登录界面,然后直接提交到sso验证中心,这个时候这个请求是浏览器和sso建立的联系,跟子系统没关系,直接返回或者转发什么的是没办法回到子系统的。
6、为什么登录要直接提交到SSO验证中心?(内心os:我感觉我快成了杠精,可能我适合去工地,2333 )
因为身份令牌要保存在浏览器客户端,保存在cookie中,也就是说sso此时要拿到浏览器请求才能给浏览器的父域种下cookie,如果是通过子系统再发出请求,那样是种不了cookie的,当然如果是登录请求提交到子系统,子系统发出验证,由子系统去种cookie也是可以的,但是这样的话你内聚就降低了很多,耦合度也高了,子系统原本不用管的登录的事也要管了,sso原本要全管的登录被别人干了一部分。
简单看下代码
SSO的统一登录代码
/**
* 统一验证登录中心
* @param username 用户名
* @param password 密码
*/
@PostMapping
public void checkLogin(String username, String password, String returnUrl,HttpServletResponse response) {
TbUser user = userService.login(username, password);
log.info("user: "+user);
String jwtValue = null;
if (user != null) {
/*
* 获得uuid,作为tokenId(TGC)
* 将tokenId存进jedis,返回客户端以jwt存储的tokenId,
* 即使tokenId被截取也无所谓
*/
String tokenId = UUID.randomUUID().toString();
//生成jwt
jwtValue = new JwtUtil(tokenId, null).creatJwt();
log.info("tokenId: " + tokenId+" jwt: "+jwtValue);
if (jwtValue != null) {
Jedis jedis = jedisPool.getResource();
jedis.set(tokenId, user.toString());
jedis.expire(tokenId,1800);
log.info("查看key的剩余生存时间:"+jedis.ttl(tokenId));
}
}
//将jwt加密的TGC存进cookie
Cookie cookie = new Cookie("tokenId", jwtValue);
cookie.setPath("/"); //设置根域名
cookie.setHttpOnly(true);
response.addCookie(cookie);
try {
response.sendRedirect(returnUrl);
} catch (Exception e) {
e.printStackTrace();
}
}
子系统使用身份令牌验证
/**
* 验证wlId
* 用jedis的原因是不用的话无法从所有session会话中找到sessionid为tokenid的会话
* @param wlId wlId
* @return 如果失效返回null,反之返回aes加密的用户信息
*/
@GetMapping
public String tokenCheck (String wlId) {
//从jedis中获得token
Jedis jedis = jedisPool.getResource();
String user = null;
if (wlId != null) {
wlId = new JwtUtil(null, wlId).decryptJwt();
user = jedis.get(wlId);
jedis.expire(wlId,1800);
log.info("tokencheck查看key的剩余生存时间:"+jedis.ttl(wlId));
}
return user;
}
退出登录的方法
/**
* 清除全局会话,子系统跳转到回到登录页的方法
* @param wlId 身份令牌
*/
@GetMapping("/loginout")
public void loginOut(String wlId) {
log.info("clear token");
Jedis jedis = jedisPool.getResource();
jedis.del(wlId);
}
与我另一篇完全跨域sso对比
首先附上篇文章链接:java实现完全跨域SSO单点登录
1、在以后你们的系统可能短时间内不会使用多个根域名的话,建议使用该版本或者修改该版本。
2、两者都可以实现单点登录,我已经自己试验过了。并且另一个版本有github代码提供。
3、上篇是完全跨域SSO,即使以后使用不同的域名,完全没问题。该版本只适用于同父域单点登录。
4、上篇虽然是完全跨域sso,但是sso需要多维护一个子系统表,用来记录向子系统添加cookie的链接,该篇不需要。
5、当以后用户量大,业务拓展的时候,第一个登录的子系统不仅要使用更多的时间验证用户是否正确,并且还要花时间对那么多个子系统进行重定向种cookie,虽然可以先跳转到用户首页再去请求,但是也会造成第一个登陆的登录速度慢一些,用户体验可能就不那么好。