ASP.NET Core 7 의 Identity 및 EF Core를 활용하여 JWT 인증 구현
ASP.NET Core 7 의 Identity 및 EF Core를 활용하여 JWT 인증 구현
해당 내용을 읽기 전에 아래 링크 내용 3가지를 숙지하면 이해하시는데 많은 도움이 될겁니다.
What is JWT?
What is JWT Bearer in ASP.Net Core?
How to Generate a JWT in ASP.NET Core API with Identity and EF Core?Core API?
참고: 위에서 언급한 JWT 문서를 읽고 Identity 및 Entity Framework Core(EF Core)가 포함된 ASP.NET Core에 대한 기본적인 이해가 필요합니다. 또한 Angular에 대한 기본 지식도 있습니다. 이 문서를 완전히 이해하려면 앞서 언급한 모든 기본 이해가 필요합니다.
프로젝트 설정
시작하려면 새 ASP.NET Core 웹 애플리케이션을 만들고 "웹 API" 템플릿을 선택합니다. 그런 다음 다음 패키지를 설치합니다.
- Microsoft.AspNetCore.Identity
- Microsoft.AspNetCore.Authentication.JwtBearer
- Microsoft.EntityFrameworkCore
- Microsoft.EntityFrameworkCore.SqlServer
- Microsoft.AspNetCore.Identity.EntityFrameworkCore
- Microsoft.EntityFrameworkCore.Design
- Microsoft.EntityFrameworkCore.Tools
이 예제에서는 SQL Server를 데이터베이스로 사용하지만 EF Core는 다른 많은 데이터베이스 공급자도 지원합니다.
학생 항목 만들기
다음으로 "Entities"라는 새 폴더를 만들고 Student 엔터티에 대해 다음 클래스를 추가합니다.
public class Student
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
}
그런 다음 "Data"라는 새 폴더를 만들고 IdentityDbContext<IdentityUser>Student 엔터티에 대한 DbSet를 상속하고 또한 포함하는 "ApplicationDbContext"라는 새 클래스를 추가합니다.
public class ApplicationDbContext : IdentityDbContext<IdentityUser>
{
public DbSet<Student> Students { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=JWTExample;Trusted_Connection=True;");
}
}
Configuring JWT Authentication
Startup 클래스에서 ConfigureServices 메서드에 다음 코드를 추가하여 JWT 인증을 구성합니다.
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = "yourdomain.com",
ValidAudience = "yourdomain.com",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("yoursecretkey"))
};
});
"yourdomain.com" 및 "yoursecretkey"를 애플리케이션의 실제 값으로 바꾸는 것을 잊지 마십시오.
StudentController 만들기
"Controllers"라는 새 폴더를 만들고 ControllerBase에서 상속되고 다음 작업이 있는 "StudentController"라는 새 클래스를 추가합니다.
[HttpPost("register")]
public async Task<IActionResult> Register([FromBody] Student student)
{
// create user
var user = new IdentityUser { UserName = student.Email, Email = student.Email };
var result = await _userManager.CreateAsync(user, "P@ssw0rd");
if (!result.Succeeded)
{
return BadRequest();
}
// add student to db
_dbContext.Students.Add(student);
await _dbContext.SaveChangesAsync();
// generate jwt token
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, student.Email),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
var token = new JwtSecurityToken(
issuer: "yourdomain.com",
audience: "yourdomain.com",
claims: claims,
expires: DateTime.UtcNow.AddMinutes(30),
signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes("yoursecretkey")), SecurityAlgorithms.HmacSha256)
);
// return jwt token
return Ok(new
{
token = new JwtSecurityTokenHandler().WriteToken(token),
expiration = token.ValidTo
});
}
이 Action 는 신입생 등록을 처리합니다. 먼저 Identity 프레임워크로 새 사용자를 생성한 다음, 학생을 데이터베이스에 추가하고 마지막으로 JWT 토큰을 생성하여 클라이언트에 반환합니다.
Authorize Attribute 사용하기
특정 Action 에 대한 액세스를 제한하려면 Authorize attribute 을 사용할 수 있습니다. 이 attribute은 컨트롤러 클래스 또는 개별 Action 메서드에 적용할 수 있습니다. 컨트롤러 클래스에 적용될 때 해당 컨트롤러 내의 모든 Action 은 권한 부여 규칙(authorization rules) 에 의해 보호됩니다.
예를 들어 StudentController에 학생 목록을 반환하는 Action 이 있는 경우 Authorize attribute 을 사용하여 해당 Action 에 대한 액세스를 인증된 사용자로만 제한할 수 있습니다.
[Authorize]
[HttpGet("students")]
public IActionResult GetStudents()
{
var students = _dbContext.Students.ToList();
return Ok(students);
}
이 action 은 이제 인증되고 유효한 JWT 토큰이 있는 사용자만 액세스할 수 있습니다. 인증되지 않은 사용자가 이 Action 에 액세스하려고 하면 401 Unauthorized 응답을 받게 됩니다.
역할 또는 정책(roles or policies)에 Authorize attribute 을 사용할 수도 있습니다. 예를 들어 "admin"이라는 role 이 있는 경우 role 이름과 함께 Authorize attribute 을 사용하여 "admin" 의 role 을 가진 사용자만 이 Action 에 액세스하도록 허용할 수 있습니다.
[Authorize(Roles = "admin")]
[HttpGet("students")]
public IActionResult GetStudents()
{
var students = _dbContext.Students.ToList();
return Ok(students);
}
이렇게 하면 "admin" 역할을 가진 사용자만 이 Action 에 액세스할 수 있습니다. 다른 사용자는 401 Unauthorized 응답을 받게 됩니다.
Authorize 속성을 사용하면 Action 에 대한 액세스를 쉽게 제어하고 승인된 사용자만 중요한 데이터에 액세스하거나 특정 Action 을 수행할 수 있도록 할 수 있습니다.
GetStudents 의 action 에 대한 액세스 확인 및 유효성 검사
Authorize 특성으로 보호되는 GetStudents 의 action에 대한 액세스를 확인하고 유효성을 검사하는 방법에는 여러 가지가 있습니다.
User Property 사용하기
Controller 또는 ControllerBase 클래스의 User 속성은 현재 사용자의 claims 및 identity 에 대한 액세스를 제공합니다. 이 속성을 사용하여 사용자가 인증되었는지 확인하고 특정 roles 및 claim 을 확인할 수 있습니다.
예를 들어 사용자 속성의 IsInRole 메서드를 사용하여 학생 목록을 반환하기 전에 사용자에게 특정 역할이 있는지 확인할 수 있습니다.
[Authorize]
[HttpGet("students")]
public IActionResult GetStudents()
{
if (User.IsInRole("admin"))
{
var students = _dbContext.Students.ToList();
return Ok(students);
}
else
{
return Unauthorized();
}
}
이렇게 하면 "admin" 역할을 가진 사용자만 이 action 에 액세스하고 학생 목록을 볼 수 있으며 다른 사용자는 401 Unauthorized 응답을 받게 됩니다.
또한 Claims 속성을 사용하여 특정 클레임을 확인할 수 있습니다.
[Authorize]
[HttpGet("students")]
public IActionResult GetStudents()
{
if (User.Claims.Any(c => c.Type == ClaimTypes.Email && c.Value == "admin@example.com"))
{
var students = _dbContext.Students.ToList();
return Ok(students);
}
else
{
return Unauthorized();
}
}
이렇게 하면 claim 유형이 "email"이고 값이 "admin@example.com"인 사용자만 이 action 에 액세스하고 학생 목록을 볼 수 있으며 다른 사용자는 401 Unauthorized 응답을 받게 됩니다.
IAuthorizationService 활용하기
IAuthorizationService를 사용하여 사용자가 특정 action 또는 resource에 대한 액세스 권한이 있는지 확인할 수도 있습니다. 이 서비스는 컨트롤러 또는 서비스에 injected 될 수 있으며 액세스를 확인하고 인증 실패를 처리하는 방법을 제공합니다.
예를 들어 AuthorizeAsync 메서드를 사용하여 사용자가 GetStudents 의 action 에 액세스할 수 있는지 확인할 수 있습니다.
[Authorize]
[HttpGet("students")]
public async Task<IActionResult> GetStudents()
{
var authorizationResult = await _authorizationService.AuthorizeAsync(User, null, "viewStudentsPolicy");
if (authorizationResult.Succeeded)
{
var students = _dbContext.Students.ToList();
return Ok(students);
}
else
{
return new ChallengeResult();
}
}
이렇게 하면 AuthorizeAsync 메서드는 사용자에게 "viewStudentsPolicy" policy 이 있는지 확인하고, 사용자에게 policy 이 있으면 학생 목록을 반환하고, 그렇지 않으면 401 Unauthorized 응답을 반환합니다.
JWT를 사용하여 GetStudents 의 Action 에 대한 액세스 확인 및 유효성 검사
인증에 JWT(JSON 웹 토큰)를 사용하는 경우 Authorize 특성을 사용하여 GetStudents 작업에 대한 액세스를 유효한 JWT 토큰이 있는 인증된 사용자로만 제한할 수 있습니다. 또한 JWT 토큰에 저장된 클레임을 사용하여 특정 역할 또는 권한을 확인할 수도 있습니다.
Authorize Attribute 사용하기
Authorize attribute 은 컨트롤러 클래스 또는 개별 action method 에 적용할 수 있습니다. 컨트롤러 클래스에 적용될 때 해당 컨트롤러 내의 모든 action 은 권한 부여 규칙에 의해 보호됩니다.
action method 에 적용하면 해당 특정 action 만 보호됩니다. 예를 들어 Authorize attribute 을 사용하여 GetStudents 작업에 대한 액세스를 인증된 사용자로만 제한할 수 있습니다.
[Authorize]
[HttpGet("students")]
public IActionResult GetStudents()
{
var students = _dbContext.Students.ToList();
return Ok(students);
}
이 action 은 이제 인증되고 유효한 JWT 토큰이 있는 사용자만 액세스할 수 있습니다. 인증되지 않은 사용자가 이 action 에 액세스하려고 하면 401 Unauthorized 응답을 받게 됩니다.
JWT Token 의 Claims 사용하기
JWT 토큰에 저장된 claim 을 사용하여 특정 역할 또는 권한( roles or permissions) 을 확인할 수도 있습니다. 예를 들어 role claim 을 확인하여 사용자에게 admin 역할이 있는지 확인할 수 있습니다.
[Authorize]
[HttpGet("students")]
public IActionResult GetStudents()
{
var role = User.Claims.FirstOrDefault(c => c.Type == "role")?.Value;
if (role == "admin")
{
var students = _dbContext.Students.ToList();
return Ok(students);
}
else
{
return Unauthorized();
}
}
이렇게 하면 관리자 role 을 가진 사용자만 이 작업에 액세스하고 학생 목록을 볼 수 있으며 다른 사용자는 401 무단 응답을 받게 됩니다.
JWT token 에 추가한 또 다른 클레임도 확인할 수 있습니다. 예를 들어 다중 권한 중에 "permissions" 이라는 custom claim 을 확인할 수 있습니다.
[Authorize]
[HttpGet("students")]
public IActionResult GetStudents()
{
var permissions = User.Claims.FirstOrDefault(c => c.Type == "permissions")?.Value;
if (permissions.Contains("view_students"))
{
var students = _dbContext.Students.ToList();
return Ok(students);
}
else
{
return Unauthorized();
}
}
이렇게 하면 view_students 권한이 있는 사용자만 이 작업에 액세스하고 학생 목록을 볼 수 있으며 다른 사용자는 401 Unauthorized 응답을 받게 됩니다.
Angular Component 에서 Register Method 사용하기
Angular component 에서 Register 메서드를 사용하기 위해 HttpClient 모듈을 사용하여, 등록하려는 학생의 데이터와 함께 메서드가 노출된 endpoint 에 대한 POST 요청을 만들 수 있습니다. 먼저 구성 요소에서 HttpClient 모듈을 가져와야 합니다.
import { HttpClient } from '@angular/common/http';
그런 다음 component's constructor 에 주입해야 합니다.
constructor(private http: HttpClient) {}
그런 다음 http.post 메서드를 사용하여 등록하려는 학생의 데이터와 함께 Register 메서드가 노출되는 엔드포인트에 대한 POST 요청을 만들 수 있습니다.
register(student: Student) {
this.http.post('/api/student/register', student).subscribe(
data => {
console.log(data);
// you can extract the token and expiration time from the data object
// and store it in the local storage
},
error => {
console.log(error);
}
);
}
위의 코드 조각은 Register 메서드가 /api/student/register 의 endpoint 에 노출되어 있다고 가정한다는 점에 유의해야 합니다. 위의 코드 스니펫은 register 메소드에 전달된 Student 객체가 서버 측의 Student 모델과 동일한 속성을 가지고 있다고 가정한다는 점에 유의하는 것도 중요합니다. 그렇지 않으면 서버가 요청에서 Student 개체를 역직렬화할 수 없습니다.
register 메소드의 응답에서 토큰과 만료시간을 추출하여 로컬 저장소에 저장하기 위해서는 subscribe 메소드에서 데이터 객체에 접근하여 토큰과 만료 속성의 값을 추출할 수 있습니다.
그런 다음 localStorage.setItem 메서드를 사용하여 토큰과 만료 시간을 로컬 저장소에 저장할 수 있습니다.
register(student: Student) {
this.http.post('/api/student/register', student).subscribe(
data => {
console.log(data);
const token = data.token;
const expiration = data.expiration;
localStorage.setItem('token', token);
localStorage.setItem('expiration', expiration);
},
error => {
console.log(error);
}
);
}
또한 JSON.stringify() 메서드를 사용하여 만료 날짜를 문자열로 저장한 다음 나중에 구문 분석할 수 있습니다.
register(student: Student) {
this.http.post('/api/student/register', student).subscribe(
data => {
console.log(data);
const token = data.token;
const expiration = data.expiration;
localStorage.setItem('token', token);
localStorage.setItem('expiration', JSON.stringify(expiration));
},
error => {
console.log(error);
}
);
}
토큰과 만료 시간을 localStorage 에 저장하면 나중에 이를 사용하여 요청을 인증하고 브라우저를 닫은 후에도 사용자가 로그인 상태를 유지할 수 있습니다. 쿠키에 HttpOnly 및 secure flags 를 사용하거나 sessionStorage 또는 in-memory 저장소와 같은 대체 저장소를 사용하는 것과 같은 안전한 방법을 사용하여 토큰을 저장하는 것을 고려해야 합니다.
Angular Component 에서 GetStudents 메서드 사용하기
Angular 구성 요소에서 GetStudents 메서드를 사용하려면 HttpClient 모듈을 사용하여 메서드가 노출되는 endpoint 에 GET 요청을 할 수 있습니다.
먼저 구성 요소에서 HttpClient 모듈을 가져와야 합니다.
import { HttpClient } from '@angular/common/http';
그런 다음 component's constructor 에 주입해야 합니다.
constructor(private http: HttpClient) {}
그런 다음 http.get 메서드를 사용하여 GetStudents 메서드가 노출된 endpoint 에 GET 요청을 할 수 있습니다. 응답 및 발생할 수 있는 오류를 처리하기 위해 get 메서드에서 반환된 observable 을 구독(subscribe) 할 수도 있습니다.
ngOnInit() {
this.http.get<Student[]>('/api/student/students').subscribe(
data => {
this.students = data;
},
error => {
console.log(error);
}
);
}
위의 코드 조각은 GetStudents 메서드가 /api/student/students 끝점에 노출되어 있다고 가정한다는 점에 유의해야 합니다. 또한 요청과 함께 JWT 토큰을 보내고 있는지 확인해야 합니다. 이는 요청 헤더에 토큰을 추가하여 수행할 수 있습니다.
ngOnInit() {
this.http.get<Student[]>('/api/student/students', {
headers: new HttpHeaders({
'Authorization': `Bearer ${localStorage.getItem('token')}`
})
}).subscribe(
data => {
this.students = data;
},
error => {
console.log(error);
}
);
}
이렇게 하면 Authorization 헤더가 로컬 저장소에 저장된 토큰과 함께 요청에 추가됩니다. 이렇게 하면 요청이 승인되고 서버가 학생 목록을 반환합니다. catchError 연산자와 rxjs 라이브러리의 throwError 함수를 사용하여 오류 또는 승인되지 않은 요청을 처리할 수도 있습니다.
import { catchError } from 'rxjs/operators';
import { throwError } from 'rxjs';
ngOnInit() {
this.http.get<Student[]>('/api/student/students', {
headers: new HttpHeaders({
'Authorization': `Bearer ${localStorage.getItem('token')}`
})
}).pipe(
catchError(err => {
if (err.status === 401) {
// handle unauthorized request
} else {
return throwError(err);
}
})
).subscribe(
data => {
this.students = data;
},
error => {
console.log(error);
}
);
}
Angular 구성 요소에서 GetStudents 메서드를 사용하여 학생 목록을 검색하고 보기에 표시할 수 있습니다. 이를 통해 서버의 데이터로 real-time 업데이트 되는, 즉 동적이면서 interactive user interface 를 생성할 수 있습니다.
>> 인증화면에서 router 로 이동하는 방법은 아래 3가지 절차대로 진행하시면 됩니다.
import { Router, RouterModule } from '@angular/router';
>> 이동 router 를 사용하기 위해 생성자에 injected 를 합니다.
export class LoginComponent implements OnInit {
constructor(private router: Router, private http: HttpClient) {}
이제 사용해 보죠. 간단합니다. this.router.navigate() 함수를 통해 화면 이동이 가능합니다.
alert('Login successful');
this.router.navigate(['/']);
이 링크를 클릭하면 이 주제를 보완하기 위해 개인적으로 만든 예제 프로젝트에 액세스하고 다운로드할 수 있습니다.
link.