添加NuGet引用

IdentityModel
Microsoft.AspNetCore.Authorization.JwtBearer

在appsettings.json中添加JwtBearer配置

"Authentication": {
    "JwtBearer": {
      "IsEnabled": "true",
      "SecurityKey": "JWTStudyWebsite_DI20DXU3",
      "Issuer": "JWTStudy",
      "Audience": "JWTStudyWebsite"
    }
}

创建JWT服务注册扩展

public static class JwtConfiguration
{
    public static void AddJwtConfiguration(this IServiceCollection services, IConfiguration configuration)
    {
        if (bool.Parse(configuration["Authentication:JwtBearer:IsEnabled"]))
        {
            services.AddAuthentication(options => {
                options.DefaultAuthenticateScheme = "JwtBearer";
                options.DefaultChallengeScheme = "JwtBearer";
            }).AddJwtBearer("JwtBearer", options =>
            {
                options.Audience = configuration["Authentication:JwtBearer:Audience"];

                options.TokenValidationParameters = new TokenValidationParameters
                {
                    // The signing key must match!
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(
                        Encoding.ASCII.GetBytes(configuration["Authentication:JwtBearer:SecurityKey"])),

                    // Validate the JWT Issuer (iss) claim
                    ValidateIssuer = true,
                    ValidIssuer = configuration["Authentication:JwtBearer:Issuer"],

                    // Validate the JWT Audience (aud) claim
                    ValidateAudience = true,
                    ValidAudience = configuration["Authentication:JwtBearer:Audience"],

                    // Validate the token expiry
                    ValidateLifetime = true,

                    // If you want to allow a certain amount of clock drift, set that here
                    ClockSkew = TimeSpan.Zero
                };
            });
        }
    }
}

在startup>ConfigureServices中注册服务

services.AddJwtConfiguration(Configuration);

创建AccessTokenController

说明:用户首次使用用户名和密码登录,生成AccessToken和RefreshToken,
其中AccessToken的有效时间为30分钟,RefreshToken的有效时间为60分钟。

可能的情况

  • AccessToken没有过期
  • AccessToken已过期,RefreshToken未过期
  • RefreshToken已过期

一、首先创建一个方法,用于生成AccessToken

private string GetAccessToken(SessionUser user)
{
    var claims = new[]
    {
        new Claim(JwtClaimTypes.Id, user.Id.ToString()),
        new Claim(JwtClaimTypes.Name, user.Name),
        new Claim(JwtClaimTypes.Role, "user")
    };

    var key = new SymmetricSecurityKey(
        Encoding.UTF8.GetBytes(_configuration["Authentication:JwtBearer:SecurityKey"]));
    var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

    var token = new JwtSecurityToken(
        _configuration["Authentication:JwtBearer:Issuer"],
        _configuration["Authentication:JwtBearer:Audience"],
        claims,
        expires: DateTime.Now.AddMinutes(30),
        signingCredentials: credentials
    );

    return new JwtSecurityTokenHandler().WriteToken(token);
}

二、通过用户名密码获取AccessToken

[HttpPost]
public IActionResult Post([FromBody]LoginModel model)
{
    if (!string.IsNullOrWhiteSpace(model.Account) && !string.IsNullOrWhiteSpace(model.Pw))
    {
        var user = new SessionUser
        {
            Id = 1,
            Name = "admin",
            Role = "user"
        };

        var refreshToken = Guid.NewGuid().ToString("N");
        var refreshTokenExpiredTime = DateTime.Now.AddMinutes(60);

        var cacheKey = $"RefreshToken:{refreshToken}";
        var cacheValue = JsonConvert.SerializeObject(user);

        _cache.SetString(cacheKey, cacheValue,
            new DistributedCacheEntryOptions
            {
                AbsoluteExpiration = refreshTokenExpiredTime
            });

        return Ok(new
        {
            AccessToken = GetAccessToken(user),
            Code = 200,
            RefreshTokenExpired = DateTimeHelper.ConvertToLong(refreshTokenExpiredTime),
            RefreshToken = refreshToken
        });
    }

    return Ok(new { Code = 0, Token = "" });
}

三、通过RefreshToken获取新的AccessToken

[Authorize]
[HttpPost("Refresh")]
public IActionResult Refresh(RefreshTokenRequest request)
{
    var token = request.Token;
    var cacheStr = _cache.GetString($"RefreshToken:{token}");
    if (string.IsNullOrWhiteSpace(cacheStr))
    {
        return Ok(new
        {
            Code = 0,
            Message = "Token不存在或已过期"
        });
    }

    var cacheUser = JsonConvert.DeserializeObject<SessionUser>(cacheStr);
    var userId = User.Claims.First(c => c.Type == JwtClaimTypes.Id);

    if (userId == null || cacheUser.Id.ToString() != userId.Value)
    {
        return Ok(new
        {
            Code = 0,
            Message = "用户不匹配"
        });
    }

    var refreshToken = Guid.NewGuid().ToString("N");
    var cacheKey = $"RefreshToken:{refreshToken}";
    var refreshTokenExpiredTime = DateTime.Now.AddMinutes(60);

    _cache.SetString(cacheKey, cacheStr, new DistributedCacheEntryOptions
    {
        AbsoluteExpiration = DateTime.Now.AddMinutes(30)
    });

    return Ok(new
    {
        AccessToken = GetAccessToken(cacheUser),
        Code = 200,
        RefreshTokenExpired = DateTimeHelper.ConvertToLong(refreshTokenExpiredTime),
        RefreshToken = refreshToken
    });
}

