2024. 6. 6. 17:36ㆍTIL
✔오늘 배운 중요한 🔑 point
- 클라이언트가 서버에 HTTP 요청을 할때 HTTP의 Authorization 헤더에 토큰이 들어있다.
- 클라이언트 요청보냄-> OncePerRequestFilter를 통해 JWT 토큰 추출 -> JwtPlugin의 validateToken으로 유효성 검사-> 유효한 토큰일시 정보 추출하여 SecurityContextHolder에 저장 -> 필터 체인을 통해 다음 단계 진행
- SecurityContextHolder에 인증정보를 저장하는 것은 현재 인증된 사용자의 정보를 관리하고 일관되게 인증 및 권한 부여를 처리하기 위함이다
- JWT (JSON Web Token) 기반의 인증에서는 자격 증명(credentials)이 JWT 자체에 포함되어 있기 때문에, 별도로 자격 증명을 처리할 필요가 없다.
- @SecurityContextHolder에 사용자 권한 정보를 넣어두면 @PreAuthorize 를 이용해서 손쉽게 권한제어를 할 수 있다.
🎯 오늘 배운 내용
클라이언트가 서버에 토큰이 담긴 요청을 보낼때 서버는 HTTP Authorization 헤더에 담긴 토큰을 추출해서 검증을 해야만 한다. 따라서 클라이언트가 요청을 할때마다 한번씩 실행이 되는 OncePerRequestFilter()를 상속받는다
class JwtAuthenticationFilter(
private val jwtPlugin: JwtPlugin
):OncePerRequestFilter(){
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
)
}
자 이제 클라이언트 요청을 가로채서 무엇을 할것인가를 정해야한다.
1. 클라이언트 요청에서 토큰 빼오기
companion object{
private val BEARER_PATTERN = Regex("^Bearer (.+)$")
}
private fun HttpServletRequest.getBearerToken():String?{
val headerValue= this.getHeader(HttpHeaders.AUTHORIZATION)?: return null
return BEARER_PATTERN.find(headerValue)?.groupValues?.get(1)
}
val jwt = request.getBearerToken()
Http 요청의 Authorization 헤더안에 있는 토큰을 빼와서 jwt 변수 안에 저장
현재 jwt에는 토큰 문자열이 들어가 있는 상황
2.토큰을 빼왔으니 그 토큰이 유효한지 검증
jwtPlugin.validateToken(jwt)
3. 유효하다면?, 토큰안에 있는 정보를 SecurityContextHolder안에 저장!!
if(jwt !=null){
jwtPlugin.validateToken(jwt)
.onSuccess {
val userId=it.payload.subject.toLong()
val role= it.payload.get("role",String::class.java)
val email =it.payload.get("email",String::class.java)
val principal = UserPrincipal(
userId,role,setOf(email))
val authentication = JwtAuthenticationToken(
principal=principal,
details = WebAuthenticationDetailsSource().buildDetails(request) // request에서 ip주소와 세션ID를 추출해서 WebAuthenticationDetails 객체를 생성
)
SecurityContextHolder.getContext().authentication = authentication
}
}
3-1 userId,role, email등 토큰에서 정보를 추출한 내용들로 사용자 정보를 저장하는 UserPrincipal에 넣어두자
data class UserPrincipal(
val id:Long,
val email:String,
val authorities:Collection<GrantedAuthority>
){
constructor(id: Long, email: String, roles: Set<String>):this(
id,
email,
roles.map{ SimpleGrantedAuthority("ROLE_$it")}
)
}
val principal = UserPrincipal(
userId,role,setOf(email))
이로써 principal이라는 변수 안에 userId,role,email의 정보가 담긴 사용자 객체를 저장한다.
3-2 사용자 인증정보는 민감한 정보이기 때문에 캡슐화를 해서 저장해야한다
class JwtAuthenticationToken( //사용자 인증정보 캡슐화 하는 클래스 -> 보안,일관성,유연성,테스트 용이성에 이점이 있음
private val principal: UserPrincipal,
details:WebAuthenticationDetails
): AbstractAuthenticationToken(principal.authorities) {
init{
super.setAuthenticated(true) // 이토큰이 이미 인증된 상태
super.setDetails(details) // 요청의 추가정보 설정
}
override fun getCredentials() = null // JWT 에서는 자격증명이!!!! 필요가 없으니 null 반환
override fun getPrincipal() = principal // 현재 인증된 사용자인 principal 객체 반환
override fun isAuthenticated():Boolean{ //jwtPlugin.validateToken(jwt)에서 검증했으니 당연히 return true
return true
}
}
val authentication = JwtAuthenticationToken(
principal=principal,
details = WebAuthenticationDetailsSource().buildDetails(request) // request에서 ip주소와 세션ID를 추출해서 WebAuthenticationDetails 객체를 생성
)
SecurityContextHolder.getContext().authentication = authentication
JwtAuthentication을 통해서 사용자의 정보(principal)와 사용자의 요청이 어디서 왔는지를 추적할 수있는(details) 를 캡슐화 해서 authentication 변수에 저장한다.
SecurityContextHolder.getContext().authentication = authentication
authentication을 현재실행중인 스레드의 보안컨텍스트에 사용자의 인증 정보로 저장한다.
4 현재필터는 다 진행했으니 다음필터로 진행시킨다
filterChain.doFilter(request,response)
이제 클라이언트가 서버로 요청을 보낼때 사용자의 인증정보를 가져올수 있게 되었다!!
그래서 이 인증정보를 가져오는건 OK , 이 인증정보를 어떻게 사용할까?
@PreAuthorize("hasRole('NORMAL') or hasRole('ADMIN')") // 해당 권한을 가진 사람만 이 기능을 사용할수 있음
@PostMapping()
fun createCard(@RequestBody createCardRequest: CreateCardRequest): ResponseEntity<CardResponse>{
return ResponseEntity.status(HttpStatus.CREATED).body(cardService.createCard(createCardRequest))
}
우리는 SecurityContextHolder에서 사용자의 인증정보를 넣음으로써 Spring Security에서 현재 사용자의 정보를 추적할 수 있기 때문에 @PreAuthorize 어노테이션을 이용해서 매우 손쉽게 클라이언트가 요청을 보낼 때 어떤 역할을 가지고 있는지를 확인하고, 그에 따라 요청을 허용하거나 거부할 수 있다!!!
인증 인가 부분 테스트를 위한 swagger 세팅
package org.example.spartatodolist.infra
import io.swagger.v3.oas.models.Components
import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.oas.models.info.Info
import io.swagger.v3.oas.models.security.SecurityRequirement
import io.swagger.v3.oas.models.security.SecurityScheme
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class SwaggerConfig {
@Bean
fun openAPI(): OpenAPI {
return OpenAPI()
.addSecurityItem(
SecurityRequirement().addList("Bearer Authentication")
)
.components(
Components().addSecuritySchemes(
"Bearer Authentication",
SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("Bearer")
.bearerFormat("JWT")
.`in`(SecurityScheme.In.HEADER)
.name("Authorization")
)
)
.info(
Info()
.title("TODO LIST작성을 위한 API")
.description("Course API schema by HJP")
.version("1.0.1")
)
}
}
이로써 Swagger에서 인증,인가에 대한 테스트를 진행할 수 있게 되었다.
🤔 어떻게 활용할까?
이제 인가 부분을 활용하여 사용자의 권한에 따라서 접근할 수 있는 기능들을 제한하고 제어할 수 있게 되었다.
📓 오늘의 한줄
"There is not love of life without despair about life"
- Albert Camus -
'TIL' 카테고리의 다른 글
인증,인가 부분이 추가된 개인 프로젝트(2일차) (0) | 2024.06.08 |
---|---|
인증, 인가 부분이 추가된 개인 프로젝트(1일차) (0) | 2024.06.07 |
JWT토큰을 이용한 로그인 기능 (0) | 2024.06.05 |
중복된 부가기능을 커스텀 어노테이션으로 (AOP) (0) | 2024.06.04 |
팀 프로젝트 회고 (0) | 2024.06.03 |