ASP.NET Core 2.2:. Twenty-six application JWT user authentication and Token refresh

Source: https://www.cnblogs.com/FlyLolo/p/ASPNETCore2_26.html

 

This article will demonstrate through a practical example of how user authentication and Token refresh program (in ASP.NET Core application of the JWT ASP.NET Core Series Catalog )

First, what is JWT?

JWT (json web token) based on open standards (RFC 7519), is a stateless distributed authentication method is mainly used to safely deliver a statement during network environment. It is based on JSON, so it can be used like json as .Net, JAVA, JavaScript ,, PHP and other languages.
Why use JWT?
Traditional Web applications generally use Cookies + Session for authentication. But for a growing number of App, applets and other applications, the server usually RestFul type of stateless their corresponding API, then use this authentication method is not very convenient. The JWT that stateless distributed authentication method is exactly in line with this demand.

Two, JWT consists of:

JWT is what look like? It is a string such as the following:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjAwMiIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiOiLmnY7lm5siLCJuYmYiOjE1NjU5MjMxMjIsImV4cCI6MTU2NTkyMzI0MiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1NDIxNCIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTQyMTUifQ.Mrta7nftmfXeo_igBVd4rl2keMmm0rg0WkqRXoVAeik
"." It is composed of three segments "distortion" connected together to form two strings. Official website https://jwt.io/ provides its authentication

 

 


Its three character strings corresponding to the Header, Payload and Signature three parts on the right side of FIG.

Header:

Header:
{
"alg": "HS256", 
"typ": "JWT"
}

Encryption identified as HS256, Token type JWT, the first string of this embodiment is formed by JSON encoding Base64Url

Payload

Payload is JWT for information storage section, which contains many statements (claims).
You can customize multiple declarations added to the Payload, the system also provides some default type
iss (issuer): Issuer
exp (expiration time): expiration time
sub (subject): Theme
aud (audience): Audience
nbf (Not Before ): Effective time
iat (issued At): The issue of time
jti (JWT ID): No.

This part of the second string generated by the encoding Base64Url.

Signature

Signature verification is for the Token. Its value expressions like this: Signature = HMACSHA256 (base64UrlEncode (header) + + base64UrlEncode (payload), secret "."), That is, it is generated by a new character string is encrypted before and after two string.

Therefore, only people who have the same encryption key in order to obtain the same string through the first two strings, this way to ensure the authenticity of the Token.

Third, the certification process

Probably the process is this:

  1. Authentication Server: login for user authentication and Token issuance.
  2. Application server: business data interface. Protected API.
  3. Client: generally APP, small procedures.

Certification process:

  1.  The user first by logging, authentication server to get a Token.
  2. When the application access to the API server, the acquired Token is placed in the Header of the request.
  3. Application server verifies the Token, the result returned by the corresponding post.

Description: This is just an example of the program, the actual project may vary.

  1. For small projects, it may be certified services and application services together. This example is achieved by separate ways, so that we can better understand the certification process between the two.
  2. For more complex projects, there may be multiple application services, users get to the Token can be authenticated at multiple distributed services, and this is one of the advantages of JWT.

 

Article on JWT's many here do not introduce too much. By following the example of practical look at how it is applied in the ASP.NET Core of.

Fourth, application examples

FIG on one: "the JWT certification process" involved in three parts client, an authentication server, an application server, by way of example to the following simulation of these three parts:

  1. Authentication Server: Create a WebApi solution, called FlyLolo.JWT.Server.
  2. Application server: Create a WebApi solution, called FlyLolo.JWT.API.
  3. Client: Fiddler sent here with a request to do the test.

Certification Services

First create a new ASP.NET Core solution WebApi solutions 

Name it FlyLolo.JWT.Server.

First create a new login and Token TokenController for issuance:

Copy the code
[Route("api/[controller]")]
public class TokenController : Controller
{
    private ITokenHelper tokenHelper = null;
    public TokenController(ITokenHelper _tokenHelper)
    {
        tokenHelper = _tokenHelper;
    }
    [HttpGet]
    public IActionResult Get(string code, string pwd)
    {
        User user = TemporaryData.GetUser(code);
        if (null != user && user.Password.Equals(pwd))
        {
            return Ok(tokenHelper.CreateToken(user));
        }
        return BadRequest();
    }
}
Copy the code

 It has a name Get Action for receiving the username and password submitted for authentication and, after the verification, the method calling TokenHelper the CreateToken generating Token returned.

