保護WEBAPI有哪些方法?
微軟官方文件推薦了好幾個:
- Azure Active Directory
- Azure Active Directory B2C (Azure AD B2C)]
- IdentityServer4
前面兩個看著就覺得搞不太明白,第三個倒是非常常見,相關的文章也很多。不過這個東西是獨立部署的,太重了,如果我就想寫一個簡單一點的API,把認證給包括的,是不是有好辦法?
準備
假設你的WEBAPI使用JWT TOKEN來儲存你的認證資訊,並且通過JWT TOKEN進行保護。那麼我們可以設計一個整合有認證授權的WEBAPI服務,一站式解決問題,程式碼簡單且方便自行修改。
要點:
- 使用類似[Authorize]的授權,需要基於token中
role
這個Claim來實現。 - 密碼的儲存需要進行特別設計。
- 使用者物件返回需要避免password和passwordhash的傳遞。
專案特點:
- RESTful設計(正常來說api的資源應該是複數userinfos,但是info應該就是不可數的,不糾結了。)
- 整合Swagger
- ASP.NET Core 3.1
- nullable設計
- EF Core
- 使用者許可權控制
- 密碼安全儲存
- Token實現與API整合
- 簡單易於理解
使用者實體類
所有認證之類的工作都在API這邊實現,因此我們需要一個userinfo類來進行處理。
[DataContract]
[Table("userinfo")]
public class UserInfo
{
[DataMember]
[Key]
public string UserId { get; set; } = default!;
//傳輸的過程中會用到密碼,但是這個密碼不應該被存入資料庫中。
[NotMapped]
[DataMember]
public string? Password { get; set; }
//傳輸的過程中不會用到密碼雜湊值,但是雜湊值需要存入資料庫中。
[IgnoreDataMember]
public string? PasswordHash { get; set; }
[DataMember]
public string? Role { get; set; }
public static string GetRole(string? role)
{
if (string.IsNullOrWhiteSpace(role)) return "User";
return role.ToLower() switch
{
"administrator" => "Administrator",
"supervisor" => "Supervisor",
_ => "User"
};
}
}
- 使用json進行序列化,[DataContract]不是必須的,我一般是不喜歡寫這個東西,不寫的話,那麼所有的public屬性和欄位都會被序列化;如果標記了[DataContract],那麼只有標記有[DataMember]的會被序列化,使用[IgnoreDataMember]可以阻止序列化。
- 使用了EF Core用來持久化,標記[NotMapped]指示屬性不被對映到資料庫中,一般來說,資料庫不應該直接儲存密碼。
令牌發放
具體實現TokenController如下。
[AllowAnonymous]
[HttpPost]
public ActionResult Post(UserInfo login)
{
ActionResult response = BadRequest("登入失敗,請檢查使用者名稱和密碼");
var user = AuthenticateUser(login);
if (user != null)
{
var tokenString = GenerateJSONWebToken(user);
response = Ok(new { access_token = tokenString, role = user.Role });
}
return response;
}
private string GenerateJSONWebToken(UserInfo userInfo)
{
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var claims = new[] {
new Claim(JwtRegisteredClaimNames.Sub, userInfo.UserName),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(ClaimTypes.Role, userInfo.Role),
};
var token = new JwtSecurityToken(null,
null,
claims,
expires: DateTime.Now.AddMinutes(120),
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
private UserInfo? AuthenticateUser(UserInfo login)
{
UserInfo? user = null;
if (string.IsNullOrWhiteSpace(login.Password)) return user;
using (var context = new ManageDataContext())
{
var result = context.UserInfos.Where(w => w.UserName.ToLower() == login.UserName.ToLower()).FirstOrDefault();
if (result != null)
if (PasswordStorage.VerifyPassword(login.Password, result.PasswordHash!)) user = result;
}
return user;
}
上面的類標誌有AllowAnonymous
,表示這個類是可以匿名訪問的,使用者先請求post請求token,然後再攜帶token訪問其他API。
上面用到一個PasswordStorage的庫,這個庫使用了加鹽雜湊的形式儲存了密碼,實踐上比較可靠。值得一提的是它的
VerifyPassword()
函式,使用的比較演算法很巧妙,我貼在了文末,推薦大家閱讀。
受保護的API
被保護的使用者管理API如下,只貼了一小部分:
[EnableCors("AllowAll")]
[Route("api/[controller]")]
//只有角色為Admin可以訪問
[Authorize(Roles = "Admin")]
//如果需要增加種子資料,可以註釋上面這行,取消註釋下面這一行
//[AllowAnonymous]
[ApiController]
public class UserInfoController : ControllerBase
{
private readonly ManageDataContext _context;
public UserInfoController(ManageDataContext context)
{
_context = context;
}
/// <summary>
/// 有參GET請求
/// </summary>
/// <param name="id">使用者編號id</param>
/// <returns></returns>
[HttpGet("{id}")]
[ProducesResponseType(typeof(UserInfo), Status200OK)]
[ProducesResponseType(typeof(string), Status404NotFound)]
public async Task<ActionResult> Get(string id)
{
var res = await _context.UserInfos.FindAsync(id);
if (res != null) return Ok(res);
else return NotFound("Cannot find key.");
}
}
啟動配置
Startup.cs注意一下順序的問題。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
//實際測試,這個UseCors如果在UseAuthentication和UseAuthorization的後面,可能會導致vue.js訪問問題。
app.UseCors("AllowAll");
app.UseAuthentication();
app.UseAuthorization();
}
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
//使用AddNewtonsoftJson為了避免json的嚴格檢查。
services.AddControllers().AddNewtonsoftJson();
services.AddDbContext<ManageDataContext>();
//後面還有不貼了
}
在ConfigureServices裡面,呼叫了AddNewtonsoftJson()
。之所以沒有使用到預設的System.Text.Json
,是因為它對客戶端上傳的資訊要求太嚴格,如果是integer型別的值,上傳使用了string就不能正確識別物件,而Newtonsoft.Json
沒有這個問題。
也可以修改
System.Text.Json
的預設行為,但是總是沒有那麼方便了。
呼叫方法
請求令牌
POST請求,api/token,設定header:Content-Type為application/json。body內容如下:
{
"userName": "admin",
"password": "123"
}
呼叫即可返回access_token與role。
呼叫被保護的API
需要設定header:
- Authorization值為Bearer [獲取到的token]
- Content-Type為application/json
然後就可以自由呼叫自己有權訪問的API了。
總結
零零散散寫了這麼些,直接貼上程式碼,專案是基於asp.net core 3.1與swagger的,本專案也可以作為一些小型專案的模板。
需要新建使用者的話,可以註釋掉[Authorize]或者我已經準備了一個使用者admin,密碼是123。
如果需要在windows上進行服務部署,可以參考我之前寫的TopShelf的文章。
Github專案地址,歡迎Fork或者Star。
展望
- token重新整理與吊銷。
- 註冊與手機/Email驗證。