ASP.NET Core+React+Typerscript를 사용하여 인증된 웹 애플리케이션의 초기 형태 만들기 ⑤ jwt 토큰 기능 추가

개요


④까지 로그인 기능을 갖추었지만 이렇게 되면 로그인 상태에 따라 조작을 제어할 수 없다.
서버에서 영패를 생성하고 클라이언트에게 되돌려주는 기능, 클라이언트에서 영패를 얻고 먼저 표시되는 기능을 추가합니다.
(최종적으로 획득한 영패를 서버 조회에 첨부하여 발송하고 서버 측에서 인증을 필요로 하는 처리는 적당한 영패가 도착할 때만 허용되며 인증 기능의 일부분은 완성됩니다. 그 전에 뒷글에서 소개합니다)
토큰을 추가합니다.
제작에 들어가면 복잡해지기 때문에 먼저 서버 측에 토큰을 생성하고 로그인에 성공하면 되돌아오는 기능을 클라이언트 측에 토큰을 획득해 표시하는 기능을 설치했다.

코드 추가


JwtService.cs


Jwt 토큰을 생성하는 새로운 기능 추가
JwtService.cs
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using server_app.Models;

namespace server_app.Services
{
    public class TokenService
    {
        private readonly IConfiguration _config;
        public TokenService(IConfiguration config)
        {
            _config = config;
        }

        public string CreateToken(ApplicationUser user)
        {
            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.Name, user.UserName),
                new Claim(ClaimTypes.NameIdentifier, user.Id),
                new Claim(ClaimTypes.Email, user.Email),
            };

            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["TokenKey"]));
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature);

            var tokenDescriptor = new SecurityTokenDescriptor
            {
                Subject = new ClaimsIdentity(claims),
                Expires = DateTime.Now.AddDays(7),
                SigningCredentials = creds
            };

            var tokenHandler = new JwtSecurityTokenHandler();

            var token = tokenHandler.CreateToken(tokenDescriptor);

            return tokenHandler.WriteToken(token);            

        }
    }
}

AccountController.cs


로그인 성공 시 토큰을 생성하는 기능 추가
AccountController.cs
using System.Security.Claims;
using System.Threading.Tasks;
using server_app.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using server_app.Services;

namespace server_app.Controllers
{
    [AllowAnonymous]
    [ApiController]
    [Route("[controller]")]
    public class AccountController : ControllerBase
    {
        private readonly UserManager<ApplicationUser> _userManager;
        private readonly  SignInManager<ApplicationUser> _signinManager;
+        private readonly TokenService _tokenService;
        public AccountController(UserManager<ApplicationUser> userManager
            ,SignInManager<ApplicationUser> signinManager
+            ,TokenService tokenService
             )
        {
+            _tokenService = tokenService;
            _signinManager = signinManager;
            _userManager = userManager;
        }

        [HttpPost("login")]
        public async Task<ActionResult<UserModel>> Login(LoginModel loginModel)
        {
            
            var user = await _userManager.FindByEmailAsync(loginModel.Email);

            if(user == null) return Unauthorized();

            var result = await _signinManager.CheckPasswordSignInAsync(user, loginModel.Password, false);

            if(result.Succeeded)
            {
                return CreateUserObject(user);
            }

            return Unauthorized();
        }

        [HttpPost("register")]
        public async Task<ActionResult<UserModel>> Register(RegisterModel registerModel)
        {
            if(await _userManager.Users.AnyAsync(x => x.Email == registerModel.Email))
            {
                ModelState.AddModelError("email", "Email taken");
                return ValidationProblem();
            }
            if(await _userManager.Users.AnyAsync(x => x.UserName == registerModel.Username))
            {
                ModelState.AddModelError("username", "Username taken");
                return ValidationProblem();
            }

            var user = new ApplicationUser
            {
                Email = registerModel.Email,
                UserName = registerModel.Username
            };

            var result = await _userManager.CreateAsync(user, registerModel.Password);

            if(result.Succeeded)
            {
                return CreateUserObject(user);
            }

            return BadRequest("Problem regist User");
        }


        private UserModel CreateUserObject(ApplicationUser user)
        {
            return new UserModel
            {
+                    Token = _tokenService.CreateToken(user),
                    Username = user.UserName
            };
        }
    }
}

UserModel.cs


