身份认证使用JWT,关于AspNetCore的身份认证和JWT可以看看我之前这篇博客

先安装nuget包

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

Services目录下新建一个AuthService类,先留着不写代码,等把准备工作完成了再来。

用户模型

StarBlog.Data 项目的 Models 目录下新建 User

代码如下

namespace StarBlog.Data.Models; 

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

配置

编辑appsettings.json

添加配置

  • Issuer 是token的发行者,我写了这个项目的名称
  • Audience 是token的接受者(使用者),我写了管理后台的项目名称
  • Key 是用来加密token的密码,找一个随机密码生成器生成一个就好了,注意位数要8的位数
"SecuritySettings": {
    "Token": {
        "Issuer": "starblog",
        "Audience": "starblog-admin-ui",
        "Key": "Pox40XC.D5>v^2B7+KAt%WfXaz0B6zC5"
    }
}

PS:本文里这种把Key放在配置文件的做法并不安全,特别是在开源项目中,这种机密数据用环境变量来存更好,MSDN上有关于安全这方面的更详细的资料,如果上生产项目的话可以关注一下。这里为了不增加复杂度我就直接偷懒把Key放在appsettings.json里了,见谅哈~

配置模型

写一个实体类,方便和配置绑定起来使用

Models/Config目录下新建SecuritySettings.cs,代码如下

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; }
}

注册服务

为了Program.cs文件内的代码整洁,我们用扩展方法的方式来注册服务

Extensions目录下新建ConfigureAppSettings.cs,用来绑定配置,代码如下

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)));
    }
}

继续新建ConfigureAuth.cs文件,用来注册认证需要的一些服务,代码如下

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还没写,但不急,先来注册服务

编辑Program.cs文件,添加这两行代码

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

身份认证关键代码

AspNetCore框架已经把身份认证的功能内置了,所以我们要做的事情很少,只需要生成一个JWT token给客户端就行了。

那就开始吧

模型

先在 ViewModels 目录下新建两个类,分别是 LoginTokenLoginUser

代码如下

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

先写上依赖注入

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;
    }
}

然后我们要在 AuthService 里实现

  • 生成token
  • 从数据库获取用户
  • 从token里取出用户信息

这几个功能

一个个来

生成token

首先是生成token,代码如下

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)
    };
}

从数据库获取用户

代码如下

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();
}

从token里取出用户信息

因为客户端要提交token到服务端,JWT token里包含用户信息,所以我们可以直接从token里解密出用户信息,不需要访问数据库浪费IO性能。

代码如下

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,最关键的部分完成了,接下来就是写个登录接口

登录接口

没啥东西,在 Apis 目录下新建 AuthController.cs,然后直接上代码

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));
    }
}

哦,再加个一个获取当前登录用户信息的接口。

代码如下

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

加了 [Authorize] 特性,表示这个接口需要登录才能用

swagger小绿锁

其实就是swagger的请求过滤器,配置了token之后,可以在请求的时候带上token,访问需要登录的接口。

首先要安装一个新的nuget依赖

dotnet add package Swashbuckle.AspNetCore.Filters

这个组件的项目地址:https://github.com/mattfrear/Swashbuckle.AspNetCore.Filters

为了使Program.cs的代码尽量简洁,我们依然新建一个扩展方法来放swagger的配置

Extensions目录下新建ConfigureSwagger

代码如下

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);
        });
    }
}

然后回到Program.cs文件,添加这行代码就行

builder.Services.AddSwagger();