SSO单点登录-cookie实现

本文介绍单点登录(SSO)的基本概念及其基于Cookie的实现方案。SSO允许用户在一个系统登录后,无需再次登录即可访问其他互信系统。文章详细解析了利用Cookie存储用户凭证并实现免登录的具体流程。

单点登录及实现方案原理

单点登录概念:

​ 单点登录英文全称Single Sign On,简称就是SSO。它的解释是:在多个应用系统中,只需要登录一次,就可以访问其他相互信任的应用系统。例如,网页登录了淘宝账号,天猫,钉钉等阿里系应用都不用再二次登录了。 SSO核心意义就一句话:一处登录,处处登录;一处注销,处处注销。

一、基于cookie实现单点登录

这是最简单的单点登录实现方式,是使用cookie作为媒介,存放用户凭证。 用户登录父应用之后,应用返回一个加密的cookie,当用户访问子应用的时候,携带上这个cookie,授权应用解密cookie并进行校验,校验通过则登录当前用户。

缺点:

  1. Cookie不安全
  2. 不能跨域实现免登

对于第一个问题,通过加密Cookie可以保证安全性,当然这是在源代码不泄露的前提下。如果Cookie
的加密算法泄露,攻击者通过伪造Cookie则可以伪造特定用户身份,这是很危险的;对于第二个问题,不能跨域实现免登更是硬伤。因此,有了基于Session的单点登录。

cookie实现单点登录流程分析

需要做ip和域名映射:

