能否不依赖Indy组件单独使用SSPI实现Google RPC服务器端NTLM认证?
Absolutely! You don’t need to rely on Indy components at all—you can implement NTLM authentication on your gRPC server directly using Windows’ native SSPI (Security Support Provider Interface) APIs. Let’s break down how to do this:
NTLM follows a multi-round challenge-response flow. On the server side, your main work revolves around the AcceptSecurityContext API (paired with supporting functions) to process client authentication messages, manage security contexts, and validate the client’s identity. You won’t need InitializeSecurityContext here—that’s primarily for the client side of the flow.
1. Acquire Server Credentials
First, you need to obtain a handle to the NTLM security provider’s credentials for inbound authentication. Use AcquireCredentialsHandle with the SECPKG_CRED_INBOUND flag, which tells SSPI we’re acting as a server accepting auth requests:
var hCred: CredHandle; tsExpiry: TimeStamp; Status: SECURITY_STATUS; begin Status := AcquireCredentialsHandle( nil, // No principal name needed for server inbound 'NTLM', // Target security provider SECPKG_CRED_INBOUND, // Server-side credential type nil, nil, nil, nil, hCred, tsExpiry ); if Status <> SEC_E_OK then raise Exception.CreateFmt('AcquireCredentialsHandle failed: %x', [Status]); end;
2. Handle the Client’s Initial Negotiate Message
The client starts by sending an NTLM Negotiate message (often an empty or minimal payload). You’ll pass this to AcceptSecurityContext to generate a Challenge message, which you send back to the client:
var hCtx: CtxtHandle; InputBufferDesc: SecBufferDesc; InputBuffer: SecBuffer; OutputBufferDesc: SecBufferDesc; OutputBuffer: SecBuffer; Status: SECURITY_STATUS; fNewContext: BOOL; tsExpiry: TimeStamp; begin // Set up input buffer with the client's Negotiate message InputBuffer.cbBuffer := Length(ClientNegotiateData); InputBuffer.BufferType := SECBUFFER_TOKEN; InputBuffer.pvBuffer := @ClientNegotiateData[0]; InputBufferDesc.ulVersion := SECBUFFER_VERSION; InputBufferDesc.cBuffers := 1; InputBufferDesc.pBuffers := @InputBuffer; // Prepare output buffer for the server's Challenge message OutputBuffer.cbBuffer := 1024; // Allocate enough space for NTLM Challenge OutputBuffer.BufferType := SECBUFFER_TOKEN; OutputBuffer.pvBuffer := AllocMem(OutputBuffer.cbBuffer); OutputBufferDesc.ulVersion := SECBUFFER_VERSION; OutputBufferDesc.cBuffers := 1; OutputBufferDesc.pBuffers := @OutputBuffer; // First call to AcceptSecurityContext (no existing context yet) Status := AcceptSecurityContext( @hCred, nil, // No existing context on first call @InputBufferDesc, 0, // No special flags SECURITY_NATIVE_DREP, // Native data representation @hCtx, @OutputBufferDesc, nil, // No attributes needed here tsExpiry ); case Status of SEC_I_CONTINUE_NEEDED: begin // Send the Challenge message in OutputBuffer to the client SendChallengeToClient(OutputBuffer.pvBuffer, OutputBuffer.cbBuffer); end; SEC_E_OK: begin // Rare for first call, but possible if auth completes immediately // Proceed to validate client identity end; else raise Exception.CreateFmt('AcceptSecurityContext (Negotiate) failed: %x', [Status]); end; end;
3. Process the Client’s Response Message
After the client receives your Challenge, it sends back a Response message. Call AcceptSecurityContext again with the existing context handle to validate this response:
// Reuse hCtx from the previous step, set up input buffer with client's Response Status := AcceptSecurityContext( @hCred, @hCtx, // Existing context from first call @InputBufferDesc, // Client's Response message 0, SECURITY_NATIVE_DREP, @hCtx, @OutputBufferDesc, nil, tsExpiry ); case Status of SEC_E_OK: begin // Authentication succeeded! Retrieve the client's identity var Names: SecPkgContext_Names; Status := QueryContextAttributes(@hCtx, SECPKG_ATTR_NAMES, @Names); if Status = SEC_E_OK then begin try ShowMessageFmt('Authenticated user: %s\%s', [Names.sDomainName, Names.sUserName]); finally // Free the strings returned by SSPI FreeContextBuffer(Names.sDomainName); FreeContextBuffer(Names.sUserName); end; end; end; SEC_I_CONTINUE_NEEDED: begin // Uncommon for NTLM, but handle if needed (send any output to client) end; else raise Exception.CreateFmt('AcceptSecurityContext (Response) failed: %x', [Status]); end;
4. Clean Up Resources
Always release SSPI handles to avoid memory leaks:
// Delete the security context when done with the connection DeleteSecurityContext(@hCtx); // Free the credentials handle when your server shuts down FreeCredentialsHandle(@hCred); // Free any allocated buffers FreeMem(OutputBuffer.pvBuffer);
- Message Transport: NTLM auth data is typically passed in HTTP/2 headers (since gRPC uses HTTP/2 under the hood). You’ll need to intercept incoming requests to extract the
Authorizationheader (containing NTLM tokens) and inject theWWW-Authenticateheader when sending challenges. - Connection Context: Since NTLM is per-connection, you’ll need to track the security context (
hCtx) for each ongoing client connection until auth completes. - Error Handling: Pay close attention to
SECURITY_STATUScodes—they guide the flow of the auth process (e.g.,SEC_I_CONTINUE_NEEDEDmeans you need another round-trip with the client).
内容的提问来源于stack exchange,提问作者Paul