Here related to the User and TokenHelper two classes.

User Related:

Copy the code
public class User
{
    public string Code { get; set; }
    public string Name { get; set; }
    public string Password { get; set; }
}
Copy the code

Since only Demo, User classes only contains three or more fields. User data is done in the analog class TemporaryData

Copy the code
    /// <Summary> 
    /// dummy data, the user is read from a database or an analog cache 
    /// </ Summary> 
    public static class TemporaryData 
    { 
        Private static List <the User> the Users List new new = <the User> () {new new User {Code = "001", Name = " Joe Smith", Password = "111111"} , new User {Code = "002", Name = " John Doe", Password = "222222"}}; 

        public static the GetUser the User (String code) 
        { 
            return Users.FirstOrDefault (m => m.Code.Equals (code)); 
        } 
    }
Copy the code

This is only analog data, the actual project should be read from the database, or caching.

TokenHelper:

Copy the code
public class TokenHelper : ITokenHelper
    {
        private IOptions<JWTConfig> _options;
        public TokenHelper(IOptions<JWTConfig> options)
        {
            _options = options;
        }

        public Token CreateToken(User user)
        {
            Claim[] claims = { new Claim(ClaimTypes.NameIdentifier,user.Code),new Claim(ClaimTypes.Name,user.Name) };

            return CreateToken(claims);
        }
        private Token CreateToken(Claim[] claims)
        {
            var now = DateTime.Now;var expires = now.Add(TimeSpan.FromMinutes(_options.Value.AccessTokenExpiresMinutes));
            var token = new JwtSecurityToken(
                issuer: _options.Value.Issuer,
                audience: _options.Value.Audience,
                claims: claims,
                notBefore: now,
                expires: expires,
                signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.Value.IssuerSigningKey)), SecurityAlgorithms.HmacSha256));
            return new Token { TokenContent = new JwtSecurityTokenHandler().WriteToken(token), Expires = expires };
        }
    }
Copy the code

    Token created by CreateToken method, here are a few key parameters:

  1. issuer Token publisher
  2. Audience Token recipients
  3. expiration time expires
  4. IssuerSigningKey signature key

Token corresponding code is as follows:

Copy the code
    public class Token
    {
        public string TokenContent { get; set; }

        public DateTime Expires { get; set; }
    }
Copy the code

This generates a Token returned to the client by the TokenHelper CreateToken method. To now, seemingly all the work has been completed. Not the case, we still need to do some settings in the Startup file.

