SSO单点登录-基于cookie的单点登录

1.概述

单点登录(Single-Sign-On),简称SSO,它的解释为:在多个应用系统中,只要登陆一次,便可以访问其它相互信任的系统。早期系统由于只有一个服务,因此只需要登录一次,就可以访问系统的其它资源。伴随着业务的发展和用户数量的增加,单系统局限性越来越突出(无法支撑大规模用户、用户数量过多系统卡顿等)。为了增强系统的并发能力和解耦合,进行了系统业务的拆分,系统业务拆分后,为了保护系统之间数据安全性,用户需要登录认证才能进行资源访问。若资源分散在不同的服务上,每访问一次都需要重新登录,这会极大地降低用户体验感。解决上述问题的其中一种思路便是在一个系统登录,系统认证成功并返回一个令牌,用户访问其它系统时也携带该令牌,每个系统校验该令牌即可,若校验通过,便允许访问,反之拒绝访问请求,这便是早期单点登录的设计方案。

1.1 普通系统登录原理

在分析单点登录前,首先讲一下普通系统的登录原理。
在这里插入图片描述
如上图所示,当我们在浏览器(Browser)中访问一个应用时,用户需要完成登录认证(输入用户名和密码等),当认证成功后,在服务端(Server,这里CloudApp表示部署在云上的Server)的session里会标记该用户的登陆状态为yes(已登录),同时会往访问的浏览器(Browser)写入一个cookie,这个cookie就是当前该用户登录的一个标识,用户每次访问服务时都会携带该cookie,服务端会根据该cookie找到对应的session,然后判断用户的状态。tomcat部署的服务,默认登录后会返回一个名叫jsessionid的cookie,jsessionid对应的的cookie值就是改用户在服务器中的sessionid,该值具有唯一性。

1.2 单点登录

单点登录分为:同根域单点登录、不同根域下单点登录

  • 同根域下单点登录:一般一个企业只会有一个主域名,通过二级域名区分子系统。举个例子:一个企业中有三个子系统(app1.test.com、app2.test.com、sso.test.com),只要在sso.test.com系统中完成了登录验证,同时便在app1.test.com、app2.test.com中完成了登录。由1.1节可知,当用户在sso.test.com完成后,服务器会返回一个cookie(携带sessionid),用户下次访问时,会携带该cookie值服务器,服务器会根据cookie中的sessionid判断用户状态。由于浏览器是不能跨域的,因此sso.test.com中对应的cookie无法被携带至app1.test.com、app2.test.com。
    解决该问题的方案:将coockie的域设置为顶域,即.test.com,这样所有子域的系统都可以访问到顶域的cookie。我们在设置cookie时,只能设置顶域和自己的域,不能设置其他的域。解决了cookie共享问题,还有一个服务端session共享问题,由于是不同的服务,那么session如何共享呢?或许有一些常用的方法:

1.Tomcat集群Session全局复制(当数据量较大时,会影响tomcat性能,不建议);
2.根据请求的IP进行Hash映射到对应的机器上(这就相当于请求的IP一直会访问同一个服务器)【如果服务器宕机了,会丢失了一大部分Session的数据,不建议】
3.把Session数据放在Redis中(使用Redis模拟Session)

  • 不同根域下单点登录:这里举个简单的例子,www.taobao.com和www.jd.com是两个不同的网站,浏览器不会把taobao.com的cookie带到jd.com下,因此无法实现cookie共享,这是因为不同的域名之间无法实现cookie共享(基于浏览器的安全机制)。针对这种情况要实现单点登录,需要借鉴CAS(Central Authentication Service)的设计思想。CAS的设计思路主要如下图所示:
    在这里插入图片描述
    上述流程的简单描述为:

1.用户访问某一个系统(假设该系统为App1),App1需要进行登录,用户现在处于未登录状态;
2.此时App1会跳转到CAS服务端(即SSO登录系统),SSO系统判断用户未登录,弹出登陆页面;
3.用户输入用户名、密码等,SSO系统认证成功后,将登录状态写入session中,同时将sessionid写入SSO域下的Cookie中,用户登录成功之后,SSO会生成一个ST(Service Ticket),然后重定向到App1系统(携带ST),App1系统拿到ST后,会从后台请求SSO服务,验证ST是否有效,验证成功后,App1系统将登录状态写入session并设置app域下的Cookie,并返回用户信息。

此时用户已完成登录,当再次访问App1时,会保持登录状态。当用户去访问系统App2时,由于App2未登录,App2同样会去访问SSO系统,流程如下:

1.App2跳转到SSO系统;
2.SSO已经登录且生成了ST,SSO携带ST跳转到App2系统;
3.app2拿到ST,后台访问SSO,验证ST是否有效,验证成功后,App2将登录状态写入session,并在App2域下写入Cookie。 用户访问App2系统,App2系统没有登录,跳转到SSO。

