cookie实现单点登录

前言

单点登录方案设计一篇中,我们谈到了目前市面上常用的一些单点登录方案的实现,关于单点登录,只需要把握一个核心的要点即可,那就是:一处登录,处处登录,登录之后,即同域下其他各个系统都能统一拿到用户的基本信息

关于cookie,想必大家也很熟悉了,cookie中可以存储会话信息,将用户的基本信息存储进去之后,就可以在前后端交互中进行传输了

本篇将分享基于cookie如何实现单点登录,本篇以实际案例为主进行演示

业务背景

以一个大家熟悉的购物业务,实际项目中,一个商城系统可能包含诸多模块的业务,比如用户中心,专门负责用户的认证功能,购物车、会员、物流等多个板块,用户只需要一次登录之后,就可以在各个业务模块的界面中来回切换

在这里插入图片描述

环境准备

JDK8 , maven , idea

实现目标

  • 用户通过用户模块的主页面登录之后,可以访问任意的其他模块,比如购物车模块的页面
  • 未登录访问其他任何模块,直接跳转到登录页面
  • 用户模块退出之后,再访问其他任何模块的页面,将无法访问

工程搭建

在这里插入图片描述

整个工程结构如上图所示, 各个模块的说明如下:

  • sso-main 业务主模块,也是登录的入口
  • sso-login 负责登录业务,实际可理解为用户中心
  • sso-cart 购物车模块
  • sso-vip 会员模块

后续可以在此基础上继续增加,比如积分模块,物流模块等

1、顶级pom依赖

	<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <version>2.2.1.RELEASE</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

sso-login模块

该模块主要负责用户的登录,退出,并将用户会话信息存储至token,同时为其他各个模块提供用户信息查询功能

1、pom依赖

 	<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.13</version>
        </dependency>

    </dependencies>

2、yml配置

server:
  port: 9000

3、登录页面

提供一个基于thymeleaf的html登录模板页

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <title>login</title>
</head>
<body>
<h3>登陆页面</h3>
<form action="/login" method="post">
    用户名:<input type="text" name="username"/>
    密码:<input type="password" name="password"/>
    <input type="submit" value="登录"/>
    <p style="color: red" th:text="${session.msg}"></p>
</form>
</body>
</html>

4、提供一个controller

ViewController 用户进行跳转至登录页,作为引导控制器

/**
 * 页面跳转逻辑
 */
@Controller
@RequestMapping("/view")
public class ViewController {

    /**
     * 跳转到登陆页面,设置重定向的地址,可以携带cookie-TOKEN,如果有cookie,不用跳转到登陆页面,直接重定向
     * @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)){
            target = "http://127.0.0.1:9010";
        }
        //若已经登陆的用户登陆系统时,直接重定向到target
        if (cookie != null){
            String value = cookie.getValue();
            User user = LoginCacheUtil.loginMap.get(value);
            if (user != null){
                return "redirect:" + target;
            }
        }
        //重定向地址,将地址保存起来
        session.setAttribute("target",target);
        return "login";
    }
}

LoginController 实际处理登录逻辑的控制器,提供登录,登出,以及查询用户信息的接口,这里为了模拟数据库的用户,就直接在程序中模拟初始化一些用户数据

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

    private static Set<User> dbUsers;

    /**
     * 模拟数据库的用户列表
     */
    static {
        dbUsers = new HashSet<>();
        dbUsers.add(new User(0,"zhangsan","123456"));
        dbUsers.add(new User(1,"lisi","123456"));
        dbUsers.add(new User(2,"wangwu","123456"));
    }

    @PostMapping
    public String doLogin(User user, HttpSession session, HttpServletResponse response){
        String target = (String) session.getAttribute("target");
        User res = null;
        for (User dbUser : dbUsers){
            if(dbUser.getUsername().equals(user.getUsername()) && dbUser.getPassword().equals(user.getPassword())){
                res = dbUser;
            }
        }
        //用户登陆成功,保存(TOKEN,用户)
        if (res != null){
            //保存用户登陆信息
            String token = UUID.randomUUID().toString();
            Cookie cookie = new Cookie("TOKEN",token);
            cookie.setDomain("127.0.0.1");
            response.addCookie(cookie);
            LoginCacheUtil.loginMap.put(token,user);
        } else {
            session.setAttribute("msg","用户名或密码错误");
            return "login";
        }
        //重定向到target地址
        return "redirect:" + target;
    }

    /**
     * 给其他子系统开发一个接口,根据token获取登陆的用户信息
     * @param token
     * @return
     */
    @GetMapping("/info")
    public ResponseEntity<User> getUserInfo(String token){
        if (!StringUtils.isEmpty(token)){
            User user = LoginCacheUtil.loginMap.get(token);
            return ResponseEntity.ok(user);
        }else {
            return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
        }
    }

    @GetMapping("/logout")
    public String logout(@CookieValue(value = "TOKEN")Cookie cookie, @RequestParam("target") String target,
                         HttpSession session,HttpServletResponse response){
        //删除用户的登陆信息
        LoginCacheUtil.loginMap.remove(cookie.getValue());
        //删除session.loginUser
        session.removeAttribute("loginUser");
        //设置cookie过期
        Cookie newCookie = new Cookie("TOKEN",null);
        newCookie.setMaxAge(0);
        newCookie.setPath("/");
        response.addCookie(newCookie);
        return "redirect:"+target;
    }

}

sso-main 模块

1、pom依赖

同 sso-login

2、yml配置

server:
  port: 9010

3、配置简单的html