Copy the code
public class Startup
{
// 。。。。。。此处省略部分代码
public void ConfigureServices(IServiceCollection services) {
//读取配置信息 services.AddSingleton<ITokenHelper, TokenHelper>(); services.Configure<JWTConfig>(Configuration.GetSection("JWT")); //启用JWT services.AddAuthentication(Options => { Options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; Options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }). AddJwtBearer(); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); } void the Configure public (App IApplicationBuilder, IHostingEnvironment the env) { IF (env.IsDevelopment ()) { app.UseDeveloperExceptionPage (); }
// enable authentication intermediate app.UseAuthentication (); app.UseMvc (); } }
Copy the code

 This uses the configuration information in the authentication information do appsettings.json configuration is as follows:

Copy the code
  "JWT": {
    "Issuer": "FlyLolo",
    "Audience": "TestAudience",
    "IssuerSigningKey": "FlyLolo1234567890",
    "AccessTokenExpiresMinutes": "30"
  }
Copy the code

 

Run the project, and access to Get Fidder by way api / token code = 002 & pwd = 222222, returned the following results?:

{"tokenContent":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8
yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjAwMiIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL
3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiOiLmnY7lm5siLCJuYmYiOjE1NjY3OTg0NzUsImV4cCI6MTU2NjgwMDI
3NSwiaXNzIjoiRmx5TG9sbyIsImF1ZCI6IlRlc3RBdWRpZW5jZSJ9.BVf3gOuW1E9RToqKy8XXp8uIvZKL-lBA-q9fB9QTEZ4",
"expires":"2019-08-26T21:17:55.1183172+08:00"}

 

 客户端登录成功并成功返回了一个Token,认证服务创建完成

应用服务

新建一个WebApi的解决方案,名为FlyLolo.JWT.API。

添加BookController用作业务API。

Copy the code
[Route("api/[controller]")]
[Authorize]
public class BookController : Controller
{
    // GET: api/<controller>
    [HttpGet]
    [AllowAnonymous]
    public IEnumerable<string> Get()
    {
        return new string[] { "ASP", "C#" };
    }

    // POST api/<controller>
    [HttpPost]
    public JsonResult Post()
    {
        return new JsonResult("Create  Book ...");
    }
}
Copy the code

 对此Controller添加了[Authorize]标识,表示此Controller的Action被访问时需要进行认证,而它的名为Get的Action被标识了[AllowAnonymous],表示此Action的访问可以跳过认证。

在Startup文件中配置认证:

Copy the code
public class Startup
{
// 省略部分代码
    public void ConfigureServices(IServiceCollection services)
    {
        #region 读取配置
        JWTConfig config = new JWTConfig();
        Configuration.GetSection("JWT").Bind(config);
        #endregion

        #region 启用JWT认证
        services.AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        }).
        AddJwtBearer(options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidIssuer = config.Issuer,
                ValidAudience = config.Audience,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config.IssuerSigningKey)),
                ClockSkew = TimeSpan.FromMinutes(1)
            };
        });
        #endregion

        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        app.UseAuthentication();
        app.UseMvc();
    }
}
Copy the code

 这里同样用到了配置:

Copy the code
    public class JWTConfig
    {
        public string Issuer { get; set; }
        public string Audience { get; set; }
        public string IssuerSigningKey { get; set; }
        public int AccessTokenExpiresMinutes { get; set; }
    }
Copy the code

 appsettings.json:

Copy the code
  "JWT": {
    "Issuer": "FlyLolo",
    "Audience": "TestAudience",
    "IssuerSigningKey": "FlyLolo1234567890",
    "AccessTokenExpiresMinutes": "30"
  }
Copy the code

 关于JWT认证,这里通过options.TokenValidationParameters对认证信息做了设置,ValidIssuer、ValidAudience、IssuerSigningKey这三个参数用于验证Token生成的时候填写的Issuer、Audience、IssuerSigningKey,所以值要和生成Token时的设置一致。

ClockSkew默认值为5分钟,它是一个缓冲期,例如Token设置有效期为30分钟,到了30分钟的时候是不会过期的,会有这么个缓冲时间,也就是35分钟才会过期。为了方便测试(不想等太长时间),这里我设置了1分钟。

TokenValidationParameters还有一些其他参数,在它的构造方法中已经做了默认设置,代码如下:

Copy the code
public TokenValidationParameters()
{
    RequireExpirationTime = true;  
    RequireSignedTokens = true;    
    SaveSigninToken = false;
    ValidateActor = false;
    ValidateAudience = true;  //是否验证接受者
    ValidateIssuer = true;   //是否验证发布者
    ValidateIssuerSigningKey = false;  //是否验证秘钥
    ValidateLifetime = true; //是否验证过期时间
    ValidateTokenReplay = false;
 }
Copy the code

 访问api/book,正常返回了结果

["ASP","C#"]

 通过POST方式访问,返回401错误。

这就需要使用获取到的Toke了,如下图方式再次访问

添加了“Authorization: bearer Token内容”这样的Header,可以正常访问了。

至此,简单的JWT认证示例就完成了,代码地址https://github.com/FlyLolo/JWT.Demo/releases/tag/1.0

这里可能会有个疑问,例如:

   1.Token被盗了怎么办?

    答: 在启用Https的情况下,Token被放在Header中还是比较安全的。另外Token的有效期不要设置过长。例如可以设置为1小时(微信公众号的网页开发的Token有效期为2小时)。

   2. Token到期了如何处理?

   答:理论上Token过期应该是跳到登录界面,但这样太不友好了。可以在后台根据Token的过期时间定期去请求新的Token。下一节来演示一下Token的刷新方案。

