曦远 (DealiAxy)一份私人的文字刊物

StarBlog
返回文章列表折页手记阅读全文
StarBlog博客开发笔记

StarBlog Development Notes: Authentication Feature

Authentication uses JWT. For AspNetCore authentication and JWT, you can check out my previous blog post First, install the NuGet package dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer Create an AuthService class in the Services directory. Leave it without writing any code for now; we'll come back after finishing the prep work. User model In the Models of the StarBlog.Data project

曦远 (DealiAxy)约 27 分钟
目录12 个章节

Authentication uses JWT. For AspNetCore authentication and JWT, you can check out my previous blog post.

First, install the NuGet package

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

Create an AuthService class in the Services directory. Leave it without writing any code for now; we'll come back after finishing the prep work.

User Model

Create a User class in the Models directory of the StarBlog.Data project

The code is as follows

namespace StarBlog.Data.Models; 

public class User {
    public string Id { get; set; }
    public string Name { get; set; }
    public string Password { get; set; }
}

Configuration

Edit appsettings.json

Add the following configuration

  • Issuer is the token issuer; I used this project's name
  • Audience is the token recipient (consumer); I used the admin backend project's name
  • Key is the password used to encrypt the token. Just generate one with a random password generator; note that the length should be a multiple of 8
"SecuritySettings": {
    "Token": {
        "Issuer": "starblog",
        "Audience": "starblog-admin-ui",
        "Key": "Pox40XC.D5>v^2B7+KAt%WfXaz0B6zC5"
    }
}

PS: Putting the Key in the config file as shown in this article is not secure, especially in open-source projects. It's better to store this kind of secret data in environment variables. MSDN has more detailed materials on security; if you're going to production, you may want to look into that. To avoid adding complexity, I just took the lazy route and put the Key in appsettings.json. Sorry about that~

Configuration Model

Write an entity class to make it convenient to bind with the configuration

Create SecuritySettings.cs in the Models/Config directory. The code is as follows

namespace StarBlog.Web.Models.Config; 

public class SecuritySettings {
    public Token Token { get; set; }
}

public class Token {
    public string Issuer { get; set; }
    public string Audience { get; set; }
    public string Key { get; set; }
}

Register Services

To keep the code in Program.cs clean, we register services using extension methods

Create ConfigureAppSettings.cs in the Extensions directory to bind configuration. The code is as follows

using StarBlog.Web.Models.Config;

namespace StarBlog.Web.Extensions; 

public static class ConfigureAppSettings {
    public static void AddSettings(this IServiceCollection services, IConfiguration configuration) {
        // 安全配置
        services.Configure<SecuritySettings>(configuration.GetSection(nameof(SecuritySettings)));
    }
}

Next, create a ConfigureAuth.cs file to register some of the services needed for authentication. The code is as follows

using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using StarBlog.Web.Models.Config;
using StarBlog.Web.Services;

namespace StarBlog.Web.Extensions; 

public static class ConfigureAuth {
    public static void AddAuth(this IServiceCollection services, IConfiguration configuration) {
        services.AddScoped<AuthService>();
        services.AddAuthentication(options => {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(options => {
                var secSettings = configuration.GetSection(nameof(SecuritySettings)).Get<SecuritySettings>();
                options.TokenValidationParameters = new TokenValidationParameters {
                    ValidateAudience = true,
                    ValidateLifetime = true,
                    ValidateIssuer = true,
                    ValidateIssuerSigningKey = true,
                    ValidIssuer = secSettings.Token.Issuer,
                    ValidAudience = secSettings.Token.Audience,
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secSettings.Token.Key)),
                    ClockSkew = TimeSpan.Zero
                };
            });
    }
}

AuthService in here hasn't been written yet, but no rush—let's register the services first

Edit the Program.cs file and add these two lines of code

builder.Services.AddSettings(builder.Configuration);
builder.Services.AddAuth(builder.Configuration);

Core Authentication Code

The AspNetCore framework already has authentication built in, so there's very little we need to do—we just need to generate a JWT token for the client.

Let's get started

Models

First, create two classes in the ViewModels directory: LoginToken and LoginUser.

The code is as follows

namespace StarBlog.Web.ViewModels;

public class LoginToken {
    public string Token { get; set; }
    public DateTime Expiration { get; set; }
}
namespace StarBlog.Web.ViewModels; 

public class LoginUser {
    public string Username { get; set; }
    public string Password { get; set; }
}

AuthService

First, add the dependency injection

public class AuthService {
    private readonly SecuritySettings _securitySettings;
    private readonly IBaseRepository<User> _userRepo;

    public AuthService(IOptions<SecuritySettings> options, IBaseRepository<User> userRepo) {
        _securitySettings = options.Value;
        _userRepo = userRepo;
    }
}

Then we need to implement these features in AuthService

  • Generate token
  • Get user from the database
  • Extract user info from the token

Let's go through them one by one

Generate Token

First is generating the token. The code is as follows

