为了限制一个账号在同一时间只能登录一个设备,常用的方法是使用 token(令牌) 机制结合服务器端的存储与验证。基本思路是,当用户登录时,生成一个 token,并将其与用户的登录状态绑定存储在服务器上。接下来,在用户每次请求时都会携带这个 token,服务器端则验证 token 是否有效、唯一,确保用户在同一时间内只能登录一个设备。
实现步骤
用户登录生成 token
当用户成功登录时,服务器生成一个唯一的 token,作为用户的会话标识。
这个 token 通常是通过 JWT(JSON Web Token)或其他随机生成机制生成的,通常包含用户 ID、时间戳等信息。
存储 token
服务器将生成的 token 与用户的账号绑定,通常会将其存储在 Redis 或数据库中。你可以将用户的 ID 作为键,将 token 作为值进行存储。
例如:user_id -> token
登录验证逻辑
如果同一个账号再次登录,那么生成一个新的 token,并更新服务器端的存储,替换旧的 token。
例如:user_id -> new_token,覆盖掉之前的 token。
因此,旧设备上的 token 将失效,即使旧设备发送请求,token 也无法通过验证。
每次请求时验证 token
在每次用户请求时,客户端都会携带当前的 token。
服务器验证请求中的 token 是否与存储的 token 一致。如果 token 不匹配,则认为用户已在另一设备上登录,将该请求视为无效,返回登录失效的响应。
用户退出登录
当用户主动退出登录时,将 token 从服务器端存储中删除,防止此 token 被再次使用。
具体实现细节
1. 登录时生成 token
可以使用 JWT 或随机生成的字符串作为 token:
import (
"crypto/rand"
"encoding/hex"
)
// 生成随机 Token
func generateToken() (string, error) {
bytes := make([]byte, 16)
_, err := rand.Read(bytes)
if err != nil {
return "", err
}
return hex.EncodeToString(bytes), nil
}
2. 在 Redis 中存储 token
使用 Redis 来存储 token 和用户 ID 的对应关系:
import (
"context"
"github.com/go-redis/redis/v8"
"time"
)
var ctx = context.Background()
// 假设你有一个 Redis 客户端
var rdb = redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// 保存 token 到 Redis
func saveTokenToRedis(userID string, token string) error {
err := rdb.Set(ctx, userID, token, time.Hour*24).Err() // 设置过期时间24小时
return err
}
3. 登录时的处理逻辑
func login(userID string, password string) (string, error) {
// 假设验证了用户的密码正确
token, err := generateToken()
if err != nil {
return "", err
}
// 将新的 token 存储到 Redis,替换旧的 token
err = saveTokenToRedis(userID, token)
if err != nil {
return "", err
}
return token, nil
}
4. 请求时验证 token
每次请求都需要带上 token,服务器端进行验证:
func validateToken(userID string, token string) bool {
// 从 Redis 中获取该用户的 token
storedToken, err := rdb.Get(ctx, userID).Result()
if err != nil || storedToken != token {
// token 不存在或不匹配,验证失败
return false
}
return true
}
5. 处理请求的伪代码
当用户发送请求时,服务器会从请求头中获取 token,验证其有效性:
func handleRequest(userID string, token string) {
if !validateToken(userID, token) {
// 返回登录失效错误
fmt.Println("登录失效,请重新登录")
return
}
// token 验证成功,继续处理请求
fmt.Println("请求处理成功")
}
6. 退出登录
用户退出时,可以将存储的 token 删除:
func logout(userID string) error {
err := rdb.Del(ctx, userID).Err() // 删除 token
return err
}
应对可能的问题
多个设备登录的处理:
如果你希望支持多设备登录,而不是只允许单设备登录,可以为每个设备生成不同的 token,并存储多个 token。你可以通过设备标识来区分 token。
存储结构可以变为:user_id -> [token1, token2, ...],每个设备一个 token。
过期时间的设置:
通常情况下,token 会有一个过期时间,过期后需要重新登录。你可以通过 Redis 的 TTL(过期时间)机制来实现 token 的自动失效。
异地登录提醒:
当新的设备登录时,旧设备的 token 会失效。你可以在旧设备请求时,返回提示信息,通知用户已经在另一设备登录。
安全性措施:
确保 token 随机性足够高,避免被预测。
使用 HTTPS 保护 token 在网络传输中的安全性,防止中间人攻击。
token 也可以采用 JWT(JSON Web Token)标准,使其更易于管理和验证。