五、Token的刷新

   为了使客户端能够获取到新的Token,对上文的例子进行改造,大概思路如下:

  1. 用户登录成功的时候,一次性给他两个Token,分别为AccessToken和RefreshToken,AccessToken用于正常请求,也就是上例中原有的Token,RefreshToken作为刷新AccessToken的凭证。
  2. AccessToken的有效期较短,例如一小时,短一点安全一些。RefreshToken有效期可以设置长一些,例如一天、一周等。
  3. 当AccessToken即将过期的时候,例如提前5分钟,客户端利用RefreshToken请求指定的API获取新的AccessToken并更新本地存储中的AccessToken。

所以只需要修改FlyLolo.JWT.Server即可。

首先修改Token的返回方案,新增一个Model

    public class ComplexToken
    {
        public Token AccessToken { get; set; }
        public Token RefreshToken { get; set; }
    }

包含AccessToken和RefreshToken,用于用户登录成功后的Token结果返回。

修改 appsettings.json,添加两个配置项:

    "RefreshTokenAudience": "RefreshTokenAudience", 
    "RefreshTokenExpiresMinutes": "10080" //60*24*7

 

RefreshTokenExpiresMinutes用于设置RefreshToken的过期时间,这里设置了7天。RefreshTokenAudience用于设置RefreshToken的接受者,与原Audience值不一致,作用是使RefreshToken不能用于访问应用服务的业务API,而AccessToken不能用于刷新Token。

修改TokenHelper:

Copy the code
    public enum TokenType
    {
        AccessToken = 1,
        RefreshToken = 2
    }
    public class TokenHelper : ITokenHelper
    {
        private IOptions<JWTConfig> _options;
        public TokenHelper(IOptions<JWTConfig> options)
        {
            _options = options;
        }

        public Token CreateAccessToken(User user)
        {
            Claim[] claims = new Claim[] { new Claim(ClaimTypes.NameIdentifier, user.Code), new Claim(ClaimTypes.Name, user.Name) };

            return CreateToken(claims, TokenType.AccessToken);
        }

        public ComplexToken CreateToken(User user)
        {
            Claim[] claims = new Claim[] { new Claim(ClaimTypes.NameIdentifier, user.Code), new Claim(ClaimTypes.Name, user.Name)
                //下面两个Claim用于测试在Token中存储用户的角色信息,对应测试在FlyLolo.JWT.API的两个测试Controller的Put方法,若用不到可删除
                , new Claim(ClaimTypes.Role, "TestPutBookRole"), new Claim(ClaimTypes.Role, "TestPutStudentRole")
            };

            return CreateToken(claims);
        }

        public ComplexToken CreateToken(Claim[] claims)
        {
            return new ComplexToken { AccessToken = CreateToken(claims, TokenType.AccessToken), RefreshToken = CreateToken(claims, TokenType.RefreshToken) };
        }

        /// <summary>
        /// 用于创建AccessToken和RefreshToken。
        /// 这里AccessToken和RefreshToken只是过期时间不同,【实际项目】中二者的claims内容可能会不同。
        /// 因为RefreshToken只是用于刷新AccessToken,其内容可以简单一些。
        /// 而AccessToken可能会附加一些其他的Claim。
        /// </summary>
        /// <param name="claims"></param>
        /// <param name="tokenType"></param>
        /// <returns></returns>
        private Token CreateToken(Claim[] claims, TokenType tokenType)
        {
            var now = DateTime.Now;
            var expires = now.Add(TimeSpan.FromMinutes(tokenType.Equals(TokenType.AccessToken) ? _options.Value.AccessTokenExpiresMinutes : _options.Value.RefreshTokenExpiresMinutes));//设置不同的过期时间
            var token = new JwtSecurityToken(
                issuer: _options.Value.Issuer,
                audience: tokenType.Equals(TokenType.AccessToken) ? _options.Value.Audience : _options.Value.RefreshTokenAudience,//设置不同的接受者
                claims: claims,
                notBefore: now,
                expires: expires,
                signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.Value.IssuerSigningKey)), SecurityAlgorithms.HmacSha256));
            return new Token { TokenContent = new JwtSecurityTokenHandler().WriteToken(token), Expires = expires };
        }

        public Token RefreshToken(ClaimsPrincipal claimsPrincipal)
        {
            var code = claimsPrincipal.Claims.FirstOrDefault(m => m.Type.Equals(ClaimTypes.NameIdentifier));
            if (null != code )
            {
                return CreateAccessToken(TemporaryData.GetUser(code.Value.ToString()));
            }
            else
            {
                return null;
            }
        }
    }
