单点登录原理及代码实现

   这次废话少说,直接扔干货,自己也是刚刚学习的单点登录,如有问题,还请评论进行一起探讨。

一、单系统登录机制

1、http无状态协议

   web应用采用的B/S架构,http作为通信协议。由于http本身是无状态协议,不存储任何登录信息,所以每次登录系统服务器之间都无任何关联,服务器都会进行独立处理操作。从网上盗个图来给大家进行解释下,下图将进行这个过程说明:在这里插入图片描述   这也就意味着任何用户都可以进行访问服务器资源,如果想进行保护服务器的资源,那么就需要进行限制对服务器的请求;要想限制对服务器的请求,那么就需要进行鉴别服务器的请求,响应合法请求,忽略非法请求;要想进行鉴别是否合法请求,那么就需要清楚浏览器的状态。由于http协议无状态,那么就需要浏览器和服务器进行共同维护一个状态,这就是会话机制。

2、会话机制

   当浏览器第一次进行请求服务器时,服务器会创建一个会话,并将会话id作为响应浏览器的一部分,浏览器并且会存储这个会话id;当浏览器进行第二次第三次第n次进行访问浏览器时,请求上会带上会话id,服务器得到会话id时会进行判断是不是同一个用户,后续请求就会和第一次产生了关联;下图就会进行说明:在这里插入图片描述   服务器将在内存中进行保存会话id,那么浏览器中将在哪里保存呢,它有两种:
   1、在请求参数中进行保存会话id
   2、在cookie中进行保存
   将会话id作为每一个请求的参数,服务器接收请求并且能进行解析获得会话id,来进行判断是否是同一用户,很明显的可以看出这种方式不安全。那就用浏览器自身来进行维护会话id,每发一次请求时都进行自动发送会话id,cookie机制正好可以来做这件事,cookie本身就是一种来进行存储少量数据的机制,数据以“key/value”键值对来进行存储,浏览器请求时自动带上cookie信息。
   Tomcat会话机制当然也实现了cookie,访问tomcat服务器时,浏览器中会看到一个名为“JSESSIONID”的cookie,这个就是浏览器维护的会话id,如图所示:在这里插入图片描述   

3、登录状态

   刚刚学习了会话机制,那么登录状态就很容易明白,我们当浏览器第一次请求时,输入用户名和密码,会将用户名和密码拿去跟数据库进行比较,比较正确的话,将记录为合法用户,否则记录为非法用户,合法用户将进行标记为“已授权”或“已登录”等状态,既然是会话状态,那么自然保存在会话对象中,tomcat会话在会话对象中设置登录状态如下:

HttpSession session = request.getSession();
session.setAttribute("isLogin", true);

   用户再次登录时,tomcat将在会话对象中查看会话对象:

HttpSession session = request.getSession();
session.getAttribute("isLogin");

二、多系统的复杂性

   随着时代的进步,单系统时代已经成为了历史,现如今已经发展成为了复杂的多系统组成的应用群,面对如此多的子系统,当用户登录和退出的时候,难道需要一个一个的登录和退出吗,这种方式肯定是不可取的。如下图所示:在这里插入图片描述   web系统已经由单系统发展成为复杂的多系统应用群,这个复杂性应该由系统内部承担,而非用户进行承担复杂性。无论系统拥有多么多的子系统多么复杂,对用户来说都是一个整体,也就是说,用户访问web系统应用群应该像访问单系统一样,进行登录和退出一次就行。在这里插入图片描述   单系统解决方案核心是cookie,cookie携带的会话id在浏览器和服务器之间维护会话状态,但是cookie是有限制的,这个限制就是域(通常对应网站的域名)的问题,浏览器发送http请求时会自动携带与该域匹配的cookie,并不是所有的cookie在这里插入图片描述   也许此时你们会说,可以使用一个共同的顶级域,也就是全部将域名设置成“*.baidu.com”,这种方法理论上可以的,甚至在早期有一些应用群都是这样解决的,但是实际中并不好,首先域名要进行统一,然后应用群要使用的技术都一致,要相同,不然cookie的key不相同,无法维持会话,并且无法实现宽平台语言的开发;第三:cookie本身是不安全的。
   因此,需要单点登录来进行解决。

