最近没事想自己写一套SSO的单点登录机制,于是再Sa-token上面学习了一下,Sa-token很优秀的实现了一套完整的单点登录机制,供用户进行使用。我选择其中的模式二,即客户端不同源,Redis同源的认证策略,进行了源码阅读和学习,这也是最常用的单点登录模型。
客户端不同源,认证中心Redis同源
首先讲一下,什么叫做同源和不同源,这个其实很容易理解,通俗来说就是是否在同一个域名下。
举个例子吧
客户端1:client1-sso.com
客户端2:client2-sso.com
认证中心:server-sso.com
我们知道客户端的是否登录这个机制,通常是通过cookie来完成的,现在如果client1跳转到认证中心登录,那么认证中心就会向client1中写入一个cookie,但是因为client1和client2不同源,这就带来了比较大的麻烦,cookie不共享啊!这咋办呢,sa-token是这样解决的。
- 用户访问客户端1,进入client1-sso.com,提示用户进行登录
- 用户点击登录,跳转到http://client1-sso.com/sso/login?back=http://client1-sso.com/,客户端对该路由进行检查,重定向到认证中心
- 用户进入认证中心http://server-sso.com/sso/auth?redirect=http://client1-sso.com/sso/login?back=http://client1-sso.com/,在这里就会有对应操作了,如果用户没有登录,那么认证中心应用程序的cookie里面必然就没有记录token字段,那么此时,就会跳转到认证中心登录界面
- 用户输入用户名和密码进行登录,登录成功后,就会往当前会话的认证中心中写入cookie。到这里为止,其实用户就已经完成了认证中心的单点登录了。
- 那么接下来,我们再次回到之前的client1,此时同样,我们还是要点击登录按钮,重复上述的操作。注意到第三步时,此时认证中心已经登录,就有了cookie,那么直接拿到了用户信息,同时认证中心生成一个ticket码,将这个ticket放到redis里面去。然后进行重定向,跳转到redirect记录的客户端登录地址http://client1-sso.com/sso/login?back=http://client1-sso.com/&ticket=xxxxxxxxx
- 客户端发现登录地址里面有ticket,就会立马连接到远程的redis上面,进行ticket对比,如果无误,就可以再进行检查拿到ticket里面的登录信息。客户端就会进行登录操作,将token写入客户端应用程序的cookie中。
- 自动登录完成后,cookie中也记录登录信息,用户返回到back记录的主页面。
那么到此为止,单点登录就已经完成了,从第5步到第7步,这些操作,实际上都是自动完成的。
再说一下,sa-token是如何实现注销的吧,其实明白了登录如何实现的,注销的原理就非常简单了。目的是客户端选择注销之后,用户需要认证中心重新登录。
1、用户在客户端点击注销,首先在客户端应用程序上删除token,这样就无法通过认证了
2、最关键的第二步,是要让认证中心的token也失效,sa-token中认证中心和客户端共用一个token存储库,实现的方式是将这个token从存储库中移除,就无法识别了,需要重新认证。
代码操作
讲了原理部分,那就看看sa-token是如何实现吧。看源码其实对于很多原理的理解会有很多帮助,实际上,通过这种图的形式来讲解原理是抽象的,当你阅读了源码之后,就会有很深刻的理解。更重要的地方在于,你可以按照别人的思路实现自己的一套SSO单点登录系统。
- 客户端登录接口
首先根据客户端登录接口,http://client1-sso.com/login,一般会有可能出现两个参数,一个是back,这个是必须要有的,相当于客户端登录成功后返回的主页url。另一个是ticket,这个可有可无。
如果没有ticket,说明这个接口客户点击过来的,那么就需要重定向到认证中心去。
如果有ticket就说明这个是认证中心登录成功后返回的,此时就可以拿着ticket去redis中拿用户信息loginId,然后直接用这个loginId进行登录,生成token,存到客户端的cookie中。
在这里注意的是,ticket的校验机制,客户端应该与认证中心生成ticket的机制相对应。
/**
* SSO-Client端:登录地址
* @return 处理结果
*/
public static Object ssoLogin() {
// 获取对象
SaRequest req = SaHolder.getRequest();
SaResponse res = SaHolder.getResponse();
SaSsoConfig cfg = SaSsoManager.getConfig();
StpLogic stpLogic = SaSsoUtil.saSsoTemplate.stpLogic;
// 获取参数
String back = req.getParam(ParamName.back, "/");
String ticket = req.getParam(ParamName.ticket);
// 如果当前Client端已经登录,则无需访问SSO认证中心,可以直接返回
if(stpLogic.isLogin()) {
return res.redirect(back);
}
/*
* 此时有两种情况:
* 情况1:ticket无值,说明此请求是Client端访问,需要重定向至SSO认证中心
* 情况2:ticket有值,说明此请求从SSO认证中心重定向而来,需要根据ticket进行登录
*/
if(ticket == null) {
String serverAuthUrl = SaSsoUtil.buildServerAuthUrl(SaHolder.getRequest().getUrl(), back);
return res.redirect(serverAuthUrl);
} else {
// ------- 1、校验ticket,获取 loginId
Object loginId = checkTicket(ticket, Api.ssoLogin);
// Be: 如果开发者自定义了处理逻辑
if(cfg.getTicketResultHandle() != null) {
return cfg.getTicketResultHandle().apply(loginId, back);
}
// ------- 2、如果 loginId 无值,说明 ticket 无效
if(SaFoxUtil.isEmpty(loginId)) {
throw new SaSsoException("无效ticket:" + ticket).setCode(SaSsoExceptionCode.CODE_20004);
} else {
// 3、如果 loginId 有值,说明 ticket 有效,此时进行登录并重定向至back地址
stpLogic.login(loginId);
return res.redirect(back);
}
}
}
- 认证中心授权地址
接下来,我们看看认证中心的授权地址http://server-sso.com/sso/auth,通常会带着一个redirect参数,表示登录成功会跳转回的客户端地址,所谓授权地址,就是检查用户有没有登录的。认证中心会查找当前会话中有没有cookie,然后获取其中token信息,拿到对应的loginId。
如果没有token,或者并没有获取到loginId,那么说明并没有登录,那么自然而然就会跳转到认证中心的登录地址进行登录。
如果获取到了loginId,那就说明登录成功了,那么认证中心,就用这个loginId作为标志,生成一个ticket存到redis中。然后重定向到redirect参数中的客户端地址,并再带上一个参数ticket,这个ticket会在客户端用于去redis中获取loginId。
/**
* SSO-Server端:授权地址
* @return 处理结果
*/
public static Object ssoAuth() {
// 获取对象
SaRequest req = SaHolder.getRequest();
SaResponse res = SaHolder.getResponse();
SaSsoConfig cfg = SaSsoManager.getConfig();
StpLogic stpLogic = SaSsoUtil.saSsoTemplate.stpLogic;
// ---------- 此处有两种情况分开处理:
// ---- 情况1:在SSO认证中心尚未登录,需要先去登录
if(stpLogic.isLogin() == false) {
return cfg.getNotLoginView().get();
}
// ---- 情况2:在SSO认证中心已经登录,需要重定向回 Client 端,而这又分为两种方式:
String mode = req.getParam(ParamName.mode, "");
// 方式1:直接重定向回Client端 (mode=simple)
if(mode.equals(SaSsoConsts.MODE_SIMPLE)) {
String redirect = req.getParam(ParamName.redirect);
SaSsoUtil.checkRedirectUrl(redirect);
return res.redirect(redirect);
} else {
// 方式2:带着ticket参数重定向回Client端 (mode=ticket)
String redirectUrl = SaSsoUtil.buildRedirectUrl(stpLogic.getLoginId(), req.getParam(ParamName.redirect));
return res.redirect(redirectUrl);
}
}
- 认证中心登录地址
这个接口http://server-sso.com/sso/doLogin相对于来说,就比较简单了。用户输入用户名和密码在认证中心进行登录,登录成功后,将token放到当前应用程序的cookie中就可以了。cookie中有了token信息,后面再跳转到认证中心授权地址的时候,就不用再重复登录。
/**
* SSO-Server端:RestAPI 登录接口
* @return 处理结果
*/
public static Object ssoDoLogin() {
// 获取对象
SaRequest req = SaHolder.getRequest();
SaSsoConfig cfg = SaSsoManager.getConfig();
// 处理
return cfg.getDoLoginHandle().apply(req.getParam(ParamName.name), req.getParam(ParamName.pwd));
}
文章的最后,真的很佩服有些大佬,Sa-token的作者真的很牛逼啊,这是一套非常完整的认证鉴权框架,实现的非常完美。不过使用上太过简单了,对于使用者而言,理解原理只能阅读源码了。