完整代码

public class LoginModel
{
    [Required]
    public string Account { get; set; }

    [Required]
    public string Pw { get; set; }
}

public class SessionUser
{
    public int Id { get; set; }

    public string Name { get; set; }

    public string Role { get; set; }
}
public class DateTimeHelper
{
    /// <summary>
    /// DateTime转时间戳
    /// </summary>
    /// <param name="date"></param>
    /// <returns></returns>
    public static long ConvertToLong(DateTime date)
    {
        var startTime = TimeZoneInfo.ConvertTimeFromUtc(new DateTime(1970, 1, 1), TimeZoneInfo.Utc);
        return (new DateTimeOffset(date).UtcTicks - startTime.Ticks) / 10000;
    }

    /// <summary>
    /// 时间戳转DateTime
    /// </summary>
    /// <param name="timestamp"></param>
    /// <returns></returns>
    public static DateTime ConvertToDateTime(long timestamp)
    {
        var startTime = TimeZoneInfo.ConvertTimeFromUtc(new DateTime(1970, 1, 1), TimeZoneInfo.Local);
        return startTime.Add(new TimeSpan(timestamp * 10000));
    }
}
[Route("api/[controller]")]
[ApiController]
public class AccessTokenController : ControllerBase
{
    private readonly IConfiguration _configuration;
    private readonly IDistributedCache _cache;
    private readonly UserService _service;

    public AccessTokenController(IConfiguration configuration, IDistributedCache cache, UserService service)
    {
        _configuration = configuration;
        _cache = cache;
        _service = service;
    }

    /// <summary>
    /// 登录,获取后原来RefreshToken将失效。
    /// AccessToken有效时间30分钟
    /// RefreshToken有效时间60分钟
    /// </summary>
    /// <param name="model"></param>
    /// <returns></returns>
    [HttpPost]
    public ActionResult Post([FromBody]LoginModel model)
    {
        var result = _service.Login(model.Account, model.Pw);
        if (result.Code != 200)
        {
            return Ok(new {Code = 0, Message = result.Message});
        }

        var user = new SessionUser
        {
            Id = result.Body.Id,
            Name = result.Body.NickName,
            Role = "user"
        };

        var refreshToken = Guid.NewGuid().ToString("N");
        var refreshTokenExpiredTime = DateTime.Today.AddDays(7);

        var cacheKey = $"RefreshToken:{refreshToken}";
        var cacheValue = JsonConvert.SerializeObject(user);

        _cache.SetString(cacheKey, cacheValue,
            new DistributedCacheEntryOptions
            {
                AbsoluteExpiration = refreshTokenExpiredTime
            });

        return Ok(new
        {
            AccessToken = GetAccessToken(user),
            Code = 200,
            RefreshTokenExpired = DateTimeHelper.ConvertToLong(refreshTokenExpiredTime),
            RefreshToken = refreshToken
        });
    }

    /// <summary>
    /// 刷新AccessToken
    /// </summary>
    /// <param name="request">刷新的请求 {"token": "refresh_token"}</param>
    /// <returns></returns>
    [Authorize]
    [HttpPost("Refresh")]
    public IActionResult Refresh(RefreshTokenRequest request)
    {
        var token = request.Token;
        var cacheStr = _cache.GetString($"RefreshToken:{token}");
        if (string.IsNullOrWhiteSpace(cacheStr))
        {
            return Ok(new
            {
                Code = 0,
                Message = "Token不存在或已过期"
            });
        }

        var cacheUser = JsonConvert.DeserializeObject<SessionUser>(cacheStr);
        var userId = User.Claims.First(c => c.Type == JwtClaimTypes.Id);

        if (userId == null || cacheUser.Id.ToString() != userId.Value)
        {
            return Ok(new
            {
                Code = 0,
                Message = "用户不匹配"
            });
        }

        var refreshToken = Guid.NewGuid().ToString("N");
        var cacheKey = $"RefreshToken:{refreshToken}";
        var refreshTokenExpiredTime = DateTime.Today.AddDays(7);

        _cache.SetString(cacheKey, cacheStr, new DistributedCacheEntryOptions
        {
            AbsoluteExpiration = refreshTokenExpiredTime
        });

        return Ok(new
        {
            AccessToken = GetAccessToken(cacheUser),
            Code = 200,
            RefreshTokenExpired = DateTimeHelper.ConvertToLong(refreshTokenExpiredTime),
            RefreshToken = refreshToken
        });
    }

    /// <summary>
    /// 通过SessionUser获取AccessToken
    /// </summary>
    /// <param name="user"></param>
    /// <returns></returns>
    private string GetAccessToken(SessionUser user)
    {
        var claims = new[]
        {
            new Claim(JwtClaimTypes.Id, user.Id.ToString()),
            new Claim(JwtClaimTypes.Name, user.Name),
            new Claim(JwtClaimTypes.Role, "user")
        };

        var key = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(_configuration["Authentication:JwtBearer:SecurityKey"]));
        var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var token = new JwtSecurityToken(
            _configuration["Authentication:JwtBearer:Issuer"],
            _configuration["Authentication:JwtBearer:Audience"],
            claims,
            expires: DateTime.Now.AddHours(2),
            signingCredentials: credentials
        );

        return new JwtSecurityTokenHandler().WriteToken(token);
    }

    /// <summary>
    /// 刷新AccessToken的请求
    /// </summary>
    public class RefreshTokenRequest
    {
        /// <summary>
        /// RefreshToken,登录后获取
        /// </summary>
        public string Token { get; set; }
    }
}
01-25 16:04