繼完成 驗證 AD 密碼的 API 後,接著嘗試寫修改密碼的部份,跌跌撞撞的也總算完成。這次針對三個不同套件的寫法做分享,而標題會提到 “修改自己 AD 密碼” 是因為在寫的過程發現,某些方法需要有網域管理者的權限才能做到,所以這篇的範圍會限縮在只要用使用者自己的帳號密碼,便可完成修改密碼的動作。
《專案範本》
- ASP .NET Core Web API
《套件》
一般來說,要修改 AD 會用到的套件可能會有以下三種:
- System.DirectoryServices.Protocols (S.DS.P)
- System.DirectoryServices (S.DS)
- System.DirectoryServices.AccountManagement (S.DS.AM)
三種套件比較:
- 操作層級:S.DS.AM > S.DS > S.DS.P
- 複雜度:S.DS.P > S.DS > S.DS.AM
- 靈活度:S.DS.P > S.DS > S.DS.AM
- 速度:S.DS.P > S.DS > S.DS.AM
另外,由於 S.DS.P 是基於 LDAP 協定的低層級操作,所以相容性也高,對於 OpenLDAP、AD 的支援都沒有問題,但 S.DS 跟 S.DS.AM 可能就會比較受限,適用於微軟的 AD (Active Directory)。
Services/PasswordManagementService.cs
using AD.Models;
using Microsoft.Extensions.Options;
using System.DirectoryServices;
using System.DirectoryServices.Protocols;
using System.Net;
using System.Text;
using System.DirectoryServices.AccountManagement;
using System.Runtime.Versioning;
namespace AD.Services
{
public class PasswordManagementService(IOptions<LdapSettings> ldapSettings)
{
private readonly string _ldapServer = ldapSettings.Value.Server;
private readonly string _domain = ldapSettings.Value.Domain;
private readonly string _baseDn = ldapSettings.Value.BaseDn;
// 修改密碼 (透過 System.DirectoryServices.Protocols)
public bool ChangePasswordSdsP(string username, string oldPassword, string newPassword)
{
try
{
using var connection = new LdapConnection(new LdapDirectoryIdentifier(_ldapServer, 636)); // 修改密碼必須使用 LDAPS 636 port。
connection.Credential = new NetworkCredential(username, oldPassword, _domain);
connection.SessionOptions.SecureSocketLayer = true; // 修改密碼必須使用 LDAPS。
connection.AuthType = AuthType.Kerberos; // 如果使用 Negotiate 會先嘗試 Kerberos,失敗再改試 NTLM。
connection.Bind(); // 嘗試綁定,成功表示驗證通過
// 以 sAMAccountName 查詢使用者的 DN
SearchRequest searchRequest = new(
_baseDn, // 根目錄
$"(sAMAccountName={username})", // 根據 sAMAccountName 查詢
System.DirectoryServices.Protocols.SearchScope.Subtree,
"distinguishedName" // 只獲取 DN 屬性
);
SearchResponse searchResponse = (SearchResponse)connection.SendRequest(searchRequest);
if (searchResponse.Entries.Count == 0)
{
Console.WriteLine("User not found.");
return false;
}
string userDn = searchResponse.Entries[0].DistinguishedName;
// 使用 LDAP 修改密碼屬性 (unicodePwd) 時,
// 只有高權限帳號 (如 Domain Admins) 可以對 unicodePwd 屬性執行修改 (DirectoryAttributeOperation.Replace)。
// 若要讓一般使用者更改自己的密碼,必須透過 LDAPS (安全通道),且同時送出 delete 與 add 操作來替換密碼。
var deleteOldPassword = new DirectoryAttributeModification
{
Operation = DirectoryAttributeOperation.Delete,
Name = "unicodePwd"
};
deleteOldPassword.Add(Encoding.Unicode.GetBytes($"\"{oldPassword}\""));
var addNewPassword = new DirectoryAttributeModification
{
Operation = DirectoryAttributeOperation.Add,
Name = "unicodePwd"
};
addNewPassword.Add(Encoding.Unicode.GetBytes($"\"{newPassword}\""));
// 組合 ModifyRequest,執行 Delete + Add 操作
var request = new ModifyRequest(
userDn,
deleteOldPassword,
addNewPassword
);
connection.SendRequest(request);
return true;
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
return false;
}
}
// 修改密碼 (透過 System.DirectoryServices)
[SupportedOSPlatform("windows")] // 宣告以下方法僅適用於 Windows,避免 PrincipalContext 等 API 被提示要留意跨平臺問題。
public bool ChangePasswordSds(string username, string oldPassword, string newPassword)
{
try
{
// 使用 DirectorySearcher 以 sAMAccountName 查詢使用者的 DN
DirectorySearcher searcher = new(new DirectoryEntry($"LDAP://{_baseDn}"));
searcher.Filter = $"(sAMAccountName={username})";
searcher.PropertiesToLoad.Add("distinguishedName");
SearchResult result = searcher.FindOne();
if (result != null)
{
string userDn = result.Properties["distinguishedName"][0].ToString();
using DirectoryEntry user = new($"LDAP://{userDn}", username, oldPassword);
user.Invoke("ChangePassword", [oldPassword, newPassword]);
user.CommitChanges();
return true;
}
else
{
Console.WriteLine("User not found.");
return false;
}
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
return false;
}
}
// 修改密碼 (透過 System.DirectoryServices.AccountManagement)
[SupportedOSPlatform("windows")] // 宣告以下方法僅適用於 Windows,避免 PrincipalContext 等 API 被提示要留意跨平臺問題。
public bool ChangePasswordSdsAm(string username, string oldPassword, string newPassword)
{
try
{
using var context = new PrincipalContext(ContextType.Domain, _domain);
// 驗證舊密碼是否正確
if (!context.ValidateCredentials(username, oldPassword))
{
throw new UnauthorizedAccessException("用戶名或舊密碼不正確!");
}
// 使用 UserPrincipal 修改密碼
using var user = UserPrincipal.FindByIdentity(context, username) ?? throw new Exception("找不到指定的使用者!");
user.ChangePassword(oldPassword, newPassword);
return true;
}
catch (Exception ex)
{
Console.WriteLine($"修改密碼時發生錯誤: {ex.Message}");
return false;
}
}
}
}
Controllers/PasswordManagementController.cs
using AD.Models;
using AD.Services;
using Microsoft.AspNetCore.Mvc;
using System.Runtime.Versioning;
namespace AD.Controllers
{
[Route("Password")]
[ApiController]
public class PasswordManagementController(PasswordManagementService passwordManagement) : ControllerBase
{
// 修改密碼 (透過 System.DirectoryServices.Protocols)
[HttpPost("ChangeSdsP")]
public IActionResult ChangeSdsP([FromBody] PasswordChangeRequest request)
{
if (passwordManagement.ChangePasswordSdsP(request.Username, request.OldPassword, request.NewPassword))
{
return Ok("密碼修改成功。");
}
return BadRequest("密碼修改失敗。");
}
// 修改密碼 (透過 System.DirectoryServices)
[HttpPost("ChangeSds")]
[SupportedOSPlatform("windows")] // 宣告以下用到的方法僅適用於 Windows,避免 ChangePassword 方法被提示要留意跨平臺問題。
public IActionResult ChangeSds([FromBody] PasswordChangeRequest request)
{
if (passwordManagement.ChangePasswordSds(request.Username, request.OldPassword, request.NewPassword))
{
return Ok("密碼修改成功。");
}
return BadRequest("密碼修改失敗。");
}
// 修改密碼 (透過 System.DirectoryServices.AccountManagement)
[HttpPost("ChangeSdsAm")]
[SupportedOSPlatform("windows")] // 宣告以下用到的方法僅適用於 Windows,避免 ChangePassword 方法被提示要留意跨平臺問題。
public IActionResult ChangeSdsAm([FromBody] PasswordChangeRequest request)
{
if (passwordManagement.ChangePasswordSdsAm(request.Username, request.OldPassword, request.NewPassword))
{
return Ok("密碼修改成功。");
}
return BadRequest("密碼修改失敗。");
}
}
}
Models/AuthRequests.cs
namespace AD.Models
{
public class PasswordChangeRequest
{
public string Username { get; set; } // 使用者帳戶
public string OldPassword { get; set; } // 舊密碼
public string NewPassword { get; set; } // 新密碼
}
}
Models/LdapSettings.cs
namespace AD.Models
{
public class LdapSettings
{
public string Server { get; set; } = string.Empty;
public string Domain { get; set; } = string.Empty;
public string BaseDn { get; set; } = string.Empty;
}
}
appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"LdapSettings": {
"Server": "dc1.abc.com.tw", // 如果是用 Kerberos 驗證,AD 的伺服器不可以使用 IP。
"Domain": "abc.com.tw",
"BaseDn": "DC=abc,DC=com,DC=tw"
}
}
Program.cs
using AD.Models;
using AD.Services;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.Configure<LdapSettings>(builder.Configuration.GetSection("LdapSettings")); // 讀取 appsettings.json 的 LdapSettings 資料。
builder.Services.AddScoped<PasswordManagementService>();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
// 讓 Swagger 只在開發環境時使用。
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
測試時,建議透過 Swagger,會方便許多。

最後,DC 伺服器要確認有支援 LDAPS,除了 636 要 ping 的通,還要幫 DC 裝憑證,可以用 DC 上的 ldp.exe 工具來驗證會比較準確。另外,記得要確認測試帳號的「使用者不能變更密碼」沒有打勾,我卡在這邊好幾天,最後才發現是我忘了取消勾選..。
《參考連結》







