Unity开发安卓游戏:基于.Net REST API与SQL的免密注册登录需求
Great question—this passwordless, auto-create/auto-login flow is exactly what modern mobile gamers expect, and it's totally achievable with your Unity + .NET/SQL stack. Let's walk through the implementation step by step, covering backend setup, client integration, and key security considerations.
The core idea is to use a stable device identifier as the initial auth trigger:
- On first launch, the game sends the device ID to your backend. If no user is linked to that ID, a new anonymous account is created automatically.
- To enable cross-device access, we add an optional account linking step (e.g., email/phone verification) so users can associate their anonymous account with a recoverable credential.
- Return a JWT token for subsequent API requests to authenticate the user without re-sending device IDs every time.
1. Backend (.NET REST API + SQL) Setup
Database Table Design
First, create a Users table in your SQL database to store account data:
| Column | Type | Description |
|---|---|---|
UserId | GUID | Primary key, unique account identifier |
DeviceId | VARCHAR(255) | Linked device identifier (supports multiple devices per user via updates) |
Email | VARCHAR(255) | Nullable, for cross-device account recovery |
PhoneNumber | VARCHAR(50) | Nullable, alternative recovery method |
UserData | NVARCHAR(MAX) | JSON blob for game progress, inventory, etc. |
CreatedAt | DATETIME | Account creation timestamp |
LastLoginAt | DATETIME | Last active timestamp |
API Endpoints & Implementation
We'll need 3 core endpoints for this flow:
Auto-Login / Account Creation
This endpoint handles both new and returning users:
[ApiController] [Route("api/auth")] public class AuthController : ControllerBase { private readonly AppDbContext _dbContext; private readonly IConfiguration _config; private readonly IVerificationService _verificationService; public AuthController(AppDbContext dbContext, IConfiguration config, IVerificationService verificationService) { _dbContext = dbContext; _config = config; _verificationService = verificationService; } [HttpPost("login")] public async Task<IActionResult> AutoLogin([FromBody] LoginRequest request) { // Check if device is already linked to an account var existingUser = await _dbContext.Users .FirstOrDefaultAsync(u => u.DeviceId == request.DeviceId); if (existingUser != null) { // Update last login timestamp existingUser.LastLoginAt = DateTime.UtcNow; await _dbContext.SaveChangesAsync(); var token = GenerateJwtToken(existingUser.UserId); return Ok(new AuthResponse { UserId = existingUser.UserId, UserData = existingUser.UserData, Token = token, IsNewUser = false }); } else { // Create new anonymous account var newUser = new User { UserId = Guid.NewGuid(), DeviceId = request.DeviceId, UserData = "{}", // Empty initial game data CreatedAt = DateTime.UtcNow, LastLoginAt = DateTime.UtcNow }; _dbContext.Users.Add(newUser); await _dbContext.SaveChangesAsync(); var token = GenerateJwtToken(newUser.UserId); return Ok(new AuthResponse { UserId = newUser.UserId, UserData = newUser.UserData, Token = token, IsNewUser = true }); } } // Helper: Generate JWT token for authenticated requests private string GenerateJwtToken(Guid userId) { var claims = new[] { new Claim(ClaimTypes.NameIdentifier, userId.ToString()) }; var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:SecretKey"])); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( issuer: _config["Jwt:Issuer"], audience: _config["Jwt:Audience"], claims: claims, expires: DateTime.UtcNow.AddDays(30), signingCredentials: creds); return new JwtSecurityTokenHandler().WriteToken(token); } } // Request/Response DTOs public class LoginRequest { public string DeviceId { get; set; } } public class AuthResponse { public Guid UserId { get; set; } public string UserData { get; set; } public string Token { get; set; } public bool IsNewUser { get; set; } }
Link Account for Cross-Device Access
Allow users to link their anonymous account to an email/phone (requires JWT authentication):
[HttpPost("link-email")] [Authorize] public async Task<IActionResult> LinkEmail([FromBody] LinkEmailRequest request) { var userId = Guid.Parse(User.FindFirst(ClaimTypes.NameIdentifier)?.Value); var user = await _dbContext.Users.FindAsync(userId); if (user == null) return NotFound("Account not found"); // Verify OTP sent to user's email if (!await _verificationService.VerifyEmailOtp(request.Email, request.Otp)) return BadRequest("Invalid verification code"); // Ensure email isn't linked to another account if (await _dbContext.Users.AnyAsync(u => u.Email == request.Email && u.UserId != userId)) return Conflict("Email already linked to another account"); user.Email = request.Email; await _dbContext.SaveChangesAsync(); return Ok("Email linked successfully"); } public class LinkEmailRequest { public string Email { get; set; } public string Otp { get; set; } }
Retrieve Account on New Device
Let users access their account via linked email/phone on a new device:
[HttpPost("retrieve-via-email")] public async Task<IActionResult> RetrieveAccount([FromBody] RetrieveAccountRequest request) { if (!await _verificationService.VerifyEmailOtp(request.Email, request.Otp)) return BadRequest("Invalid verification code"); var user = await _dbContext.Users.FirstOrDefaultAsync(u => u.Email == request.Email); if (user == null) return NotFound("No account linked to this email"); // Link new device to the existing account user.DeviceId = request.NewDeviceId; user.LastLoginAt = DateTime.UtcNow; await _dbContext.SaveChangesAsync(); var token = GenerateJwtToken(user.UserId); return Ok(new AuthResponse { UserId = user.UserId, UserData = user.UserData, Token = token, IsNewUser = false }); } public class RetrieveAccountRequest { public string Email { get; set; } public string Otp { get; set; } public string NewDeviceId { get; set; } }
2. Unity Client Integration
Get Stable Device ID
On Android, use Google's Advertising ID (more stable than SystemInfo.deviceUniqueIdentifier, which can change after factory resets):
using UnityEngine; public static class DeviceIdHelper { public static string GetDeviceId() { #if UNITY_ANDROID try { AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); AndroidJavaObject currentActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity"); AndroidJavaClass adIdClient = new AndroidJavaClass("com.google.android.gms.ads.identifier.AdvertisingIdClient"); AndroidJavaObject adInfo = adIdClient.CallStatic<AndroidJavaObject>("getAdvertisingIdInfo", currentActivity); string adId = adInfo.Call<string>("getId"); bool isAdTrackingEnabled = adInfo.Call<bool>("isLimitAdTrackingEnabled"); // Fallback to device unique ID if user disabled ad tracking return isAdTrackingEnabled ? SystemInfo.deviceUniqueIdentifier : adId; } catch (System.Exception e) { Debug.LogError($"Failed to get Advertising ID: {e.Message}"); return SystemInfo.deviceUniqueIdentifier; } #else return SystemInfo.deviceUniqueIdentifier; #endif } }
Auth Manager for API Calls
Wrap the backend API calls in a reusable Unity script:
using UnityEngine; using UnityEngine.Networking; using System.Threading.Tasks; public class GameAuthManager : MonoBehaviour { private const string ApiBaseUrl = "https://your-backend-url.com/api/auth"; private string _authToken; private void Start() { // Load saved token on app launch _authToken = PlayerPrefs.GetString("AuthToken", string.Empty); if (!string.IsNullOrEmpty(_authToken)) { // Optionally validate token with backend here } } public async Task<AuthResponse> AutoLogin() { string deviceId = DeviceIdHelper.GetDeviceId(); var requestData = new LoginRequest { DeviceId = deviceId }; using (var request = new UnityWebRequest($"{ApiBaseUrl}/login", "POST")) { string json = JsonUtility.ToJson(requestData); byte[] body = System.Text.Encoding.UTF8.GetBytes(json); request.uploadHandler = new UploadHandlerRaw(body); request.downloadHandler = new DownloadHandlerBuffer(); request.SetRequestHeader("Content-Type", "application/json"); await request.SendWebRequest(); if (request.result != UnityWebRequest.Result.Success) { Debug.LogError($"Login failed: {request.error}"); return null; } AuthResponse response = JsonUtility.FromJson<AuthResponse>(request.downloadHandler.text); _authToken = response.Token; PlayerPrefs.SetString("AuthToken", _authToken); PlayerPrefs.Save(); return response; } } public async Task<bool> LinkEmail(string email, string otp) { if (string.IsNullOrEmpty(_authToken)) return false; var requestData = new LinkEmailRequest { Email = email, Otp = otp }; using (var request = new UnityWebRequest($"{ApiBaseUrl}/link-email", "POST")) { string json = JsonUtility.ToJson(requestData); byte[] body = System.Text.Encoding.UTF8.GetBytes(json); request.uploadHandler = new UploadHandlerRaw(body); request.downloadHandler = new DownloadHandlerBuffer(); request.SetRequestHeader("Content-Type", "application/json"); request.SetRequestHeader("Authorization", $"Bearer {_authToken}"); await request.SendWebRequest(); return request.result == UnityWebRequest.Result.Success; } } // Serializable classes for JSON serialization [System.Serializable] private class LoginRequest { public string DeviceId; } [System.Serializable] public class AuthResponse { public string UserId; public string UserData; public string Token; public bool IsNewUser; } [System.Serializable] private class LinkEmailRequest { public string Email; public string Otp; } }
3. Key Security & Reliability Tips
- JWT Security: Store your JWT secret key in environment variables (never hardcode it). Set a reasonable token expiry (e.g., 30 days) and implement a token refresh endpoint for long sessions.
- OTP Best Practices: Use short-lived (5-10 minute) one-time codes for email/phone verification. Store used codes in your database to prevent reuse.
- Device ID Fallbacks: Always have a fallback for when the Advertising ID isn't available (e.g., older Android devices).
- Error Handling: Show user-friendly messages for failed requests (e.g., "Invalid verification code" or "Network error, please try again").
- Data Sync: Ensure game data is pulled from the backend on login, not just stored locally, to keep cross-device progress in sync.
内容的提问来源于stack exchange,提问作者Riddlah




