保護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驗證,
參考資料
- 密碼哈希指南
- 加鹽哈希指南
- password-hashing
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/40673.html
標籤:.NET Core