UserModel.cs
namespace server_app.Models
{
    public class UserModel
    {
+        public string Token { get; set; }
        public string Username {get; set; }
        
    }
}

Startup.cs


토큰 생성 기능 추가 및 Cros 관련 기능 변경
Startup.cs
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Text;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
+using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using server_app.Data;
using server_app.Models;
+using server_app.Services;

namespace server_app
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

-        readonly string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(opt =>
            {
                opt.UseSqlite(Configuration.GetConnectionString("DefaultConnection"));
            } );


            services.AddControllers();
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "server_app", Version = "v1" });
            });

             services.AddDefaultIdentity<ApplicationUser>(
                 options => {
                                options.SignIn.RequireConfirmedAccount = false;
                             }
                 )
                 .AddEntityFrameworkStores<ApplicationDbContext>();
		 

            //for jwt token
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["TokenKey"]));

            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(opt =>
                {
                    opt.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuerSigningKey = true,
                        IssuerSigningKey = key,
                        ValidateIssuer = false,
                        ValidateAudience = false
                    };
                });
            services.AddScoped<TokenService>();
            //--------------------------------------------------------------------------------------

-            services.AddCors(o => o.AddPolicy(MyAllowSpecificOrigins, builder =>
-            {
-                builder.AllowAnyOrigin()    // Allow CORS Recest from all Origin
-                       .AllowAnyMethod()    // Allow All Http method
-                       .AllowAnyHeader();   // Allow All request header
-            }));

+            services.AddCors();

        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseSwagger();
                app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "server_app v1"));
            }

            app.UseHttpsRedirection();

            app.UseRouting();            
	    
-            app.UseCors(MyAllowSpecificOrigins);   // Add For CORS

+            app.UseCors(builder => builder
+                .WithOrigins(new []{"http://localhost:3000","https://localhost:3000"})
+                .AllowAnyHeader()
+                .AllowAnyMethod()
+                .AllowCredentials()
+            );
+            
+            app.UseAuthentication();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

appsettings.json


영패 생성 키 추가
appsettings.json
{
  "ConnectionStrings": {
    "DefaultConnection":"Data Source=database.db"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
+  "TokenKey":"key for jwt token"
}

Login.tsx


로그인에 성공했을 때 돌아오는 영패로 변경
Login.tsx
import React, { SyntheticEvent, useState } from 'react';

const Login = (
    ) => 
{
    
    const [name, setName] = useState('');
    const [email, setEmail] = useState('');
    const [password, setPassword] = useState('');
    const [resultcode, setResultcode] = useState(0);
    const [resultTitle, setResultTitle] = useState('');
+    const [token, setToken] = useState('');



    const submit = async (e: SyntheticEvent) => {
        e.preventDefault();
        const response = await fetch('https://localhost:5001/account/login',
        {
            method : 'POST',
            headers:{'Content-Type' : 'application/json'},
+            credentials: 'include',
            body: JSON.stringify({
                email,
                password
            })
        });
        const content = await response.json();
        const status = await response.status

        setResultcode(status);
        setResultTitle(content.title);
        if(status==200){
            setName(content.username);
+            setToken(content.token);

        }

    }

    
    return (
        <>
        <form onSubmit={submit}>
            <h2>Sign in</h2>

            <ul>

                <li>
                    <label>email</label>
                    <input type="email" placeholder="[email protected]" required 
                        onChange = {e => setEmail(e.target.value)}            
                    />
                </li>

                <li>                    
                    <label>password</label>
                    <input type="password" placeholder="Password" required 
                        onChange = {e => setPassword(e.target.value)}            
                    />
                </li>
            </ul>

            <button type="submit">Sign in</button>

        </form>
        
        <h2>Response</h2>

        <ul>
            <li>
                {resultcode!=0 && <>{resultcode==200 ? <>Login Success</> : <>Login Fail</>}</>}
            </li>

            <li>
                {resultcode==200 && <>Name:{name}</>}
            </li>

            <li>
                {resultcode!=0 && <>Code:{resultcode}</>}
            </li>

            <li>
                {resultcode!=0 && <>msg:{resultTitle}</>}
            </li>
+
+            <li>
+                {resultcode!=0 && <p>token : {token}</p>}
+            </li>
        </ul>
        </>
    );

}

export default Login;

실행 결과


실행 후 아래 수령한 영패를 표시합니다

좋은 웹페이지 즐겨찾기