三、单点登录

   单点登录的全称为Single Sign On(简称SSO),单点登录就是在多系统集群的情况下只需要进行登录其中一个系统,那么其他系统就可以得到授权无需再次登录,包括单点登录和单点注销两个部分。
   相比较与单系统来说,SSO需要一个独立的认证中心,只有认证中心能够接受用户名和密码等安全信息,其他系统将不再提供注册登录入口,只接受认证中心的间接授权。间接授权通过令牌实现,认证中心判断是合法的用户会创建令牌,在接下来跳转的过程中,认证中心会将令牌作为参数发送给各个系统,子系统拿到令牌,即得到授权,可以借此创建局部会话,局部会话登录方式和单系统的登录方式相同。这也就是单点登录系统的原理,如图所示:在这里插入图片描述   下面对上图进行分析解释:

  1. 用户访问系统1的受保护资源,系统1发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数
  2. sso认证中心发现用户未登录,将用户引导至登录页面
  3. 用户输入用户名密码提交登录申请
  4. sso认证中心校验用户信息,创建用户与sso认证中心之间的会话,称为全局会话,同时创建授权令牌
  5. sso认证中心带着令牌跳转会最初的请求地址(系统1)
  6. 系统1拿到令牌,去sso认证中心校验令牌是否有效
  7. sso认证中心校验令牌,返回有效,注册系统1
  8. 系统1使用该令牌创建与用户的会话,称为局部会话,返回受保护资源
  9. 用户访问系统2的受保护资源
  10. 系统2发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数
  11. sso认证中心发现用户已登录,跳转回系统2的地址,并附上令牌
  12. 系统2拿到令牌,去sso认证中心校验令牌是否有效
  13. sso认证中心校验令牌,返回有效,注册系统2
  14. 系统2使用该令牌创建与用户的局部会话,返回受保护资源

   当用户登录成功时,会与SSO认证中心及各个子系统建立会话,用户与SSO认证中心建立的会话俗称为全局会话,用户与各个子系统创建的会话称为局部会话,局部会话创建后,用户可以访问子系统的受保护的资源,通过sso认证中心,全局会话和局部会话有如下关系:

  1. 局部会话存在,那么全局会话一定存在
  2. 全局会话存在,局部会话不一定存在
  3. 全局会话销毁,局部会话一定销毁

2、 注销

   单点登录自然需要单点注销。一个系统进行注销,那么所有系统都将进行注销。下图将进行说明:在这里插入图片描述   SSO认证中心一直监听全局会话状态,一旦全局会话销毁,那么监听器将通知所有注册系统执行注销操作。
   下面将上图进行解释说明:

  1. 用户向系统1发起注销请求
  2. 系统1根据用户与系统1建立的会话id拿到令牌,向sso认证中心发起注销请求
  3. sso认证中心校验令牌有效,销毁全局会话,同时取出所有用此令牌注册的系统地址
  4. sso认证中心向所有注册系统发起注销请求
  5. 各注册系统接收sso认证中心的注销请求,销毁局部会话
  6. sso认证中心引导用户至登录页面

四、部署图

   单点登录涉及sso认证中心与众子系统,子系统与sso认证中心需要通信以交换令牌、校验令牌及发起注销请求,因而子系统必须集成sso的客户端,sso认证中心则是sso服务端,整个单点登录过程实质是sso客户端与服务端通信的过程,用下图描述在这里插入图片描述

五、实现

1、总体描述

   我先总体介绍下我所实现的内容,这个应用群中包含有四个系统,分别为登录系统,主页系统,VIP系统和购物车系统。大致思路为:用户访问任意一个系统受保护资源时,会进行判断是否有访问令牌(是否登录),如果登录了则允许访问,如果未登录则直接拦截进行跳转至登录页面,进行登录,然后输入用户名和密码进行验证,如果正确,则生成令牌,分发给各个子系统,然后会进行跳转到刚刚用户所在的系统,然后验证其令牌,如果令牌正确,则可以进行访问受保护的系统资源。代码大致如下,创建的工程为gradle工程,使用的是template,大家也可以改成maven工程和前后端分离的项目。在这里插入图片描述
   gradle的配置代码如下所示:
在这里插入图片描述

buildscript {
    
    
    repositories {
    
    
        mavenLocal()
        mavenCentral()
    }
    ext{
    
    
        springBootVersion = '2.1.3.RELEASE'
    }

    dependencies {
    
    
        classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}"
    }
    
}

subprojects {
    
    
    group 'com.sso'
    version '1.0-SNAPSHOT'
    apply plugin: 'java'
    apply plugin: 'io.spring.dependency-management'
    apply plugin: 'org.springframework.boot'

    repositories {
    
    
        mavenLocal()
        mavenCentral()
    }

    dependencies {
    
    
        compile 'org.springframework.boot:spring-boot-starter-web'
        annotationProcessor 'org.projectlombok:lombok:1.18.2'
        compileOnly 'org.projectlombok:lombok:1.18.2'
        compile 'org.springframework.boot:spring-boot-starter-thymeleaf'
    }
}


2、登录系统(sso-login)

在这里插入图片描述LoginController.java代码如下图所示:
   注意:此代码未进入数据库,模拟数据和进行验证,此子系统端口号为9000,记得在yml中进行配置端口号。

