思路:在startup中的configservice設定jwt鑑權,在config方法中use鑑權,然後新增兩個頁面,
一個登入頁面(登入後生成AccessToken--短時間的token 用於api呼叫 ,RefreshToken--長時間的 token,用於重新整理accesstoken)
一個使用者資訊頁面(登陸後才能訪問,沒有登入則返回失敗或者需要登入)
相關涉及內容:使用swagger方便http請求帶上bearer xxxtokenxxx
考慮場景:當使用者第二次登入/重新登入/許可權變更,需要讓之前生成的token失效
(方案一:新增一個全域性的快取,每次登入後,拿到前一個登入的token,加入黑名單 ,當資料請求時,校驗是否token在黑名單中,如果在,則不讓繼續訪問)
(方案二:登入的時候,使用redis的string型別儲存token,(登入儲存token,重新整理儲存token,退出刪除token)
當使用者的請求進來 拿redis儲存的的使用者的token和 url header中的token 做比較,如果是一致的,則是有效的
)
1:新增配置swagger ,方便測試(這裡給swagger新增了兩個api版本)(將下面兩方法引用到startup.cs的 ConfigureServices/Configure 方法中)
1 public static class SwaggerConfig 2 { 3 public static void ConfigureServices(IServiceCollection services) 4 { 5 //{ 6 //標記介面:[ApiExplorerSettings(GroupName = "v1")] 7 //http://localhost:8001/swagger/index.html 8 services.AddSwaggerGen(x => 9 { 10 11 //var security = new Dictionary<string, IEnumerable<string>> { { "CoreAPI", new string[] { } }, }; 12 //options.AddSecurityRequirement(security); 13 //options.AddSecurityDefinition("CoreAPI", new ApiKeyScheme 14 //{ 15 // Description = "JWT授權(資料將在請求頭中進行傳輸) 在下方輸入Bearer {token} 即可,注意兩者之間有空格", 16 // Name = "Authorization",//jwt預設的引數名稱 17 // In = "header",//jwt預設存放Authorization資訊的位置(請求頭中) 18 // Type = "apiKey" 19 //}); 20 21 x.AddSecurityDefinition("CoreAPI", 22 new OpenApiSecurityScheme 23 { 24 Description = "請輸入token,格式Bearer jwttoken", 25 Name = "Authorization", 26 In = Microsoft.OpenApi.Models.ParameterLocation.Header, 27 Type = Microsoft.OpenApi.Models.SecuritySchemeType.ApiKey, 28 BearerFormat = "JWT", 29 Scheme = JwtBearerDefaults.AuthenticationScheme 30 }); 31 32 33 ////把所有方法配置為增加bearer頭部資訊 34 var securityRequirement = new OpenApiSecurityRequirement 35 { 36 { 37 new OpenApiSecurityScheme 38 { 39 Reference = new OpenApiReference 40 { 41 Type = ReferenceType.SecurityScheme, 42 Id = "CoreAPI" 43 } 44 }, 45 new string[] {} 46 } 47 }; 48 x.AddSecurityRequirement(securityRequirement); 49 50 //新增介面版本 51 x.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo 52 { 53 Version = "v1.0.0", 54 Title = "Api", 55 Description = "XXX Api" 56 }); 57 58 x.SwaggerDoc("v2", new Microsoft.OpenApi.Models.OpenApiInfo 59 { 60 Version = "v1.0.0", 61 Title = "Api2", 62 Description = "XXX Api2" 63 }); 64 65 66 //給swagger新增方法註釋 67 var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; 68 var xmlPath = Path.Combine(Directory.GetCurrentDirectory(), xmlFile); 69 x.IncludeXmlComments(xmlPath); 70 }); 71 } 72 73 74 75 public static void Configure(IApplicationBuilder app, IWebHostEnvironment env) 76 { 77 if (env.IsDevelopment()) 78 { 79 app.UseSwagger(); 80 app.UseDeveloperExceptionPage(); 81 app.UseSwaggerUI(c => 82 { 83 c.SwaggerEndpoint("/swagger/v1/swagger.json", "API1"); 84 c.SwaggerEndpoint("/swagger/v2/swagger.json", "API2"); 85 86 }); 87 88 89 } 90 } 91 }
2:新增jwt相關的鑑權配置 (將JWTAuthConfig 中的兩方法引用到startup.cs的 ConfigureServices/Configure 方法中)
1 public class JWTAuthConfig 2 { 3 public static void ConfigureServices(IServiceCollection services, IConfiguration Configuration) 4 { 5 //使用redis做快取,連線字串 6 var redisConnectionString = Configuration["Redis:Default:Connection"]; 7 var redis = ConnectionMultiplexer.Connect(redisConnectionString); 8 services.AddSingleton<IConnectionMultiplexer>(redis); 9 10 11 //auth 使用jwt作為令牌 12 services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) 13 .AddJwtBearer(options => 14 { 15 options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters 16 { 17 ValidateIssuer = true,//是否校驗發行人 18 ValidateAudience = true,//是否校驗受眾 19 ValidateLifetime = true,//是否校驗失效時間 20 // ClockSkew = TimeSpan.FromSeconds(Convert.ToInt32(Configuration.GetSection("JWT")["ClockSkew"])), 21 ValidateIssuerSigningKey = true,//是否校驗key 22 ValidAudience = Configuration.GetSection("JWT")["ValidAudience"],//可以從配置檔案拿 23 ValidIssuer = Configuration.GetSection("JWT")["ValidIssuer"],//可以從配置檔案拿 24 IssuerSigningKey = new SymmetricSecurityKey 25 (Encoding.UTF8.GetBytes(Configuration.GetSection("JWT")["IssuerSigningKey"])),//可以從配置檔案拿 26 27 }; 28 29 options.Events = new JwtBearerEvents() 30 { 31 32 OnMessageReceived = async context => 33 { 34 //var accessToken = context.HttpContext.Request.Headers["Authorization"].ToString().Replace("Bearer ", ""); 35 //方法一:黑名單方式 36 //當一個使用者二次登入時,需要讓前一次的token失效 37 //邏輯:新增一個全域性的快取,每次登入後,拿到前一個登入的token,加入黑名單 38 //當資料請求時,校驗是否token在黑名單中,如果在,則不讓繼續訪問 39 //var blackListService = context.HttpContext.RequestServices.GetRequiredService<IBlackListService>(); 40 //if (blackListService.IsContains(accessToken)) 41 //{ 42 // context.Fail("Invalid accessToken,請重新登入"); 43 //} 44 45 await Task.CompletedTask; 46 }, 47 OnTokenValidated = async context => 48 { 49 //方法二:登入的時候,使用redis的string型別儲存token,(登入儲存token,重新整理儲存token,退出刪除token) 50 //當使用者的請求進來 拿redis儲存的的使用者的token和 url header中的token 做比較,如果是一致的,則是有效的 51 var accessToken = context.HttpContext.Request.Headers["Authorization"].ToString().Replace("Bearer ", ""); 52 var username = context.Principal?.FindFirst(ClaimTypes.Name)?.Value; 53 54 if (!string.IsNullOrEmpty(username)) 55 { 56 var redis = context.HttpContext.RequestServices.GetRequiredService<IConnectionMultiplexer>(); 57 var db = redis.GetDatabase(); 58 var token = await db.StringGetAsync($"{username}_AccessToken"); 59 60 if (!accessToken.Equals(token)) 61 { 62 context.Fail("Invalid accessToken,請重新登入"); 63 } 64 } 65 await Task.CompletedTask; 66 }, 67 OnChallenge = async context => 68 { 69 //當頁面沒有授權401時,返回json:xxx 70 context.HandleResponse(); 71 context.Response.ContentType = "application/json"; 72 context.Response.StatusCode = StatusCodes.Status401Unauthorized; 73 await context.Response.WriteAsync("{\"message\":\"Unauthorized\",\"success\":false}"); 74 } 75 }; 76 77 }); 78 } 79 80 public static void Configure(IApplicationBuilder app, IWebHostEnvironment env) 81 { 82 83 app.UseAuthentication(); 84 } 85 }
3:appsettings.json相關配置資訊
1 { 2 "Logging": { 3 "LogLevel": { 4 "Default": "Information", 5 "Microsoft": "Warning", 6 "Microsoft.Hosting.Lifetime": "Information" 7 } 8 }, 9 "AllowedHosts": "*", 10 "SystemInfo": "Asp.net Core", 11 "IEmployeeService": "oneStudy.IBLL.EmployeeService", 12 "JWT": { 13 "ClockSkew": 10, 14 "ValidAudience": "http://localhost:8001", 15 "ValidIssuer": "http://localhost:8001", 16 "IssuerSigningKey": "6Zi/5pifUGx1c+mYv+aYn1BsdXPpmL/mmJ9QbHVz6Zi/5pifUGx1c+mYv+aYn1BsdXPpmL/mmJ9QbHVz6Zi/5pifUGx1c+mYv+aYn1BsdXPpmL/mmJ9QbHVz6Zi/5pifUGx1cw==", 17 "Expires": 30 18 }, 19 "Redis": { 20 "Default": { 21 "Connection": "127.0.0.1:6379", 22 "InstanceName": "local", 23 "DefaultDB": 8 24 } 25 } 26 }
4:介面具體呼叫
1 [ApiExplorerSettings(GroupName = "v1")] 2 [Route("api")] 3 [ApiController] 4 public class APIController : ControllerBase 5 { 6 private readonly IBlackListService blackListService; 7 private readonly IConnectionMultiplexer redis; 8 9 private IConfiguration _configuration { get; set; } 10 private IJWTService _jWTService { get; set; } 11 public APIController(IConfiguration configuration, 12 IJWTService jWTService, 13 IBlackListService blackListService, 14 IConnectionMultiplexer redis 15 ) 16 { 17 this._jWTService = jWTService; 18 this.blackListService = blackListService; 19 this.redis = redis; 20 _configuration = configuration; 21 } 22 23 24 25 /// <summary> 26 /// 根據startup程式碼的配置, 27 /// 如果是使用cookie鑑權,就要設定cookie 28 /// 如果是使用jwt令牌鑑權,就要設定jtw token 29 /// </summary> 30 /// <returns></returns> 31 [HttpGet] 32 [Route("info")] 33 [Authorize(Roles = "Admin")] 34 35 public IActionResult GetInfo() 36 { 37 38 var user = base.HttpContext.User; 39 return new JsonResult(new 40 { 41 Name = "caijun", 42 Age = 34 43 }); 44 } 45 46 47 48 49 [Route("Login")] 50 [HttpPost] 51 public IActionResult Login(LoginArg arg) 52 { 53 if (base.HttpContext.Request.Headers.TryGetValue("Authorization", out var authorizationHeader)) 54 { 55 // Bearer token 格式 56 var oldToken = authorizationHeader.ToString().Replace("Bearer ", ""); 57 //拿到前一個失效的accesstoken加入黑名單 58 blackListService.Add(oldToken); 59 } 60 61 62 if ("liping".Equals(arg.Account) && "123456".Equals(arg.Password)) 63 { 64 //生成AccessToken,RefreshToken 65 var claims = new[] 66 { 67 new Claim(ClaimTypes.Name,"liping"), 68 new Claim(ClaimTypes.NameIdentifier,"liping"), 69 new Claim(ClaimTypes.Email,"111@qq.com"), 70 new Claim(ClaimTypes.Gender,"男"), 71 new Claim(ClaimTypes.Role,"Admin"), 72 new Claim(ClaimTypes.MobilePhone,"19011112345"), 73 new Claim("Account","liping"), 74 }; 75 76 var rfClaims = new[] 77 { 78 new Claim("AccountID","10001"), 79 }; 80 81 var AccessToken = _jWTService.CreateToken(claims.ToList(), 1); 82 var RefreshToken = _jWTService.CreateToken(rfClaims.ToList(), 60 * 24); 83 84 85 86 //redis快取token,用於校驗 87 var db = redis.GetDatabase(); 88 db.StringSetAsync($"{arg.Account}_AccessToken", AccessToken, TimeSpan.FromMinutes(double.Parse(_configuration["Jwt:Expires"]))); 89 db.StringSetAsync($"{arg.Account}_AccessToken", RefreshToken, TimeSpan.FromMinutes(double.Parse(_configuration["Jwt:Expires"]))); 90 91 return new JsonResult(new 92 { 93 AccessToken = AccessToken, 94 RefreshToken = RefreshToken 95 96 }); 97 } 98 else 99 { 100 return new JsonResult("登入失敗"); 101 } 102 } 103 104 105 106 /// <summary> 107 /// 拿refreshToken 獲取新的accesscode 108 /// 1:檢查refreshtoken是否過期,如果過期,需要使用者登入 109 /// 2:如果沒有過期重新頒發accesstoken 110 /// </summary> 111 /// <param name="refreshToken"></param> 112 /// <returns></returns> 113 [HttpPost] 114 [Route("RefreshToken")] 115 public async Task<string> RefreshToken(string refreshToken) 116 { 117 await Task.CompletedTask; 118 119 var handler = new JwtSecurityTokenHandler(); 120 var token = handler.ReadToken(refreshToken); 121 122 if (token == null) 123 { 124 return "無效token"; 125 } 126 else 127 { 128 var expiration = token.ValidTo; 129 if (expiration < DateTime.UtcNow) 130 { 131 //過期:生成一個新的token返回 132 var claims = DataList.FirstOrDefault(t => t.RefreshToken == refreshToken)?.Claims; 133 var AccessToken = _jWTService.CreateToken(claims, 30); 134 //redis快取token,用於校驗 135 var db = redis.GetDatabase(); 136 var username = base.HttpContext.User.Claims.FirstOrDefault(t => t.Type.Equals(ClaimTypes.Name))?.Value; 137 await db.StringSetAsync($"{username}_AccessToken", AccessToken, TimeSpan.FromMinutes(double.Parse(_configuration["Jwt:Expires"]))); 138 return AccessToken; 139 } 140 else 141 { 142 //過期,則把快取的資料刪掉,redis刪掉等 143 //DataList.Remove(DataList.FirstOrDefault(t => t.RefreshToken == refreshToken)); 144 return "refreshToken過期,請重新登入"; 145 } 146 } 147 } 148 149 150 } 151 152 153 154 155 156 public interface IBlackListService 157 { 158 void Add(string blacklist); 159 bool IsContains(string blacklist); 160 } 161 162 163 public class BlackListService: IBlackListService 164 { 165 private readonly HashSet<string> _blacklist = new HashSet<string>(); 166 167 public void Add(string blacklist) 168 { 169 170 _blacklist.Add(blacklist); 171 } 172 173 public bool IsContains(string blacklist) 174 { 175 176 return _blacklist.Contains(blacklist); 177 } 178 179 180 }
5:具體的生成jwt token程式碼:
1 public interface IJWTService 2 { 3 string CreateToken(List<Claim> claims, int expirese); 4 } 5 public class JWTService:IJWTService 6 { 7 private IConfiguration _configuration { get; set; } 8 public JWTService(IConfiguration configuration) 9 { 10 _configuration = configuration; 11 } 12 13 /// <summary> 14 /// 建立jwt token 15 /// </summary> 16 /// <param name="claims">token裡面的資訊</param> 17 /// <param name="expirese">過期時間/mins</param> 18 /// <returns></returns> 19 public string CreateToken(List<Claim> claims, int expirese) 20 { 21 //var claims = new[] 22 // { 23 // new Claim(ClaimTypes.Name,"liping"), 24 // new Claim(ClaimTypes.Email,"111@qq.com"), 25 // new Claim(ClaimTypes.Gender,"男"), 26 // new Claim(ClaimTypes.Role,"Admin"), 27 // new Claim(ClaimTypes.MobilePhone,"19011112345"), 28 // new Claim("Account","liping"), 29 30 // }; 31 32 33 //var configKey2 = _configuration["JWT:IssuerSigningKey"]; 34 var configKey = _configuration.GetSection("JWT")["IssuerSigningKey"]; 35 var configValidIssuer = _configuration.GetSection("JWT")["ValidIssuer"]; 36 var configValidAudience = _configuration.GetSection("JWT")["ValidAudience"]; 37 38 39 //var confiExpires = _configuration.GetSection("JWT")["Expires"]; 40 41 var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configKey)); 42 43 var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); 44 45 46 var jwtSecurity = new JwtSecurityToken( 47 issuer: configValidIssuer, 48 audience: configValidAudience, 49 claims: claims, 50 notBefore: DateTime.Now,//立即生效 51 expires: DateTime.Now.AddMinutes(Convert.ToInt32(expirese)), 52 signingCredentials: creds 53 ); 54 var newToken = new JwtSecurityTokenHandler().WriteToken(jwtSecurity); 55 56 return newToken; 57 } 58 }