此处需要注意的是:SSO的客户端可以是多个。

2.基于cookie的小案例

2.1 设计思想

本文将基于上述CAS的设计思想,实现一个简单的小案例。案例的需求如下:有一个购物车功能和一个首页功能,这两个功能分别属于两个系统,用户登录之后才能访问首页功能和购物车功能,只需登录一次便可访问两个系统的功能。
这里包含的系统如下图所示:
在这里插入图片描述
用户访问流程如下图所示:

请添加图片描述
将上图的流程进行拆分,得到以下几点:
1.当用户访问购物车系统时(该系统需要登录才能访问),用户未登录访问,于是重定向到SSO登陆系统(携带系统地址www.cart.com作为参数),请求地址及形式大致如下:

www.sso.com?target=www.cart.com;

2.SSO登录系统判断用户未登录,会跳转到登录页面,用户输入账号、密码等信息进行登录,登录成功之后会生成token,并写入到cookie中,保存至浏览器;随后,SSO会重定向至target地址(携带token),重定向地址及形式如下:

www.cart.com?token=xxxxxxx

3.此时购物车系统会携带返回回来的token到SSO系统进行验证,验证成功之后,建立会话,修改用户登陆状态;

4.与此同时,用户想要访问首页系统(www.index.com),由于用户并未在首页系统登录,于是会重定向到SSO登陆系统,请求的地址及方式如下:

www.sso.com?target=www.index.com;

5.由于在购物车系统已完成登录,即浏览器与www.sso.com已建立了会话(携带cookie至浏览器),此时浏览器会携带cookie至认证中心www.sso.com,浏览器根据cookie中的信息判断已建立会话,SSO登陆系统会携带token重定向至www.index.com,重定向地址及形式如下:

www.index.com?token=xxxxxxx

6.首页系统携带token去SSO登录系统认证,如验证通过,则允许访问,修改用户登录状态。

2.2 部分代码

案例需要映射几个不同的域名来模仿不同的系统,可以使用nginx来进行实现,我这里之间在windows的hosts文件中加了如下几行配置:

127.0.0.1  www.login.test.com
127.0.0.1  www.cart.test.com
127.0.0.1  www.index.test.com
127.0.0.1  www.test.com

2.2.1 购物车控制器部分代码

package com.eckey.lab.controller;

import com.eckey.lab.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.client.RestTemplate;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;

@Slf4j
@Controller
@RequestMapping("/cart")
public class CartController {

    @Autowired
    private RestTemplate restTemplate;

    private final String USER_LOGIN_ADDR = "http://www.login.test.com:8081/login/info?token=";

    @GetMapping()
    public String toIndex(@CookieValue(required = false, value = "token") Cookie cookie, HttpServletRequest request) {
        if (cookie != null) {
            String value = cookie.getValue();
            if (!StringUtils.isEmpty(value)) {
                ResponseEntity<User> result = restTemplate.getForEntity(USER_LOGIN_ADDR + value, User.class);
                User user = result.getBody();
                request.getSession().setAttribute("user", user);
            }
        }
        return "cart";
    }
}

2.2.2 cart.html页面代码

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <title>Cart</title>
</head>
<body>
<h1>欢迎来到购物车页面</h1>
<span>
    <a th:if="${session.user == null}"
       href="http://www.login.test.com:8081/view/login?target=http://www.index.test.com:8082/index">登录</a>
    <a th:unless="${session.user == null}" href="http://www.login.test.com:8081/login/logout">退出</a>
</span>
<p th:unless="${session.user == null}">
    <span style="color: red;" th:text="${session.user.userName}"></span>已登录
</p>
</body>
</html>

2.2.3 登录控制器

package com.eckey.lab.controller;

import com.alibaba.fastjson.JSON;
import com.eckey.lab.entity.User;
import com.eckey.lab.utils.LoginUsers;
import com.eckey.lab.utils.UserUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import com.eckey.lab.utils.EncryptAndDecryptUtil;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

@Slf4j
@Controller
@RequestMapping("/login")
public class LoginController {

    @Autowired
    private UserUtils userUtils;

