.net core jwt 鑑權

毛毛球的书签發表於2024-08-20

思路:在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  }

相關文章