一站式WebAPI與認證授權服務


保護WEBAPI有哪些方法?

微軟官方文檔推薦了好幾個:

  • Azure Active Directory
  • Azure Active Directory B2C (Azure AD B2C)]
  • IdentityServer4

前面兩個看着就覺得搞不太明白,第三個倒是非常常見,相關的文章也很多。不過這個東西是獨立部署的,太重了,如果我就想寫一個簡單一點的API,把認證給包括的,是不是有好辦法?

准備

假設你的WEBAPI使用JWT TOKEN來保存你的認證信息,並且通過JWT TOKEN進行保護。那么我們可以設計一個集成有認證授權的WEBAPI服務,一站式解決問題,代碼簡單且方便自行修改。

要點:

  1. 使用類似[Authorize]的授權,需要基於token中role這個Claim來實現。
  2. 密碼的保存需要進行特別設計。
  3. 用戶對象返回需要避免password和passwordhash的傳遞。

項目特點:

  1. RESTful設計(正常來說api的資源應該是復數userinfos,但是info應該就是不可數的,不糾結了。)
  2. 集成Swagger
  3. ASP.NET Core 3.1
  4. nullable設計
  5. EF Core
  6. 用戶權限控制
  7. 密碼安全存儲
  8. Token實現與API集成
  9. 簡單易於理解

用戶實體類

所有認證之類的工作都在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。

展望

  1. token刷新與吊銷。
  2. 注冊與手機/Email驗證。

參考資料

  1. 密碼哈希指南
  2. 加鹽哈希指南
  3. password-hashing


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM