前言

网上搜单点登录的文章有很多,实现方案有cookie、session、jwt等,但大多讲的都是原理,核心问题都没提到,因此我根据这俩条件整理个可行方案:

  • 只考虑同域,不跨域,如a.my.com、b.my.com等,都在*.my.com主域内,因此使用cookie比较方便
  • 前后端分离,目前大多前端项目都基于react、vue,因此必须要考虑

有个问题一直没搞清,就是使用cookie方案时,前端如何判断是否登录。按自己的理解,我想了以下几个方案:

  • 方案一:HttpOnly设为false ,这样前端js就能读取cookie,判断是否存在登录信息,但是安全性会降低
  • 方案二:使用nginx判断,nginx代理判断header里是否有指定cookie,没有则跳转sso登录页
  • 方案三:前端不判断,等到网页中有接口请求时,让后端判断有无cookie
  • 方案四:设置两个cookie(推荐),一个存登录令牌(HttpOnly=true),一个存用户id(HttpOnly=false),前端读取cookie中有无用户id来判断是否登录

实现逻辑(方案四)

场景如下:

  • A网站:a.my.com
  • B网站:b.my.com
  • SSO网站: sso.my.com
  • cookie值:
    • tk=xxxxx sso服务端加密后的令牌
    • uid=123 用户id

A网站步骤:

  • 浏览器打开A首页 a.my.com/index.html
  • A网站js判断cookie不存在uid,跳转sso.my.com/login.html?return=a.my.com/index.html
  • sso登录页输入账号密码点击登录,post请求给sso.my.com/api/login
  • sso服务端验证账号密码,成功则设置cookie:tk=xxx(HttpOnly=true),uid=123(HttpOnly=false),域名都设为.my.com,返回200
  • sso登录页js判断返回200,则获取url中return值a.my.com/index.html,并跳转到A网站
  • A网站每次请求api时,A后台验证cookie中的tk和uid(解密tk的方法应与sso加密方法对应),失败或过期则返回401
  • A网站js判断返回401,则跳转sso登录页sso.my.com/login.html?return=当前url

B网站步骤:

  • 浏览器打开b.my.com/index.html
  • B网站js判断cookie中存在uid,则进入网页,实现单点登录
  • 其他同上

部分代码

sso前端

<body>
	<div><label>Username</label><inputtype="text"></div>
	<div><label>Password</label><input type="password"></div>
	<div><button onclick="loginRequest()">Login</button></div>
</body>

<script>
    function loginRequest() {
        fetch("/api/login", {
            method: "post",
            data: null,
        }).then((data) =>  {
            if (data.status == 200){
                const url = new URL(window.location.href);
                location = url.searchParams.get("return")
            }else{
                console.log(data)
            }
        })
    }
</script>

sso后端(golang)

package main

import (
	"net/http"
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()
	r.POST("/api/login", func(c *gin.Context) {
		c.SetCookie("tk", "xxxxx", 3600, "/", ".my.com", true, false)
		c.SetCookie("uid", "123", 3600, "/", ".my.com", false, false)
		c.JSON(200, "ok")
	})
	r.Run(":8000")
}

A网站前端

<body>
    <h2>A网站</h2>
    <button onclick="testRequest()">Test</button>
</body>
<script>
    function getCookie(key) {
    	return decodeURIComponent(document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*" + encodeURIComponent(sKey).replace(/[-.+*]/g, "\\$&") + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1")) || null;
    }
    
    const ssoUrl = "http://localhost:8000/login"
    if (!getCookie("uid")){
        window.location = ssoUrl + "?return=" + encodeURIComponent(location.href)
    }
    
    function testRequest() {
        fetch("/api/test").then((data) => {
            if (data.status == 401) {
                window.location = ssoUrl + "?return=" + encodeURIComponent(location.href)
            } else {
            	console.log(data)
            }
        })
    }
</script>

A网站后端(golang)

package main

import (
	"net/http"
	"github.com/gin-gonic/gin"
)

func checkCookies(c *gin.Context) bool{
	tk, _ := c.Cookie("tk")
	uid, _ := c.Cookie("uid")
	// TODO 验证tk、uid
	return true
}

func main() {
	r := gin.Default()
	r.GET("/api/test", func(c *gin.Context) {
		if checkCookies() == false {
			c.JSON(http.StatusUnauthorized, "invalid cookie")
		} else {
			c.JSON(http.StatusOK, "ok")
		}
	})
	r.Run(":8001")
}

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