using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Security.Claims; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; namespace JsonWebTokenTesting { public class Program { public static void Main(string[] args) { CreateWebHostBuilder(args).Build().Run(); } public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>(); } public class Startup { public Startup() { JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); } public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("O73xQtVrtLnBIEA+Oaq79x1d2BGOFbmHroVuE4ogrOk=")), ValidateIssuer = true, ValidIssuer = "http://localhost:5000", ValidateAudience = true, ValidAudience = "http://localhost:5000", ValidateLifetime = true, ClockSkew = TimeSpan.FromMinutes(0) }; options.Events = new JwtBearerEvents { OnAuthenticationFailed = context => { if (context.Exception.GetType() == typeof(SecurityTokenExpiredException)) { context.HttpContext.Response.Headers.Remove("Token-Expired"); context.HttpContext.Response.Headers.Add("Token-Expired", "Relative"); } return Task.CompletedTask; } }; }); services.AddCors(options => { options.AddPolicy("default", builder => { builder.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod().AllowCredentials().WithExposedHeaders("Token-Expired"); }); }); services.AddMvc(options => { options.Filters.Add(new ExceptionFilter()); }); } public void Configure(IApplicationBuilder app) { app.UseDeveloperExceptionPage(); app.UseAuthentication(); app.UseCors("default"); app.UseMvc(); } } public class ExceptionFilter : IExceptionFilter { public void OnException(ExceptionContext context) { var exception = context.Exception; var statusCode = StatusCodes.Status500InternalServerError; if (exception is SecurityTokenException) { statusCode = StatusCodes.Status401Unauthorized; context.HttpContext.Response.Headers.Remove("Token-Expired"); context.HttpContext.Response.Headers.Add("Token-Expired", "Absolute"); } context.Result = new ObjectResult(exception.Message) { StatusCode = statusCode }; } } public class TokenCreateInput { public string Account { get; set; } public string Password { get; set; } } public class TokenRefreshInput { public string AccessToken { get; set; } public string RefreshToken { get; set; } } [Route("token")] public class TokenController : ControllerBase { private static readonly Dictionary<string, dynamic> _dict = new Dictionary<string, dynamic>(); [HttpPost("create")] public IActionResult Create([FromBody]TokenCreateInput input) { // todo: check the argument var userId = "10086"; // todo: validate the user var accessToken = CreateAccessToken( new Claim[] { new Claim(JwtRegisteredClaimNames.Sub, userId) } ); var refreshToken = CreateRefreshToken(); _dict[userId] = new { RefreshToken = refreshToken, RefreshTokenExpires = DateTime.UtcNow.AddSeconds(60) }; return Ok(new { accessToken, refreshToken, _dict }); } [HttpPost("refresh")] public IActionResult Refresh([FromBody]TokenRefreshInput input) { // todo: check the argument var principal = ValidateAccessToken(input.AccessToken); var userId = principal.Claims.First(v => v.Type == JwtRegisteredClaimNames.Sub)?.Value; _dict.TryGetValue(userId, out var details); if (details.RefreshToken == input.RefreshToken && details.RefreshTokenExpires >= DateTime.UtcNow) { var accessToken = CreateAccessToken( new Claim[] { new Claim(JwtRegisteredClaimNames.Sub, userId) } ); var refreshToken = CreateRefreshToken(); _dict[userId] = new { RefreshToken = refreshToken, RefreshTokenExpires = DateTime.UtcNow.AddSeconds(60) }; return Ok(new { accessToken, refreshToken, _dict }); } else { throw new SecurityTokenException("Invalid RefreshToken"); } } private string CreateAccessToken(Claim[] claims) { return new JwtSecurityTokenHandler().WriteToken( new JwtSecurityToken( issuer: "http://localhost:5000", audience: "http://localhost:5000", claims: claims, notBefore: DateTime.UtcNow, expires: DateTime.UtcNow.AddSeconds(20), signingCredentials: new SigningCredentials( key: new SymmetricSecurityKey(Encoding.UTF8.GetBytes("O73xQtVrtLnBIEA+Oaq79x1d2BGOFbmHroVuE4ogrOk=")), algorithm: SecurityAlgorithms.HmacSha256 ) ) ); } private string CreateRefreshToken() { var random = new byte[32]; using (var generator = RandomNumberGenerator.Create()) { generator.GetBytes(random); return Convert.ToBase64String(random); } } private ClaimsPrincipal ValidateAccessToken(string accessToken) { try { return new JwtSecurityTokenHandler().ValidateToken( accessToken, new TokenValidationParameters { ValidateAudience = false, ValidateIssuer = false, ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("O73xQtVrtLnBIEA+Oaq79x1d2BGOFbmHroVuE4ogrOk=")), ValidateLifetime = false }, out var validatedToken ); } catch (Exception) { throw new SecurityTokenException("Invalid AccessToken"); } } } [Authorize] [Route("users")] public class UserController : ControllerBase { [HttpGet] public IActionResult Get() { return Ok(User.FindFirst(v => v.Type == JwtRegisteredClaimNames.Sub)?.Value); } } }
import axios from 'axios' axios.defaults.baseURL = 'http://localhost:5000' const tokenRefreshAsync = () => { return new Promise((resolve, reject) => { axios.post('token/refresh', { AccessToken: localStorage.getItem('accessToken'), RefreshToken: localStorage.getItem('refreshToken') }).then(response => { resolve(response.data) }).catch(error => { reject(error) }) }) } axios.interceptors.request.use( config => { const accessToken = localStorage.getItem('accessToken') if (accessToken) { config.headers = { 'Authorization': 'Bearer ' + accessToken } } return config }, error => { return Promise.reject(error) } ) axios.interceptors.response.use( response => { return response }, async error => { if (error && error.response) { switch (error.response.status) { case 401: var tokenExpired = error.response.headers['token-expired'] if (tokenExpired === 'Relative') { let { accessToken, refreshToken } = await tokenRefreshAsync() localStorage.setItem('accessToken', accessToken) localStorage.setItem('refreshToken', refreshToken) return axios.request(error.config) } else if (tokenExpired === 'Absolute') { localStorage.removeItem('accessToken') localStorage.removeItem('refreshToken') window.alert('需要重新登录') } else { localStorage.removeItem('accessToken') localStorage.removeItem('refreshToken') window.alert('需要重新登录') } break } } return Promise.reject(error) } ) const http = { get: (url, params, config) => { return new Promise((resolve, reject) => { axios.get(url, { params: params }, config).then(response => { resolve(response.data) }).catch(error => { reject(error) }) }) }, post: (url, params, config) => { return new Promise((resolve, reject) => { axios.post(url, params, config).then(response => { resolve(response.data) }).catch(error => { reject(error) }) }) } } export default http
<template> <div> <button @click="tokenCreate">token/create</button> <br /> <button @click="users">users</button> </div> </template> <script> export default { data() { return { msg: 'Welcome to Your Vue.js App' } }, methods: { async tokenCreate() { let { accessToken, refreshToken } = await this.$http.post('token/create', { Account: 'Alice', Password: '12345' }) localStorage.setItem('accessToken', accessToken) localStorage.setItem('refreshToken', refreshToken) }, async users() { console.info(await this.$http.get('users')) } } } </script>