package com.sso.login.controller;

import com.sso.login.utils.LoginCacheUtil;
import com.sso.pojo.User;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;

/**
 * @Author: 闫高岭同志
 * @Date: 2020/9/13  21:45
 * @Version 1.0
 */
@Controller
@RequestMapping("/login")
public class LoginController {
    
    
    //模拟用户数据
    private static Set<User> dbUsers;
    static {
    
    
        dbUsers = new HashSet<>();
        dbUsers.add(new User(0,"zhangsan","zhangsan"));
        dbUsers.add(new User(1,"lisi","lisi"));
        dbUsers.add(new User(2,"wangwu","wangwu"));
    }

    @PostMapping
    public String doLogin(User user , HttpSession session , HttpServletResponse response){
    
    
        System.out.println("4444444:"+user);
        //记录从哪个页面跳转的网址
        String target = (String) session.getAttribute("target");
        //模拟从数据库中通过登录的用户名和密码去查找数据库中的用户
        Optional<User> first = dbUsers.stream().filter(dbUser -> dbUser.getUsername().equals(user.getUsername()) &&
                dbUser.getPassword().equals(user.getPassword())).findFirst();
        //判断用户是否登录
        if (first.isPresent()){
    
    
            //保存用户登录信息
            //随机生成token,也就是令牌
            String token = UUID.randomUUID().toString();
            Cookie cookie = new Cookie("TOKEN", token);
            //解决跨域问题,注意127.0.0.1地址映射问题
            cookie.setDomain("codeshop.com");
            response.addCookie(cookie);
            //将信息存储在loginUser中
            LoginCacheUtil.loginUser.put(token,first.get());

        }else {
    
    
            //登录失败
            session.setAttribute("msg","用户名或密码错误");
            return "login";
        }

        //重定向到target地址
        return "redirect:"+target;
    }

    @GetMapping("info")
    @ResponseBody
    public ResponseEntity<User> getUserInfo(String token){
    
    
        if (!StringUtils.isEmpty(token)){
    
    
            User user = LoginCacheUtil.loginUser.get(token);
            return ResponseEntity.ok(user);
        }else {
    
    
            return new ResponseEntity<>(null, HttpStatus.BAD_REQUEST);
        }
    }
}

ViewController.java代码入下图所示:

package com.sso.login.controller;

import com.sso.login.utils.LoginCacheUtil;
import com.sso.pojo.User;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpSession;

/**
 * @Author: 闫高岭同志
 * @Date: 2020/9/13  21:48
 * @Version 1.0
 */
//页面跳转逻辑
@Controller
@RequestMapping("/view")
public class ViewController {
    
    
    /**
     * 跳转到登录页面
     * @return
     */
    @GetMapping("/login")
    //target可能为空,用@RequestParam注解去设置,cookie也是这种情况
    private String toLogin(@RequestParam(required = false,defaultValue = "") String target, HttpSession session, @CookieValue(required = false,value = "TOKEN") Cookie cookie){
    
    
        //如果target为空,则设置成主页网址,最后往主页进行跳转
        if (StringUtils.isEmpty(target)){
    
    
            target = "http://www.codeshop.com:9010";
        }
        if (cookie != null){
    
    
            //如果是已经登录的用户再次访问登录系统时,就要重定向
            String value = cookie.getValue();
            User user = LoginCacheUtil.loginUser.get(value);
            if (user != null){
    
    
                return "redirect:"+target;
            }
        }
        //重定向地址
        session.setAttribute("target",target);
        return "login";
    }
}

工具类如图所示:
在这里插入图片描述登录子系统启动类:
在这里插入图片描述user.java实体类如图所示:
在这里插入图片描述登录页面HTML如图所示:
在这里插入图片描述

2、主页面系统(sso-main)

主页面系统总架构:
   端口号是9010,记得到配置文件中进行更改哦!
在这里插入图片描述ViewController.java代码如图所示:

package com.sso.main.controller;

import org.springframework.beans.factory.annotation.Autowired;
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.HttpSession;
import java.util.Map;

/**
 * @Author: 闫高岭同志
 * @Date: 2020/10/12  11:13
 * @Version 1.0
 */
@Controller
@RequestMapping("/view")
public class ViewController {
    
    

    @Autowired
    private RestTemplate restTemplate;

    private final String LOGIN_INFO_ADDRESS = "http://login.codeshop.com:9000/login/info?token=";

    @GetMapping("/index")
    public String toIndex(@CookieValue(required = false ,value = "TOKEN")Cookie cookie, HttpSession session){
    
    
        if (cookie != null){
    
    
            String token = cookie.getValue();
            //判断是否登录
            if (!StringUtils.isEmpty(token)){
    
    
                //取出登录用户且且存入进session
                Map result = restTemplate.getForObject(LOGIN_INFO_ADDRESS + token, Map.class);
                session.setAttribute("loginUser",result);
            }
        }
        return "index";
    }
}