由于需要进行页面展示,并模拟登录过程,这里使用了thymeleaf模板,使用比较简单,main模块的页面主要负责登录之后跳转后的主页面,简单起见,只需要对登录之后的用户信息做一下展示即可

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>
    <h1>欢迎来到main首页</h1>
    <span>
        <a th:if="${session.loginUser} == null" href="http://127.0.0.1:9000/view/login?target=http://127.0.0.1:9010/view/index">登陆</a>
        <a th:unless="${session.loginUser} == null" href="http://127.0.0.1:9000/login/logout?target=http://127.0.0.1:9010/view/index">退出</a>
    </span>
    <p th:unless="${session.loginUser} == null">
        <span style="color: deepskyblue" th:text="${session.loginUser.username}"></span>已登陆
    </p>
</body>
</html>

4、编写一个controller

当用户登录成功后,需要进行页面跳转,即跳转到main的页面,由于是不同的业务模块,实际项目中,可能是分布式部署的话,为了拿到用户的登录信息,这里简单采用restTemplate的方式

@Controller
@RequestMapping("/view")
public class ViewController {

    @Autowired
    private RestTemplate restTemplate;

    private final static String REMOTE_LOGIN_INFO_ADDRESS = "http://127.0.0.1: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();
            //根据login子系统暴露的info方法去根据token获取user
            if (!StringUtils.isEmpty(token)){
                Map result = restTemplate.getForObject(REMOTE_LOGIN_INFO_ADDRESS + token, Map.class);
                session.setAttribute("loginUser",result);
            }
        }
        return "index";
    }
}

sso - main 和sso-login模块写好之后,我们就可以简单做个测试了,分别启动这两个模块

1、访问登录入口页面,http://127.0.0.1:9010/view/index
在这里插入图片描述
2、点击登录

登录之后,由sso-main模块中的页面上的a链接,携带一个完整的href地址跳转到sso-login的登录主页面,我们的实现逻辑是,进入登录页面时,由于携带了完整的url信息,里面包含了从哪个页面(这里从main模块)过来的,那么在sso-login登录成功之后,还能跳转回去原来的页面
在这里插入图片描述
3、输入程序中初始化的用户名和密码

在这里插入图片描述

在这里插入图片描述
在sso-login的doLogin接口处理完成之后,将会把登录用户的信息写入session并带来页面上进行展示;

同时用户登录成功后,在接口中我们生成了一个token,用一个map进行存储,token作为key,user对象作为value,方便在后续其他模块访问的时候,直接从map中快速获取,关于这一点,在实际开发项目中,可以考虑使用threadLocal进行存储

如何验证我们的单点登录是好使的呢?还没有完,上面只是完成了用户的登录,接下来我们再将另外2个模块的业务逻辑编写完整

sso-vip 模块

该模块作为会员业务模块,另外的cart购物车模块也是如此

1、pom依赖

如上的sso-login

2、yml配置

server:
  port: 9011

3、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://127.0.0.1:9000/view/login?target=http://127.0.0.1:9011/view/index">登陆</a>
        <a th:unless="${session.loginUser} == null" href="http://127.0.0.1:9000/login/logout?target=http://127.0.0.1:9011/view/index">退出</a>
    </span>
    <p th:unless="${session.loginUser} == null">
        <span style="color: deepskyblue" th:text="${session.loginUser.username}"></span>已登陆
    </p>
</body>
</html>

该页面主要展示登录成功后的用户信息

4、提供一个controller,用于从sso-login中获取用户信息

@Controller
@RequestMapping("/view")
public class ViewController {

    @Autowired
    private RestTemplate restTemplate;

    private final static String REMOTE_LOGIN_INFO_ADDRESS = "http://127.0.0.1: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(REMOTE_LOGIN_INFO_ADDRESS + token, Map.class);
                session.setAttribute("loginUser",result);
            }
        }
        return "index";
    }
}

接下来启动该模块,下面开始测试,

1、在已经登录的情况下,输入:http://localhost:9011/view/index

已经登录的情况下,VIP模块的controller中通过rest接口可以拿到cookie中用户的信息,因此直接将用户信息取出进行页面展示
在这里插入图片描述

2、在未登录的情况下,输入:http://localhost:9011/view/index

先点击退出登录,然后直接展示登录页面的信息
在这里插入图片描述

最后我们将cart模块也仿照vip博客搭建好,启动项目,然后再次做测试,

3、访问car模块主页面:http://127.0.0.1:9012/view/index

由于第二步中进行了退出,这时候,访问cart页面时,也是需要登录的状态
在这里插入图片描述
4、cart中点击登录,并进行登录操作

登录成功后,我们再次刷新cart页面,这时候显示已登录

在这里插入图片描述

通过以上案例的展示,我们基于cookie的方式实现了模拟单点登录的效果,此种方案,在不少生产级的项目中仍有使用,关于cookie实现单点登录,做一下简单的说明

优点:

  • 实现难度低,无需太多的学习成本
  • 后续维护相对简单
  • 新的业务模块加入时,扩展实现成本较低

缺点:

  • cookie中如果存放了用户的敏感信息,一旦被窃取,这个负面影响大,生产环境,最好使用加密手段加密
  • 如果大量的用户信息放入cookie,对浏览器端压力较大,同时cookie存储的信息量大小有限
  • 不够轻量级,跨域情况下将会带来一定的麻烦(这种问题,需要考虑到自身系统和外部系统对接问题)

猜你喜欢

转载自blog.csdn.net/zhangcongyi420/article/details/121190102