public LoginToken GenerateLoginToken(User user) {
    var claims = new List<Claim> {
        new("username", user.Name),
        new(JwtRegisteredClaimNames.Name, user.Id), // User.Identity.Name
        new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), // JWT ID
    };
    var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_securitySettings.Token.Key));
    var signCredential = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
    var jwtToken = new JwtSecurityToken(
        issuer: _securitySettings.Token.Issuer,
        audience: _securitySettings.Token.Audience,
        claims: claims,
        expires: DateTime.Now.AddDays(7),
        signingCredentials: signCredential);

    return new LoginToken {
        Token = new JwtSecurityTokenHandler().WriteToken(jwtToken),
        Expiration = TimeZoneInfo.ConvertTimeFromUtc(jwtToken.ValidTo, TimeZoneInfo.Local)
    };
}

Get User from the Database

The code is as follows

public User? GetUserById(string userId) {
    return _userRepo.Where(a => a.Id == userId).ToOne();
}

public User? GetUserByName(string name) {
    return _userRepo.Where(a => a.Name == name).ToOne();
}

Extract User Info from the Token

Because the client submits the token to the server, and the JWT token contains user information, we can decrypt the user info directly from the token without accessing the database and wasting I/O performance.

The code is as follows

public User? GetUser(ClaimsPrincipal userClaim) {
    var userId = userClaim.Identity?.Name;
    var userName = userClaim.Claims.FirstOrDefault(c => c.Type == "username")?.Value;
    if (userId == null || userName == null) return null;
    return new User { Id = userId, Name = userName };
}

OK, the most critical part is done. Next up is writing a login API.

Login API

Nothing much here. Create AuthController.cs in the Apis directory, then here's the code

using Microsoft.AspNetCore.Mvc;
using StarBlog.Web.Services;
using StarBlog.Web.ViewModels;
using StarBlog.Web.ViewModels.Response;

namespace StarBlog.Web.Controllers; 

[ApiController]
[Route("Api/[controller]")]
public class AuthController : ControllerBase {
    private readonly AuthService _authService;

    public AuthController(AuthService authService) {
        _authService = authService;
    }

    [HttpPost]
    public ApiResponse<LoginToken> Login(LoginUser loginUser) {
        var user = _authService.GetUserByName(loginUser.Username);
        if (user==null) return ApiResponse.NotFound(Response);
        if(loginUser.Password != user.Password) return ApiResponse.Unauthorized(Response);
        return new ApiResponse<LoginToken>(_authService.GenerateLoginToken(user));
    }
}

Oh, and let's also add an endpoint to get the currently logged-in user's info.

The code is as follows

[Authorize]
[HttpGet]
public ActionResult<User> GetUser() {
    var user = _authService.GetUser(User);
    if (user == null) return NotFound();
    return user;
}

Added the [Authorize] attribute, which means this API requires login to use

Swagger little green lock

It's actually Swagger's request filter. After you configure the token, you can attach the token when making requests to access APIs that require login.

First, install a new NuGet dependency

dotnet add package Swashbuckle.AspNetCore.Filters

This component's project URL: https://github.com/mattfrear/Swashbuckle.AspNetCore.Filters

To keep the Program.cs code as concise as possible, we still create a new extension method to hold the Swagger configuration

Create a new ConfigureSwagger class under the Extensions directory

The code is as follows

using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.Filters;

namespace StarBlog.Web.Extensions;

public static class ConfigureSwagger {
    public static void AddSwagger(this IServiceCollection services) {
        services.AddSwaggerGen(options => {
            var security = new OpenApiSecurityScheme {
                Description = "JWT模式授权,请输入 \"Bearer {Token}\" 进行身份验证",
                Name = "Authorization",
                In = ParameterLocation.Header,
                Type = SecuritySchemeType.ApiKey
            };
            options.AddSecurityDefinition("oauth2", security);
            options.AddSecurityRequirement(new OpenApiSecurityRequirement { { security, new List<string>() } });
            options.OperationFilter<AddResponseHeadersFilter>();
            options.OperationFilter<AppendAuthorizeToSummaryOperationFilter>();
            options.OperationFilter<SecurityRequirementsOperationFilter>();

            var filePath = Path.Combine(System.AppContext.BaseDirectory, $"{typeof(Program).Assembly.GetName().Name}.xml");
            options.IncludeXmlComments(filePath, true);
        });
    }
}

Then go back to the Program.cs file and just add this line of code

builder.Services.AddSwagger();

_EOF_

StarBlog Development Notes: Authentication Feature

本文作者:
程序设计实验室
版权声明:
本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

相关文章

StarBlog博客开发笔记(22):开发博客文章相关接口

前言 本文介绍博客文章相关接口的开发,作为接口开发介绍的第一篇,会写得比较详细,以抛砖引玉,后面的其他接口就粗略带过了,着重于WebApi开发的周边设施。 涉及到的接口:文章CRUD、置顶文章、推荐文章等。 开始前先介绍下AspNetCore框架的基础概念,MVC模式(前后端不分离)、WebApi模式(前后端分离),都是有Controller的。 区别在前者的Controller集成自 Contr

2023-01-02 · StarBlog博客开发笔记 · 约 1 分钟

讨论

共 0 条

良言一句三冬暖,恶语伤人六月寒。

加载评论中…