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>