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:
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.
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()); } }
- DeviceUUID Uniqueness: Ensure
DeviceUUIDis truly unique per device. For web browsers, generate a persistent UUID stored inlocalStorage; 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

