You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ServiceStack:如何实现单用户单设备唯一会话机制?

Alright, let's walk through how to implement that one active session per user per device pattern you're after in ServiceStack—mirroring the custom solution you built using DeviceUUID. Here's a practical, step-by-step approach that integrates cleanly with ServiceStack's authentication system:

Core Concept Recap

First, to align with your requirements: every unique device (identified by a DeviceUUID) should only have one valid session for a given user. Any new login from the same device should invalidate the user's existing session on that device, preventing multiple active sessions per device.

Implementation Steps

1. Pass DeviceUUID with All Authentication Requests

First, ensure your authentication requests carry the unique DeviceUUID for the device. You can either extend ServiceStack's default Authenticate DTO or attach it via request metadata:

Option A: Custom Authenticate DTO

public class DeviceAwareAuthenticate : Authenticate
{
    // Unique identifier for the device (generated once per device)
    public string DeviceUUID { get; set; }
}

Option B: Attach via Request Meta

If you prefer using the default Authenticate DTO, add the DeviceUUID to the request's Meta dictionary before sending:

var authRequest = new Authenticate {
    UserName = "user@example.com",
    Password = "password123",
    Meta = new Dictionary<string, string> { { "DeviceUUID", "your-unique-device-id-here" } }
};

2. Extend Auth Session to Store DeviceUUID

Create a custom session class that extends ServiceStack's AuthUserSession to track the DeviceUUID associated with the session:

public class DeviceBoundUserSession : AuthUserSession
{
    // Store the device ID tied to this session
    public string DeviceUUID { get; set; }
}

3. Override Auth Provider to Invalidate Existing Sessions

Next, customize ServiceStack's authentication provider (e.g., CredentialsAuthProvider) to handle session invalidation when a new session is created for the same user + device.

public class DeviceAwareCredentialsAuthProvider : CredentialsAuthProvider
{
    public override async Task OnAuthenticatedAsync(IServiceBase authService, IAuthSession session, IAuthTokens tokens, Dictionary<string, string> authInfo)
    {
        var deviceSession = session as DeviceBoundUserSession;
        if (deviceSession == null)
            throw new InvalidOperationException("Session must be of type DeviceBoundUserSession");

        // Extract DeviceUUID from request (either from DTO or Meta)
        var deviceUUID = authService.Request.GetParam("DeviceUUID") 
                         ?? authInfo.GetValueOrDefault("DeviceUUID");

        if (string.IsNullOrEmpty(deviceUUID))
            throw new ArgumentNullException(nameof(deviceUUID), "DeviceUUID is required for authentication");

        // Assign DeviceUUID to the new session
        deviceSession.DeviceUUID = deviceUUID;

        // Get all existing sessions for the authenticated user
        var cacheClient = authService.TryResolve<ICacheClient>();
        var userSessionKeys = authService.SessionFeature.GetSessionKeysForUser(session.UserAuthId);
        var existingSessions = await cacheClient.GetAllAsync<DeviceBoundUserSession>(userSessionKeys);

        // Invalidate any existing sessions for the same user and device
        foreach (var existingSession in existingSessions.Where(s => 
                 s.DeviceUUID == deviceUUID && s.Id != session.Id))
        {
            await authService.RemoveSessionAsync(existingSession.Id);
        }

        // Save the new session and complete authentication
        await base.OnAuthenticatedAsync(authService, session, tokens, authInfo);
    }
}

4. Enforce Device-Session Validation on Every Request

Add a global request filter to ensure every authenticated request matches the session's DeviceUUID, preventing session reuse across devices:

public class DeviceSessionValidationFilter : IRequestFilter
{
    public void Execute(IRequest req, IResponse res, object requestDto)
    {
        var session = req.SessionAs<DeviceBoundUserSession>();
        if (!session.IsAuthenticated)
            return;

        // Get DeviceUUID from request (could be query param, header, or DTO)
        var requestDeviceUUID = req.GetParam("DeviceUUID") 
                               ?? req.Headers["X-Device-UUID"];

        // Reject request if device doesn't match the session's device
        if (session.DeviceUUID != requestDeviceUUID)
        {
            res.StatusCode = (int)HttpStatusCode.Unauthorized;
            res.ContentType = MimeTypes.Json;
            res.Write(new ErrorResponse { 
                ErrorCode = "InvalidDeviceSession", 
                Message = "Session is not valid for this device" 
            });
            res.EndRequest();
        }
    }
}

5. Register Components in AppHost

Wire up all your custom components in your ServiceStack AppHost configuration:

public class AppHost : AppHostBase
{
    public AppHost() : base("Your App Name", typeof(MyServices).Assembly) { }

    public override void Configure(Container container)
    {
        // Register custom session type
        container.RegisterAs<DeviceBoundUserSession, IAuthSession>();

        // Enable AuthFeature with custom provider
        Plugins.Add(new AuthFeature(() => new DeviceBoundUserSession(),
            new IAuthProvider[] { new DeviceAwareCredentialsAuthProvider() }));

        // Add device session validation filter
        GlobalRequestFilters.Add(new DeviceSessionValidationFilter());

        // Configure cache (use Redis for distributed environments)
        container.Register<ICacheClient>(new MemoryCacheClient());
        // For Redis: container.Register<IRedisClientsManager>(new PooledRedisClientManager("localhost:6379"));
        // container.Register<ICacheClient>(c => c.Resolve<IRedisClientsManager>().GetCacheClient());
    }
}
Key Considerations
  • DeviceUUID Uniqueness: Ensure DeviceUUID is truly unique per device. For web browsers, generate a persistent UUID stored in localStorage; for mobile apps, use platform-specific device identifiers (be mindful of privacy regulations like GDPR).
  • Distributed Environments: If your app runs across multiple servers, use a distributed cache like Redis for session storage—this ensures session invalidation works consistently across instances.
  • Session Expiry: Leverage ServiceStack's built-in session expiry settings (set via SessionFeature.SessionExpiry) to automatically clean up stale sessions.

内容的提问来源于stack exchange,提问作者Ted

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 07:55:01