原文在这里:
单点登录系统SSO是如何实现的?mp.weixin.qq.com所谓单点登录就是在A系统登录以后,跳转到B系统,此时可以直接访问B系统的资源,而不需要二次登录,目前这种需求已经非常普遍了,那么背后是怎么实现的呢?本文将用一个实际的例子来给大家详细的讲解下。
准备工作
(1)准备3个域名来模拟3个站点,http://www.site1.com和http://www.site2.com是业务域,http://www.usercenter.com是用户中心。一般单点登录系统背后都有一个独立的用户中心。
当然我们不需要真的去万网申请3个域名,只需要修改下本机的host文件即可,修改C:WindowsSystem32driversetc,添加以下内容:
127.0.0.1 http://www.site1.com
127.0.0.1 http://www.site2.com
127.0.0.1 www.usercenter.com
(2)准备一个nginx,把三个域名都挂上
我们本机启动3个tomcat,端口分别是site1:8081,site2:8082,usercenter:8080,通过nginx统一用标准的80来访问。
server {
listen 80;
server_name www.usercenter.com;
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-Ip $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_pass http://127.0.0.1:8080;
}
}
server {
listen 80;
server_name www.site1.com;
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-Ip $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_pass http://127.0.0.1:8081;
}
}
server {
listen 80;
server_name www.site2.com;
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-Ip $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_pass http://127.0.0.1:8082;
}
}
现在我们就可以在本机用3个tomcat来模拟3个独立域名的服务器了。
原理分析
(1)浏览器访问site1,site1首先检查请求中是否带有site1的cookie,cookie的值是用户的token。如果有则向用户中心检查token的有效性,如果有效则可以直接访问site1,如果没有或者失效,重定向到用户中心去登录。
(2)浏览器进入用户中心的登录页面,用户输入用户名和密码,登录成功以后,用户中心需要写usercenter域的cookie,cookie的值是随机生成的一个token,然后重定向回到site1,同时把token作为url的参数给回传过去,因为cookie是usercenter域的,不是site1域的,只能通过url参数来传递。
(3)浏览器到了site1,还是首先检查cookie为空,然后需要检查参数中是否有用户中心设置的token参数,如果有,检查参数有限性,拿到用户信息,写site1的cookie,cookie的值是用户中心设置的token。
此时,如果浏览器继续访问site1的页面,会回传site1的cookie,只要去用户中心校验有效性即可。
如果,此时要跳转到site2,site2同样也要做:
(1)浏览器访问site2,site2首先检查请求中是否带有site2的cookie,cookie的值是用户的token。如果有则向用户中心检查token的有效性,如果有效则可以直接访问site2,如果没有或者失效,重定向到用户中心去登录。
(2)浏览器进入用户中心要做登录的时候,此时浏览器是传递了site1登录成功以后用户中心下发的cookie的,因为此时访问的是usercenter域,因此用户中心拿到cookie以后,只需要去校验有效性,校验通过则重定向回site2.
(3)浏览器到了site2,还是首先检查cookie为空,然后需要检查参数中是否有用户中心设置的token参数,如果有,检查参数有效性,拿到用户信息,写site2的cookie,cookie的值是用户中心设置的token。
此时,如果浏览器继续访问site2的页面,会回传site2的cookie,只要去用户中心校验有效性即可。
上面啰啰嗦嗦一大堆,看上去很绕很抽象,一团乱麻,还是来看代码吧,看代码就清爽多了,图就不画了,感觉帮助不大。
代码实现
本文我们用springboot+thymeleaf来实现,springmvc也是一样的道理。
site1访问需要登录的页面的时候:
@GetMapping("/main")
public String main(HttpServletRequest request, HttpServletResponse response, Model model) throws Exception {
//首先从cookie中取token,这里只能取site1的cookie
//首次访问肯是空,除非后面登陆成功以后设置过
String tk = getFromCookie(request);
if(tk != null) {
//验证token有效性
String username = getFromUserCenter(tk);
if(username != null) {
model.addAttribute("username", username);
return "main";
}else {
//如果失效,重新登录
String url = getFullUrl(request);
return "redirect:"+String.format(USER_CENTER_LOGIN_URL, URLEncoder.encode(url, "UTF-8"));
}
}else {
//从参数中取,如果从用户中心跳转回来,会在参数中传递token
tk = getFromParam(request);
if(tk != null) {
//还是要校验下参数的有效性
String username = getFromUserCenter(tk);
if(username != null) {
//生成自己站点下面的cookie
Cookie cookie = new Cookie(COOKIE_NAME, tk);
cookie.setMaxAge(Integer.MAX_VALUE);
response.addCookie(cookie);
//进入主页
model.addAttribute("username", username);
return "main";
}else {//说明token已经过期,进入用户中心做登录
String url = getFullUrl(request);
return "redirect:"+String.format(USER_CENTER_LOGIN_URL, URLEncoder.encode(url, "UTF-8"));
}
}else {
//进入用户中心做登录
String url = getFullUrl(request);
return "redirect:"+String.format(USER_CENTER_LOGIN_URL, URLEncoder.encode(url, "UTF-8"));
}
}
}
usercenter处理业务域的跳转和做登录:
当各个站定向用户中跳的时候,首先访问这个USER_CENTER_LOGIN_URL:
@GetMapping(value="/login")
public String to_login(HttpServletRequest request, Model model)throws Exception {
String redir_url = request.getParameter("redir_url");
//先看是否存在usercenter下面额cookie
//只要有一个站点登录成功过,其他的站点往用户中心跳的时候
//都会携带这个cookie,只要有效,就可以不用再次登陆
String tk = getFromCookie(request);
if(tk == null) {//去登陆
model.addAttribute("redir_url", redir_url);
return "login";
}else {//check是否有效
String username = getByToken(tk);
if(username == null) {
model.addAttribute("redir_url", redir_url);
return "login";
}else {//如果有效,直接跳回去
redir_url = addTokenToUrl(redir_url, tk);
return "redirect:"+redir_url;
}
}
}
在用户中心做登录:
@PostMapping(value="/login")
public String do_login(LoginUser loginUser, String redirect_url, HttpServletResponse res, Model model)throws Exception {
String username = loginUser.getUsername();
LoginUser userDB = getFromDB(username);
if(userDB == null) {
model.addAttribute("errmsg", "用户不存在");
model.addAttribute("redir_url", redirect_url);
return "login";
}
String pwdDB = userDB.getPassword();
if(!pwdDB.equals(loginUser.getPassword())) {
model.addAttribute("errmsg", "密码错误");
model.addAttribute("redir_url", redirect_url);
return "login";
}
//存redis,注意要设置有效期
String tk = UUID.randomUUID().toString();
redis.put(tk, userDB);
//生成用户中心的cookie
Cookie cookie = new Cookie(COOKIE_NAME, tk);
cookie.setDomain("www.usercenter.com");
cookie.setPath("/");
cookie.setMaxAge(Integer.MAX_VALUE);
res.addCookie(cookie);
//跳转回去
redirect_url = addTokenToUrl(redirect_url, tk);
return "redirect:"+redirect_url;
}
下面是用户中心校验token是否有效,有效就返回用户信息:
@GetMapping(value="/getByToken")
@ResponseBody
public String getByToken(String token)throws Exception {
//这里注意:延长下redis的有效期
LoginUser user = redis.get(token);
if(user != null) {
return user.getUsername();
}else {
return null;
}
}
代码其实还是挺简单的,主要的界面截图如下:
(1)浏览器访问http://www.site1.com/index.html,这个是个静态页面,不需要登陆:
this is site1 index<br/>
<input type="button" onclick="visitMain()" value="visit main page" />
<script>
function visitMain(){
window.location.href="http://www.site1.com/site1/main";
}
</script>
(2)点击visit main page按钮,访问site1站点下需要登录的页面,此时会重定向到usercenter,完整的url是
http://www.usercenter.com/user_center/login?redir_url=http%3A%2F%2Fwww.site1.com%2Fsite1%2Fmain:www.usercenter.com(3)输入用户名和密码做登陆,登录成功,跳转回site1,完整的url:
http://www.site1.com/site1/main?user_center_tk=19d89eb0-b135-492f-a684-2a8fc6f7e859,注意:参数中传递了token:www.site1.com此时,刷新页面的的话,site1是可以拿到cookie中的token的,只需要去检验token有效性即可。
欢迎你:<span th:text="${username}"></span>,这是site1的主页面<br/>
<input type="button" onclick="goToSite2()" value="go to site2"/>
<script>
function goToSite2(){
window.location.href="http://www.site2.com/site2/main";
}
</script>
(4)点击页面上go to site2按钮,会直接进入site2的主界面而不需要做登录,因为site2首先是去用户中心,用户中心会拿到之前设置的cookie,校验通过以后,会跳转回site2,注意看site2的url:
http://www.site2.com/site2/main?user_center_tk=19d89eb0-b135-492f-a684-2a8fc6f7e859:www.site2.com<span th:text="${username}"></span>,这是site2的主页面<br/>
<input type="button" onclick="goToSite1()" value="go to site1"/>
<script>
function goToSite1(){
window.location.href="http://www.site1.com/site1/main";
}
</script>
以上就是整个项目的实现,主要的点在于cookie不能跨域,A站只能设置A站的cookie,也只能读取A站的cookie,明白了这个就ok了。退出的代码就不写了,在site1退出的时候,首先要删除site1的cookie,然后要删除用户中心的redis即可。大家有兴趣可以自己实现下。
完整的代码下载:扫码关注文章开头的公众号看原文
代码仅仅是为了展示整个流程,如果是在实际项目中使用可以在业务域上用拦截器之类的来做。