    @PostMapping
    public String login(User user, HttpServletRequest request, HttpServletResponse response) throws IOException {
        HttpSession session = request.getSession();
        String loginUrl = (String) session.getAttribute("loginUrl");
        log.info("loginUrl:{}", loginUrl);
        loginUrl = "http://www.index.test.com:8082/index";
        if (userUtils.contain(user)) {
            log.info("用户登录成功:{}", JSON.toJSONString(user));
        } else {
            session.setAttribute("msg", "用户" + user.getUserName() + "登录失败");
            return "login";
        }
        log.info("用户登录信息为:{}", JSON.toJSONString(user));
        if (user == null) {
            log.error("用户未填写登录名和密码");
            return "error";
        }
        if (user.getPassword() == null || user.getPassword().equals("")) {
            log.error("password不能为空!");
            return "error";
        }
        if (user.getUserName() == null || user.getUserName().equals("")) {
            log.error("username不能为空!");
            return "error";
        }
        session.setAttribute("user", user);
        //todo 应结合加密代码生成token,此处为了演示,做简单生成策略
        long currentTimeMillis = System.currentTimeMillis();
        String token = user.getUserName() + "-" + currentTimeMillis;
        String encryptStr = EncryptAndDecryptUtil.base64Encrypt(token);
        Cookie cookie = new Cookie("token", encryptStr);
        cookie.setDomain("test.com");
        response.addCookie(cookie);
        LoginUsers.add(encryptStr, user);
        response.sendRedirect(loginUrl);
        return "success";
    }

    @GetMapping("/logout")
    public String loginOut(@CookieValue(required = false, value = "token") Cookie cookie, HttpServletRequest request) {
        HttpSession session = request.getSession();
        if (cookie == null) {
            log.info("退出失败,未登录!");
            session.setAttribute("msg", "退出失败,未登录");
            return null;
        }
        String token = cookie.getValue();
        if (StringUtils.isEmpty(token)) {
            log.info("退出失败,未登录!");
        } else {
            boolean remove = LoginUsers.remove(token);
            if (remove) {
                log.info("退出成功!");
            }
        }
        return "redirect:" + "/view/login";
    }

    @GetMapping("/info")
    @ResponseBody
    public User getToken(@RequestParam("token") String token) {
        if (StringUtils.isEmpty(token)) {
            return null;
        }
        User user = LoginUsers.USERS.get(token);
        return user;
    }
}

2.2.4 login.html

<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org/">
<head>
    <meta charset="UTF-8">
    <title>登陆页面</title>
</head>
<body>
<h1>登陆页面</h1>
<p style="color: red" th:text="${session.msg}"></p>
<form method="post" action="/login">
    用户名:<input name="userName" value="" type="text" id="userName">
    登陆密码:<input name="password" value="" type="password" id="password">
    <button type="submit">提交</button>
</form>
</body>
</html>

具体详细代码可见章节6。

3.小结

1.SSO的核心在于登录系统的剥离以及系统之间的登录验证,业务系统拿到token后需要再一次调用SSO完成进一步验证,确保token真实性;
2.CAS的设计思想兼顾了功能与安全性,唯一的缺陷在于需要系统之间多次信息交换验证;
3.也可以考虑将用户登录信息防在redis缓存中,设置过期时间,有请求时不断延长时间,这种方式的缺陷在于当用户数量过于庞大时,缓存压力较大,且当redis宕机时,所有的用户登录信息都会消失,用户全部需要重新登录。

4.参考文献

1.https://juejin.cn/post/6844903845424971783
2.https://www.jianshu.com/p/a58c559bf0e1
3.https://blog.51cto.com/sinonqu/1575944
4.https://www.jianshu.com/p/75edcc05acfd

5.声明

本文章为原创文章,若转载,请声明!码字不易,请尊重知识!参考文章及思路已在章节4中标出,在此表示感谢。

6.案例地址

https://gitee.com/Marinc/springboot-demos/tree/master/sso-cookie

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
SSO (Single Sign-On) 单点登录是指在访问多个系统或应用程序时,用户只需登录一次就可以访问所有的系统,而无需再次输入用户名和密码。基于cookie二级域名下跨域共享是指在跨域访问的情况下,通过设置cookie的域名和路径,使得不同域名下的系统能够共享登录状态。 具体来说,当用户成功登录一个系统后,该系统会生成一个包含用户登录状态的cookie,并设置该cookie的域名为当前系统的二级域名。然后,该cookie会被发送给浏览器保存,在用户访问其他系统时,浏览器会自动通过cookie将用户的登录状态传递给其他系统。 为了实现跨域共享,所有需要实现SSO的系统的二级域名需要设置为相同的根域名。例如,系统A的域名为a.example.com,系统B的域名为b.example.com,则它们的根域名为example.com。为了在这两个系统之间实现跨域共享,可以将cookie的域名设置为.example.com,这样两个系统就可以共享同一个cookie。 当用户访问系统A时,系统A会检查是否存在含有登录状态的cookie,如果存在则表示用户已经登录,可以直接访问系统A的资源。如果用户访问系统B,系统B也会检查是否存在含有登录状态的cookie,如果存在则表示用户已经登录,可以直接访问系统B的资源。 通过基于cookie二级域名下跨域共享的方式,SSO单点登录实现了用户在不同系统间的无缝登录体验,提高了用户的使用便捷性和系统的安全性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值