#主系统
127.0.0.1	www.sso.com
#登录系统
127.0.0.1	login.sso.com
#vip子系统
127.0.0.1	vip.sso.com
#购物车子系统
127.0.0.1	cart.sso.com
  1. 用户首次登录,携带请求页url,跳转登录系统控制层接口

    @Controller
    @RequestMapping("view")
    public class ViewController {
        @Autowired
        RestTemplate restTemplate;
    
        //携带token跨服务请求登录服务接口获取已经登陆用户信息--若解决session共享则可以省略该步骤
        private final String LOGIN_INFO_ADDRESS="http://login.sso.com:9001/login/info?token=";
        @GetMapping("/index")
        public String toIndex(@CookieValue(required = false,value = "TOKEN")Cookie cookie, HttpSession session){
            if(null!=cookie){
                String token = cookie.getValue();
                if(StringUtils.isNotEmpty(token)){
                    Map result = restTemplate.getForObject(LOGIN_INFO_ADDRESS + token, Map.class);
                    session.setAttribute("loginUser",result);
                }
            }else {
                session.removeAttribute("loginUser");
            }
            return "index";
        }
    

    首页

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
    <head>
        <meta charset="UTF-8">
        <title>首页</title>
    </head>
    <body>
        <h1>这里是首页展示页面</h1>
        <span>
            <a th:if="${session.loginUser==null}" href="http://login.sso.com:9001/view/login?target=http://www.sso.com:9000/view/index">登录</a>
            <a th:unless="${session.loginUser==null}" href="http://www.sso.com:9000/view/exit">退出</a>
        </span>
        <p th:unless="${session.loginUser==null}">
            <span style="color: deepskyblue" th:text="${session.loginUser.username}"></span> 已登录
        </p>
    </body>
    </html>
    
  2. 判断cookie为空,直接跳转登录界面

    /**
     * 页面跳转逻辑
     */
    @Controller
    @RequestMapping("view")
    public class ViewController {
        /**
         * 跳转到登录页面
         *
         * @return
         */
        @GetMapping("/login")
        public String toLogin(@RequestParam(required = false, defaultValue = "") String target, HttpSession session,
                              @CookieValue(required = false, value = "TOKEN") Cookie cookie) {
            if (StringUtils.isEmpty(target)) {
                //未携带url设置默认页面
                target = "http://www.sso.com:9000";
            }
            if (cookie != null) {
                //如果是已经登录的用户再次访问登录系统时,就重定向到回页面
                String token = cookie.getValue();
                User user = LoginCacheUtil.loginUser.get(token);
                if (user != null) {
                    return "redirect:" + target;
                }
            }
            //未登录,跳转登录controller
            session.setAttribute("target", target);
            return "login";
        }
    }
    

    登录界面

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
    <head>
        <meta charset="UTF-8">
        <title>Login Module</title>
    </head>
    <body>
        <h1>欢迎来到登录页面</h1>
        <p style="color:red" th:text="${session.msg}"></p>
        <form action="/login" method="post">
            用户名:<input name="username" value=""/>
            密码:<input name="password" value=""/>
            <button type="submit">登录</button>
        </form>
    </body>
    </html>
    
  3. 登录页面表单提交,登录控制层接收用户信息并校验数据是否正确,用户存在,则将该用户信息保存(redis,或Map),创建cookie,将新建token存储在cookie中,并响应到HttpServletResponse中供其他系统访问,登录成功跳回请求系统控制层接口。

    判断是否登录,已登录判断用户存在session,跳转会请求页面

    @Controller
    @RequestMapping("view")
    public class ViewController {
        /**
         * 跳转到登录页面
         *
         * @return
         */
        @GetMapping("/login")
        public String toLogin(@RequestParam(required = false, defaultValue = "") String target, HttpSession session,
                              @CookieValue(required = false, value = "TOKEN") Cookie cookie) {
            if (StringUtils.isEmpty(target)) {
                //未携带url设置默认页面
                target = "http://www.sso.com:9000";
            }
            if (cookie != null) {
                //如果是已经登录的用户再次访问登录系统时,就重定向到回页面
                String token = cookie.getValue();
                User user = LoginCacheUtil.loginUser.get(token);
                if (user != null) {
                    return "redirect:" + target;
                }
            }
            //未登录,跳转登录controller
            session.setAttribute("target", target);
            return "login";
        }
    }
    

    未登录请求登录接口

    @Controller
    @RequestMapping("/login")
    public class LoginController {
        // 模拟数据库用户
        private static Set<User> dbUser;
        static {
            dbUser = new HashSet<>();
            dbUser.add(new User(0, "zhangsan", "1234"));
            dbUser.add(new User(1, "lisi", "1234"));
            dbUser.add(new User(2, "中国魂", "123456"));
        }
        @PostMapping
        public String doLogin(User user, HttpSession session, HttpServletResponse response) {
            String target = (String) session.getAttribute("target");
            // 模拟从数据库中通过登录的用户名和密码查找用户,
            Optional<User> first = dbUser.stream().filter(dbUser -> dbUser.getUsername().equals(user.getUsername()) &&
                    dbUser.getPassword().equals(user.getPassword())).findFirst();
            // 判断用户是否登录d
            if (first.isPresent()) {
                // 保存用户登录信息
                String token = UUID.randomUUID().toString();
                LoginCacheUtil.loginUser.put(token, first.get());
                session.removeAttribute("msg");
                //设置cookie
                Cookie cookie = new Cookie("TOKEN", token);
                //cookie在子系统间访问域要相同
                //cookie不能跨域
                cookie.setDomain("sso.com");
                /*
                cookie过期时间设置方式:
                cookie.setMaxAge(0);//不记录cookie
                cookie.setMaxAge(-1);//会话级cookie,关闭浏览器失效
                cookie.setMaxAge(60*60);//过期时间为1小时
                 */
                cookie.setMaxAge(-1);
                cookie.setPath("/");
                //通过HttpServletResponse将cookie响应到子系统
                response.addCookie(cookie);
            } else {
                //登录失败
                session.setAttribute("msg", "用户名或密码错误");
                return "login";
            }
            // 重定向到target地址
            return "redirect:" + target;
        }
    
  4. (用户回显)判断cookie不为空,从cookie中获取token,用token从redis中或者跨系统从Map中获取用户信息,并存入session,跳转请求页面显示登录用户。

  5. 子系统点击登录,在登录系统中判断cookie是否存在,存在则获取token,根据token获取登录用户信息,如果用户存在,则直接跳回请求的子系统页面。

    子系统页面

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
    <head>
        <meta charset="UTF-8">
        <title>购物车</title>
    </head>
    <body>
        <h1>这里是购物车展示页面</h1>
        <span>
            <a th:if="${session.loginUser==null}" href="http://login.sso.com:9001/view/login?target=http://cart.sso.com:9003/view/index">登录</a>
            <a th:unless="${session.loginUser==null}" href="http://cart.sso.com:9003/view/exit">退出</a>
        </span>
        <p th:unless="${session.loginUser==null}">
            <span style="color: deepskyblue" th:text="${session.loginUser.username}"></span> 已登录
        </p>
    </body>
    </html>
    

    登录逻辑

    @Controller
    @RequestMapping("view")
    public class ViewController {
        @Autowired
        RestTemplate restTemplate;
        //携带token跨服务请求登录服务接口获取已经登陆用户信息--若解决session共享则可以省略该步骤
        private final String LOGIN_INFO_ADDRESS="http://login.sso.com:9001/login/info?token=";
        @GetMapping("/index")
        public String toIndex(@CookieValue(required = false,value = "TOKEN")Cookie cookie, HttpSession session){
            if(null!=cookie){
                String token = cookie.getValue();
                if(StringUtils.isNotEmpty(token)){
                    Map result = restTemplate.getForObject(LOGIN_INFO_ADDRESS + token, Map.class);
                    session.setAttribute("loginUser",result);
                }
            }else {
                session.removeAttribute("loginUser");
            }
            return "index";
        }
    

    跨服务交互接口,从map中获取用户信息

        /**
         * 通过token获取用户登录信息
         * 跨系统交互
         */
        @GetMapping("/info")
        @ResponseBody
        public ResponseEntity<User> getUserInfo(String token) {
            if (StringUtils.isNotEmpty(token)) {
                User user = LoginCacheUtil.loginUser.get(token);
                return ResponseEntity.ok(user);
            }
            return new ResponseEntity(HttpStatus.BAD_REQUEST);
        }
    
    public class LoginCacheUtil {
        public static HashMap<String, User> loginUser = new HashMap<>();
    }
    
  6. 退出单点登录:设置cookie过期时间为0,就ok。

     //退出
        @GetMapping("/exit")
        public String toExit(HttpServletRequest request, HttpSession session, HttpServletResponse response) {
            Cookie[] cookies = request.getCookies();
            if (cookies != null && cookies.length > 0) {
                for (Cookie cookie : cookies) {
                    if ("TOKEN".equals(cookie.getName())) {
                        cookie.setDomain("sso.com");
                        cookie.setPath("/");
                        cookie.setMaxAge(0);
                        response.addCookie(cookie);
                    }
                }
            }
            return "redirect:/view/index";
        }
    

    pom文件

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <groupId>com.sso</groupId>
        <artifactId>sso-use-cookie</artifactId>
        <packaging>pom</packaging>
        <version>1.0-SNAPSHOT</version>
    
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.4.1</version>
            <relativePath/><!--指定pom.xml文件的相对路径,默认从当前pom文件的上一级目录找-->
        </parent>
    
        <!--所属子系统-->
        <modules>
            <module>sso-main</module><!--主系统-->
            <module>sso-vip</module><!--vip系统-->
            <module>sso-cart</module><!--购物车系统-->
            <module>sso-login</module><!--登录系统-->
        </modules>
    
        <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
            <project.version>1.1</project.version>
            <java.version>1.8</java.version>
            <!-- lombok -->
            <lombok.version>1.18.16</lombok.version>
            <commons-lang3.version>3.11</commons-lang3.version>
        </properties>
    
        <dependencies>
            <!--内嵌tomcat,包含starter依赖-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <!--支持单元测试依赖-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
            <!-- 工具包 -->
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
                <version>${commons-lang3.version}</version>
            </dependency>
            <!-- lombok简化实体代码 -->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
                <scope>provided</scope>
            </dependency>
            <!--页面模板-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-thymeleaf</artifactId>
            </dependency>
        </dependencies>
    
        <build>
            <pluginManagement>
                <plugins>
                    <plugin>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-maven-plugin</artifactId>
                    </plugin>
                </plugins>
            </pluginManagement>
        </build>
    </project>
    

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值