[ASP.NET Core][Entity Framework Core] JWT 2 사용해보기

23627 단어 csharpaspnetcore

소개



이번에는 마지막 프로젝트의 세부 사항을 볼 것입니다.


  • JWT 소개



    JWT(JSON Web Token)는 3개의 JSON 값으로 만들어집니다.

    {Header}.{Payload}.{Signature}
    


    해당 값은 Base64로 인코딩됩니다.

    예를 들어 "헤더"에는 토큰 유형 이름과 알고리즘 이름이 있습니다.
    (암호화 때문에 서명을 직접 복호화할 수 없음)

    UserTokens.cs



    using System.IdentityModel.Tokens.Jwt;
    using System.Security.Claims;
    using System.Text;
    using Microsoft.AspNetCore.Authentication.JwtBearer;
    using Microsoft.IdentityModel.Tokens;
    
    namespace BookshelfSample.Users;
    
    public class UserTokens: IUserTokens
    {
    ...
        public string GenerateToken(ApplicationUser user)
        {
            return new JwtSecurityTokenHandler()
                .WriteToken(new JwtSecurityToken(this.config["Jwt:Issuer"],
                    this.config["Jwt:Audience"],
                    claims: new []
                    {
                        new Claim(ClaimTypes.Email, user.Email)
                    },
                    expires: DateTime.Now.AddSeconds(30),
                    signingCredentials: new SigningCredentials(
                        new SymmetricSecurityKey(Encoding.UTF8.GetBytes(this.config["Jwt:Key"])),
                        SecurityAlgorithms.HmacSha256)));
        }
        public IEnumerable<string> DecodeToken(string value)
        {
            foreach(var t in value.Split("."))
            {
                yield return Base64UrlEncoder.Decode(t);
            }
        }
    }
    

    ApplicationUserService.cs



    ...
        public async Task<UserActionResult> SigninAsync(SigninValue value, ISession session)
        {
    
            var target = await this.users.GetByEmailForSigninAsync(value.Email);
            if(target == null)
            {
                return ActionResultFactory.GetFailed("Invalid e-mail or password");
            }
            var result = await this.signInManager.PasswordSignInAsync(target, value.Password, false, false);
            if(result.Succeeded)
            {
                var token = this.userTokens.GenerateToken(target);
                session.SetString("user-token", token);
    
                logger.LogDebug($"Token: {token}");
                foreach(var t in this.userTokens.DecodeToken(token))
                {
                    logger.LogDebug($"Decoded: {t}");
                }            
                return ActionResultFactory.GetSucceeded();
            }
            return ActionResultFactory.GetFailed("Invalid e-mail or password");
        }
    ...
    

    결과



    Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9lbWFpbGFkZHJlc3MiOiJleGFtcGxlQG1haWwuY29tIiwiZXhwIjoxNjQ4MzEzMDY0LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUxMTAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUxMTAifQ.AtzZ-RnSNG3SRLEsNEC3XI7OjiQ8Y8YWpRdBCTI5vNY
    Decoded: {"alg":"HS256","typ":"JWT"}
    Decoded: {"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress":"[email protected]","exp":1648397836,"iss":"http://localhost:5110","aud":"http://localhost:5110"}
    Decoded: ♀?▼?Q?   ←|?☻?L????YGn?Y~?$?1???
    

    RFC7519에서 HeaderClaims의 사양을 읽을 수 있습니다.

    입증



    JWT 전달자 인증을 위해 AuthenticationBuilder에 옵션과 함께 "AddJwtBearer"를 추가했습니다.

    Program.cs



    ...
        builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidateLifetime = true,
                    ValidateIssuerSigningKey = true,
                    ValidIssuer = builder.Configuration["Jwt:Issuer"],
                    ValidAudience = builder.Configuration["Jwt:Audience"],
                    IssuerSigningKey = new SymmetricSecurityKey(
                        Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])),
                };
            });
    ...
    

    "Validate~"속성을 모두 "false"로 설정하면 항상 동일한 토큰 값을 사용할 수 있나요?
    대답은 '예'입니다.

    Program.cs



    ...
        builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateTokenReplay = false,
                    ValidateActor = false,
                    ValidateIssuer = false,
                    ValidateAudience = false,
                    ValidateLifetime = false,
                    ValidateIssuerSigningKey = false,
                    ValidIssuer = builder.Configuration["Jwt:Issuer"],
                    ValidAudience = builder.Configuration["Jwt:Audience"],
                    IssuerSigningKey = new SymmetricSecurityKey(
                        Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])),
                };
            });
    ...
        app.Use(async (context, next) =>
        {
            var token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9lbWFpbGFkZHJlc3MiOiJleGFtcGxlQG1haWwuY29tIiwiZXhwIjoxNjQ4MzEzMDY0LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUxMTAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUxMTAifQ.AtzZ-RnSNG3SRLEsNEC3XI7OjiQ8Y8YWpRdBCTI5vNY";
            context.Request.Headers.Add("Authorization", $"Bearer {token}");
            await next();
        });
    ...
    

    설정된 토큰은 보안 키로 암호화되므로 "IssuerSigningKey"를 생략할 수 없습니다.

    쓰기 토큰



    다음으로 JWT 생성을 살펴보겠습니다.

    UserTokens.cs



    ...
        public string GenerateToken(ApplicationUser user)
        {
            return new JwtSecurityTokenHandler()
                .WriteToken(new JwtSecurityToken(this.config["Jwt:Issuer"],
                    this.config["Jwt:Audience"],
                    claims: new []
                    {
                        new Claim(ClaimTypes.Email, user.Email)
                    },
                    expires: DateTime.Now.AddSeconds(30),
                    signingCredentials: new SigningCredentials(
                        new SymmetricSecurityKey(Encoding.UTF8.GetBytes(this.config["Jwt:Key"])),
                        SecurityAlgorithms.HmacSha256)));
        }
    ...
    

    JwtSecurityToken에는 5개의 오버로드가 있습니다. 그러나 아무도 JwtHeader, JwtPayload 및 JwtSecurityToken을 인수로 사용하지 않습니다.

    JwtSecurityToken.cs는 SigningCredentials에서 JwtHeader를 만들고 JwtSecurityToken 생성자의 다른 인수에서 JwtPayload를 만듭니다.

    헤더 및 페이로드 섹션은 Base64로 인코딩된 JSON입니다.
    그러나 서명 섹션은 여러 Base64 인코딩 값으로 만들어집니다.
    그래서 직접 해독할 수 없습니다.
  • JwtSecurityToken.cs - GitHub
  • JwtSecurityTokenHandler.cs - GitHub
  • JwtHeader.cs - GitHub
  • JwtPayload.cs - GitHub
  • JwtTokenUtilities.cs - GitHub

  • 보안 키



    보안 키에는 특별한 요구사항이 없습니다.

    하지만 HMAC SHA256을 사용하기 때문에 키 값의 길이가 16비트 이상이어야 합니다.
    키가 16비트 미만이면 런타임 예외가 발생합니다.

    The encryption algorithm 'System.String' requires a key size of at least 'System.Int32' bits. Key 'Microsoft.IdentityModel.Tokens.SymmetricSecurityKey', is of size: 'System.Int32'. (Parameter 'key')
    ...
    


    알파벳과 숫자 외에도 기호, 일본어 등을 사용할 수 있다.
    키가 길든 짧든 결과는 256비트 길이입니다.

    자원


  • RFC 7516 - JSON Web Encryption (JWE)
  • RFC 7519 - JSON Web Token (JWT)
  • RFC7521 - Assertion Framework for OAuth 2.0 Client Authentication and Authorization Grants
  • RFC7523 - JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants
  • OAuth徹底入門 セキュアな認可システムを適用するための原則と実践(OAuth2 in Action)
  • JwtBearerExtensions.AddJwtBearer Method - Microsoft Docs
  • Microsoft.IdentityModel.JsonWebTokens - GitHub
  • Secure Hash Standard - NIST
  • 좋은 웹페이지 즐겨찾기