1.单点登录(SSO)原理
单点登录(Single Sign-On,简称SSO)是一种身份验证和授权机制,允许用户在访问多个相关独立的系统或应用程序时只需一次登录, 而不需要为每个系统都提供单独的身份验证凭证。SSO的目的是简化用户体验、提高安全性, 并减少用户因频繁登录而可能面临密码疲劳问题。
SSO的工作原理涉及以下关键概念:
- 身份提供者(Identity Provider, IdP): 负责验证用户的身份并生成令牌(Token)。IdP通常是一个中心化的认证系统, 负责向其他相关系统提供认证服务。
- 服务提供者(Service Provider, SP): 各个系统或应用程序, 它们依赖于IdP来验证用户身份。SP接收到IdP颁发的令牌后, 可以通过验证令牌的有效性来信任用户身份。
- 令牌(Token): 由IdP颁发, 包含有关用户身份的信息, 以及可能的授权信息。
- 单一登录会话(Single Sign-On Session): 用户只需一次登录到IdP,然后就可以访问所有与IdP集成的SP, 而无需再次提供用户名和密码。
原理图如下:
图片
2.JWT原理
JWT 是一种基于 JSON 格式的轻量级令牌,其主要原理是通过在服务端生成一个包含用户信息的 JSON 对象,然后使用密钥对该对象进行签名,生成一个令牌。这个令牌可以被发送到客户端,客户端可以在之后的请求中携带该令牌,服务端使用密钥验证令牌的签名,并解析其中的信息, 从而完成身份验证。
JWT 由三部分组成:Header(头部)、Payload(负载)和 Signature(签名)。
- Header(头部):包含了两部分信息,token 的类型(JWT)和使用的签名算法,通常是 Base64 编码的 JSON 字符串。
- Payload(负载):包含了一些声明(Claim),其中包括标准声明、私有声明等。这部分也是 Base64 编码的 JSON 字符串,用于携带一些关键的信息。
- Signature(签名):由前两部分使用指定的算法签名而成,用于验证消息的完整性。
JWT原理图如下:
图片
3.使用Golang模拟实现过程
为了模拟单点登录(SSO), 将创建两个简单的Golang服务: 一个用于认证用户(认证中心), 另一个用于资源提供。用户如果要获取资源,必须先登录认证中心获取令牌, 然后再通过令牌访问资源服务器, 下面是认证中心的服务端实现代码:
package main
import (
"fmt"
"net/http"
"time"
"github.com/dgrijalva/jwt-go"
)
var secretKey = []byte("btk.gqv7jtu7VZD1dar")
func main() {
http.HandleFunc("/login", handleLogin)
http.ListenAndServe(":8080", nil)
}
func handleLogin(w http.ResponseWriter, r *http.Request) {
// 在实际应用中,这里应该有用户认证的逻辑,为了简化,这里直接使用一个固定用户
userID := "9527"
tokenString, err := createToken(userID)
if err != nil {
http.Error(w, "创建令牌失败", http.StatusInternalServerError)
return
}
// 将 JWT 令牌附加到响应中
w.Header().Set("Authorization", "Bearer "+tokenString)
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "登录成功. Token: %s", tokenString)
}
func createToken(userID string) (string, error) {
// 创建负载
payload := jwt.MapClaims{
"user": userID,
"exptime": time.Now().Add(time.Minute * 15).Unix(), // 令牌过期时间为15分钟
}
// 创建 Token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, payload)
// 签名并获取完整的 Token 字符串
tokenString, err := token.SignedString(secretKey)
if err != nil {
return "", err
}
return tokenString, nil
}
在上面的代码中, 认证中心服务端在本地监听8080端口, 用来模拟处理用户的登录请求, 在收到用户请求之后, 服务端根据用户ID调用JWT的方法生成Token, 并将Token设置到HTTP头的Authorization字段中返回给客户端。
接下来实现资源提供的服务端,参考代码如下:
package main
import (
"fmt"
"net/http"
"github.com/dgrijalva/jwt-go"
)
var secretKey = []byte("btk.gqv7jtu7VZD1dar")
func main() {
http.HandleFunc("/resource", handleResource)
http.ListenAndServe(":8081", nil)
}
func handleResource(w http.ResponseWriter, r *http.Request) {
// 从请求中获取 Authorization 头
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "未找到Authorization字段", http.StatusUnauthorized)
return
}
// 解析JWT令牌
tokenString := authHeader[len("Bearer "):]
token, err := parseToken(tokenString)
if err != nil || !token.Valid {
http.Error(w, "令牌不合法", http.StatusUnauthorized)
return
}
// 获取负载信息
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
http.Error(w, "令牌不合法", http.StatusUnauthorized)
return
}
userID, ok := claims["user"].(string)
if !ok {
http.Error(w, "访问令牌中的用户不存在", http.StatusUnauthorized)
return
}
// 在实际应用中,这里可以根据 userID 获取用户信息或提供资源
// 这里只是一个简单的示例
response := fmt.Sprintf("用户ID%s获取资源成功!", userID)
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, response)
}
func parseToken(tokenString string) (*jwt.Token, error) {
// 解析 Token 字符串
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return secretKey, nil
})
if err != nil {
return nil, err
}
return token, nil
}
在上面的代码中, 资源服务端监听本地的8081端口, 当接收到用户请求之后, 首先从请求头中的Authorization字段获取Token令牌, 并对令牌进行解析, 如果正确解析出用户ID, 返回该用户的资源信息。
4.验证实现结果
将上面两段代码分别编译成两个独立Server端,并开启两个窗口分别运行。首先请求SSO服务端, 返回结果如下:
图片
从上图可知,SSO服务端成功返回了一个Token令牌, 下面先不用Token访问一下资源服务器试试:
图片
从上图可以看到,没有令牌无法正常请求到所需资源, 下面使用Apifox新建一个请求, 在里面加上Token, 如图:
图片
保存之后, 带着Token请求一下资源服务端, 如图:
图片
可以看到, 成功返回了资源, 整个流程测试成功。