博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

Java 中实现 sso单点登录

Posted on 2022-04-01 17:27  zachry-r  阅读(2440)  评论(0编辑  收藏  举报

sso单点登录

gitee 源码地址: https://gitee.com/zarchary/sso-single-sign-on
邮箱: 361400631@qq.com

一、sso?

1.1 什么是sso

​ 单点登录(SingleSignOn,SSO),就是通过用户的一次性鉴别登录。当用户在身份认证服务器上登录一次以后,即可获得访问单点登录系统中其他关联系统和应用软件的权限,同时这种实现是不需要管理员对用户的登录状态或其他信息进行修改的,这意味着在多个应用系统中,用户只需一次登录就可以访问所有相互信任的应用系统。

1.2 为什么用sso

​ 如果只是对于父子域名之间,完全可以通过 session共享,来完成登录一次即可在系统任意处都有权限;

​ 如果是跨系统,跨域名:例如两个完全不同的域名 www.aa.com; www.bb.com 没有丝毫关联的系统怎么完成在一处登录之后,另一处是直接登录的呢?

​ 这就用到了 sso单点登录。单点登录原理: 用户通过客户端访问服务器,如果是第一次访问,会跳转公共的 认证服务器 并携带客户端的url地址;登录成功后,认证服务器会重定向到客户端并携带认证的令牌(token),同时会给所在浏览器设置 Cookie。

当第二个客户端再次访问,因为浏览器保存了 Cookie,就会自动完成登录

1.3 环境搭建

  • 所需服务器:
    • 域名:client1.com:8001;client1.com:8002; sso.com:8000
  • 所需环境:
    • 使用 springBoot完成服务快速搭建。 这里我是用的版本是 2.6.6

二、正式开始

1. 修改本机的dns解析的域名

  • 修改hosts文件

2. 创建客户端/ 认证服务器

2.1 创建空包项目

2.2 创建客户端 / 认证服务器

使用Spring Initializr 初始化springBoot项目。

添加基本依赖

完成创建,两个 客户端,一个认证服务器,客户端配置是一样的。可以复制

2.4 编排配置 properties

  • Client1 客户端配置
    • Client1 :配置文件设置 server.port=8001;
    • Client1: 认证服务器地址配置:sso.auth.path=http://sso.com:8000/auth/login.html
    • Client1:客户端1地址配置:sso.client1.path=http://client1.com:8001/emps
    • Client1:认证服务用户信息配置:sso.auth.info.auth=http://sso.com:8000/auth/userInfo
  • Client2客户端配置
    • Client2 :配置文件设置 server.port=8002;
    • Client2: 域名配置:sso.auth.path=http://sso.com:8000/auth/login.html
    • Client2:客户端2地址配置:sso.client2.path=http://client1.com:8001/emps
    • Client2:认证服务用户信息配置:sso.auth.info.auth=http://sso.com:8000/auth/userInfo
  • sso 认证服务器配置
    • sso服务:server.port=8000
    • sso.auth.path=http://sso.com
    • redis 地址 :spring.redis.host=192.168.64.3

3. 编写具体业务

​ 简介:

  • 客户端,认证中心的域名地址都配置在properties 文件中,通过 @Value 获取具体值
  • 客户端流程:
    • 先获取token信息,如果有直接跳转受保护的资源信息 ,并将查出来的用户信息渲染到页面
    • 如果没有token信息,则去认证服务器登陆,如果浏览器保存了登陆的cookie信息,也是不需要登陆的。如果没有cookie信息,则需要登陆。一旦登陆成功,只要浏览器不去清除 cookie信息,则与认证服务器有关的登陆,都是可以通过已有的 Cookie做免登陆的。这就是 sso的一处登陆,处处免登陆
  • 认证中心服务器:
    • 客户端首先访问的就是 认证中心的 login页面, 这里如果是浏览器已经存储了登陆的 Cookie信息,则直接返回客户端的受保护资源的地址;否则重定向到 doLogin的登陆页面
    • doLogin 登陆的处理:
      • 首先就是前端页面的 携带的 客户端的url,这是上一步认证中心将重定向的客户端url ,set到 request请求域中的。这样一来,登陆提交 form表单就会把 url携带。这样认证中心在通过认证之后才知道重定向到哪个客户端
      • 全局唯一的 用户id,这个可以通过 UUID工具生成。这样每个用户都是唯一的。通过这个唯一的 id,可以将用户信息存入到 redis缓存中。
    • userInfo 用户信息获取:
      • 这是认证服务器提供给 客户端的开放接口: 用户在认证成功之后会拿到属于自己的 token令牌,这样客户端就可以根据 token去远程查询 认证中心。通过 http 的get请求,携带 token获取用户的完整信息

流程图:

3.1 client 客户端编写

  • 编写controller

    • package com.sso.client1.controller;
      
      import org.springframework.beans.factory.annotation.Value;
      import org.springframework.http.HttpStatus;
      import org.springframework.http.ResponseEntity;
      import org.springframework.stereotype.Controller;
      import org.springframework.ui.Model;
      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.RequestParam;
      import org.springframework.web.client.RestTemplate;
      
      import javax.servlet.http.HttpSession;
      import java.util.ArrayList;
      
      /**
       * @author:zzz
       * @data: 2022/4/1
       * @product_name: SsoServer
       */
      @Controller
      public class ResourceController {
          /**
           * 远程调用次数
           */
          private static Integer FLAG = 3;
      
          @Value("${sso.auth.path}")
          String authPath;
      
          @Value("${sso.client1.path}")
          String client1;
      
          @Value("${sso.auth.info.auth}")
          String userInfoPath;
      
      
          /**
           * <p>受保护资源 ,需要登陆认证</p>
           *
           * @param token 认证服务器返回的token令牌,用于从数据库查出用户信息(required = false, 不是必须的)
           * @param model Model, springMVc 设置请求域的值, 页面通过 thymeleaf获取值
           * @param session 登陆验证,通过验证的才能跳转 emps页面展示数据,否则 重定向授权服务器的授权页面 sso.com
           * @return 成功 / 失败 跳转展示 / 授权页面
           */
          @GetMapping("/emps")
          public String emps(@RequestParam(value = "token", required = false) String token,
                             Model model,
                             HttpSession session){
              // 页面可展示资源, 这里使用 ArrayList 生成假数据
              ArrayList<String> emps = new ArrayList<>();
              emps.add("张三");
              emps.add("李四");
      
              // Model, springMVc 设置请求域的值, 页面通过 thymeleaf获取值
              model.addAttribute("emps", emps);
      
              if (StringUtils.hasText(token)){
                  // token 存在则从远程的认证中心获取用户信息
                  String userInfo = getUserInfo(token);
                  // 将用户信息渲染到页面
                  model.addAttribute("userLogin", userInfo);
                  // 从session 获取已经登陆的信息, 有则跳转页面展示
                  return "empList";
              }
      
              // 登陆验证,通过验证的才能跳转 emps页面展示数据,否则 重定向授权服务器的授权页面 sso.com
              Object loginUser = session.getAttribute("loginUser");
              if (loginUser != null) {
                  if (StringUtils.hasText(token)) {
                      // token 存在则从远程的认证中心获取用户信息
                      String userInfo = getUserInfo(token);
                      // 将用户信息渲染到页面
                      model.addAttribute("userLogin", userInfo);
                      // 从session 获取已经登陆的信息, 有则跳转页面展示
                      return "empList";
                  } else {
                      return "redirect:" + authPath + "?redirect_url=" + client1;
                  }
              } else {
                  // 当前没用需用登陆, 跳转统一的 认证服务器, 注意:需要带上本机客户端的地址
                  // 相当于  http://sso.com:8000/auth/login.html?redirect_url=http://client1.com:8001/emps.html
                  return "redirect:" + authPath + "?redirect_url=" + client1;
              }
      
          }
      
      
          /**
           *  <p>远程调用尝试三次, 失败后返回null</p>
           * @param token
           * @return
           */
          private String getUserInfo(String token) {
              if (FLAG == 0) {
                  return null;
              }
              RestTemplate template = new RestTemplate();
              String url = userInfoPath +"?token="+token;
              ResponseEntity<String> response = template.getForEntity(url, String.class);
              if (response.getStatusCode() == HttpStatus.OK) {
                  // 成功获取用户信息,
                  return response.getBody();
              } else {
                  // 获取失败,重新获取
                  FLAG--;
                  getUserInfo(token);
              }
              return null;
          }
      }
      
3.1.1 客户端的html文件

这里使用 thmeleaf 进行前端页面渲染

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>欢迎 [[${userLogin}]]</h1>
    <ul>
        <li th:each="emp : ${emps}">[[${emp}]]</li>
    </ul>
</body>
</html>

3.2 认证服务器端

添加 redis依赖

<!-- 引入redis缓存 -->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

controller 编写

package com.sso.ssoserverauth.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

/**
 * @author:zzz
 * @data: 2022/4/1
 * @product_name: SsoServer
 */
@Controller
@RequestMapping("/auth")
public class SSOAuthController {

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    /**
     * 所有登陆认证的首页,在此设置重定向的客户端地址
     * <li>从浏览器端获取 Cookie, 如果Cookie包含token,怎不需要登陆</li>
     *
     * @param url 客户端url
     * @param model 模型对象
     * @param cookie 客户端 cookie
     * @return 设置客户端的url, 返回登陆页面
     */
    @GetMapping("/login.html")
    public String login(@RequestParam("redirect_url") String url,
                        Model model,
                        @CookieValue(value = "sso_token",required = false) String cookie){
        if (StringUtils.hasText(cookie)){
            return "redirect:"+url + "?token="+cookie;
        }

        // 将客户端重定向的 url, 渲染到 login.html页面
        model.addAttribute("url",url);
        return "login";
    }

    /**
     * <p>统一认证服务器</p>
     * <li>验证登陆成功设置用户的唯一 id</li>
     * <li>验证登陆成功设置浏览器 Cookie</li>
     *
     * @param uname 登录名
     * @param passwd 密码
     * @param url 客户端访问url
     * @param response 相应对象
     * @return 成功 / 失败
     */
    @PostMapping("/doLogin")
    public String doLogin(@RequestParam("uname") String uname,
                          @RequestParam("passwd") String  passwd,
                          @RequestParam("url") String url,
                          HttpServletResponse response){



        // 这里验证用户名密码验证做最简单的 非空判断; 不为空,即登陆成功
        if (StringUtils.hasText(uname) && StringUtils.hasText(passwd)){
            // 登陆成功之后首先给当前登陆用户 生成唯一的id
            String ssoToken = UUID.randomUUID().toString().replace("-", "");
            // 将用户信息,根据唯一id放入 redis (这里只简单放入用户名)
            stringRedisTemplate.opsForValue().set(ssoToken, uname);
            // 给浏览器设置 Cookie, 将 token 设置到 cookie中
            Cookie cookie = new Cookie("sso_token", ssoToken);
            response.addCookie(cookie);

            return "redirect:"+ url +"?token=" + ssoToken;
        }

        // 失败 返回登陆页,带上客户端地址
        return "redirect:login?redirect_url="+url;
    }

    /**
     * 远程调用,获取用户信息
     *
     * @param token 用户唯一标识
     * @return 用户信息
     */
    @ResponseBody
    @GetMapping("/userInfo")
    public String userInfo(@RequestParam("token") String token){
        String userInfo = stringRedisTemplate.opsForValue().get(token);
        // 根据客户端提供的 token 获取用户信息
        if (StringUtils.hasText(userInfo)){
            return userInfo;
        }

        return "";
    }
}
3.2.1 认证服务器页面

登陆页面 携带 hidden 隐藏的 客户端url。这里参照 controller 的login方法。携带重定向的 url发给认证服务器。这样 服务器处理完登陆结果,也知道把信息返回给哪个服务器的地址

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
  <form method="post" th:action="@{/auth/doLogin}">
    用户名: <input type="text" name="uname"><br/>
    密 码: <input type="password" name="passwd"><br/>
      <input type="hidden" name="url" th:value="${url}">
      <button type="submit" value="登陆">登陆</button>
  </form>
</body>
</html>