身份认证使用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 目录下新建两个类,分别是 LoginToken 和  LoginUser。
代码如下
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();