Copy the code

 

在登录后,生成两个Token返回给客户端。在TokenHelper添加了一个RefreshToken方法,用于生成新的AccessToken。对应在TokenController中添加一个名为Post的Action,用于调用这个RefreshToken方法刷新Token

Copy the code
[HttpPost]
[Authorize]
public IActionResult Post()
{
    return Ok(tokenHelper.RefreshToken(Request.HttpContext.User));
}
Copy the code

这个方法添加了[Authorize]标识,说明调用它需要RefreshToken认证通过。既然启用了认证,那么在Startup文件中需要像上例的业务API一样做JWT的认证配置。

Copy the code
        public void ConfigureServices(IServiceCollection services)
        {
            #region 读取配置信息
            services.AddSingleton<ITokenHelper, TokenHelper>();
            services.Configure<JWTConfig>(Configuration.GetSection("JWT"));
            JWTConfig config = new JWTConfig();
            Configuration.GetSection("JWT").Bind(config);
            #endregion

            #region 启用JWT
            services.AddAuthentication(Options =>
            {
                Options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                Options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            }).
             AddJwtBearer(options =>
             {
                 options.TokenValidationParameters = new TokenValidationParameters
                 {
                     ValidIssuer = config.Issuer,
                     ValidAudience = config.RefreshTokenAudience,
                     IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config.IssuerSigningKey))
                 };
             });
            #endregion

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        }
Copy the code

 

 注意这里的ValidAudience被赋值为config.RefreshTokenAudience,和FlyLolo.JWT.API中的不一致,用于防止AccessToken和RefreshToken的混用。

再次访问/api/token?code=002&pwd=222222,会返回两个Token:

Copy the code
{"accessToken":{"tokenContent":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8y
MDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjAwMiIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUva
WRlbnRpdHkvY2xhaW1zL25hbWUiOiLmnY7lm5siLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW
1zL3JvbGUiOlsiVGVzdFB1dEJvb2tSb2xlIiwiVGVzdFB1dFN0dWRlbnRSb2xlIl0sIm5iZiI6MTU2NjgwNjQ3OSwiZXhwIjoxNTY2ODA4Mjc5LCJ
pc3MiOiJGbHlMb2xvIiwiYXVkIjoiVGVzdEF1ZGllbmNlIn0.wlMorS1V0xP0Fb2MDX7jI7zsgZbb2Do3u78BAkIIwGg",
"expires":"2019-08-26T22:31:19.5312172+08:00"},

"refreshToken":{"tokenContent":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8y
MDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjAwMiIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUva
WRlbnRpdHkvY2xhaW1zL25hbWUiOiLmnY7lm5siLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW
1zL3JvbGUiOlsiVGVzdFB1dEJvb2tSb2xlIiwiVGVzdFB1dFN0dWRlbnRSb2xlIl0sIm5iZiI6MTU2NjgwNjQ3OSwiZXhwIjoxNTY3NDExMjc5LCJ
pc3MiOiJGbHlMb2xvIiwiYXVkIjoiUmVmcmVzaFRva2VuQXVkaWVuY2UifQ.3EDi6cQBqa39-ywq2EjFGiM8W2KY5l9QAOWaIDi8FnI",
"expires":"2019-09-02T22:01:19.6143038+08:00"}}
Copy the code

 

 可以使用RefreshToken去请求新的AccessToken

 

测试用AccessToken可以正常访问FlyLolo.JWT.API,用RefreshToken则不可以。

至此,Token的刷新功能改造完成。代码地址:https://github.com/FlyLolo/JWT.Demo/releases/tag/1.1

疑问:RefreshToken有效期那么长,被盗了怎么办,和直接将AccessToken的有效期延长有什么区别?

Think:. 1 RefreshToken not AccessToken as are used in the majority of requests. 2. Apply more like API, the corresponding service (is) likely to be more, so the larger the probability of leakage.

Guess you like

Origin www.cnblogs.com/frank0812/p/11854572.html