启动类代码如图所示:
   注意这里采用的是Spring中的RestTemplate模板类,大家可以自行百度。

在这里插入图片描述主页面显示如下图所示:
在这里插入图片描述

3、VIP系统

系统总体架构如图所示:
   子系统端口号为9011,记得在配置文件更改哦。
在这里插入图片描述viewController.java代码如图所示:

package com.sso.vip.controller;

import org.springframework.beans.factory.annotation.Autowired;
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.HttpSession;
import java.util.Map;

/**
 * @Author: 闫高岭同志
 * @Date: 2020/10/12  11:22
 * @Version 1.0
 */
@Controller
@RequestMapping("/view")
public class ViewController {
    
    

        @Autowired
        protected RestTemplate restTemplate;

        private final String USER_INFO_ADDRESS = "http://login.codeshop.com:9000/login/info?token=";

        @GetMapping("/index")
        public String toIndex(@CookieValue(required = false,value = "TOKEN") Cookie cookie,
                              HttpSession session) {
    
    
            if (cookie != null){
    
    
                String token = cookie.getValue();
                if (!StringUtils.isEmpty(token)){
    
    
                    Map result = restTemplate.getForObject(USER_INFO_ADDRESS + token, Map.class);
                    session.setAttribute("loginUser",result);
                }
            }
            return "index";
        }
}

启动类(vipapp.java)如图所示:

package com.sso.vip;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

/**
 * @Author: 闫高岭同志
 * @Date: 2020/10/12  11:20
 * @Version 1.0
 */
@SpringBootApplication
public class VipApp {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(VipApp.class,args);
    }
    @Bean
    public RestTemplate restTemplate(){
    
    
        return new RestTemplate();
    }
}

index.html页面如图所示:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <title>Vip</title>
</head>
<body>
    <h1>欢迎来到VIP系统</h1>
    <span>
        <a th:if="${session.loginUser == null}" href="http://login.codeshop.com:9000/view/login?target=http://vip.codeshop.com:9011/view/index">登录</a>
        <a th:if="${session.loginUser != null}" href="#">退出</a>
    </span>
    <p th:unless="${session.loginUser == null}">
        <span style="color : red;" th:text="${session.loginUser.username}">已登录</span>
    </p>
</body>
</html>

4、购物车系统(sso-cart)

   总体概括如下图所示:
在这里插入图片描述viewController.java如图所示:

package com.sso.cart.controller;

import org.springframework.beans.factory.annotation.Autowired;
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.HttpSession;
import java.util.Map;

/**
 * @Author: 闫高岭同志
 * @Date: 2020/10/12  11:32
 * @Version 1.0
 */
@Controller
@RequestMapping("view")
public class ViewController {
    
    
    @Autowired
    protected RestTemplate restTemplate;

    private final String USER_INFO_ADDRESS = "http://login.codeshop.com:9000/login/info?token=";

    @GetMapping("index")
    public String toIndex(@CookieValue(required = false,value = "TOKEN") Cookie cookie,
                          HttpSession session){
    
    
        if (cookie != null){
    
    
            String token = cookie.getValue();
            if (!StringUtils.isEmpty(token)){
    
    
                Map result = restTemplate.getForObject(USER_INFO_ADDRESS+token, Map.class);
                session.setAttribute("loginUser",result);
            }
        }
        return "index";
    }
}

CartApp.java启动类如下图所示:

package com.sso.cart;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

/**
 * @Author: 闫高岭同志
 * @Date: 2020/10/12  11:24
 * @Version 1.0
 */
@SpringBootApplication
public class CartApp {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(CartApp.class,args);
    }
    @Bean
    public RestTemplate restTemplate(){
    
    
        return new RestTemplate();
    }
}

cart页面(index.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>欢迎来到Cart页面</h1>
    <span>
        <a th:if="${session.loginUser == null}" href="http://login.codeshop.com:9000/view/login?target=http://cart.codeshop.com:9012/view/index">登录</a>
        <a th:if="${session.loginUser != null}" href="#">退出</a>
    </span>
    <p th:unless="${session.loginUser == null}">
        <span style="color : red;" th:text="${session.loginUser.username}"></span>已登录
    </p>
</body>
</html>

好的,单点登录到此结束,单点退出还没实现,后期有时间的话会进行实现单点退出,思路也就是销毁cookie就行,大家可以先进行试试。
与人方便,与己方便
加油,奥利给

猜你喜欢

转载自blog.csdn.net/weixin_45150104/article/details/109053824