인가

2024. 6. 6. 17:36TIL

✔오늘 배운 중요한 🔑 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 -