티스토리 뷰

 

이어서 계속 구현해보겠습니다.

 

구현 자체가 목적이므로 자세한 설명을 생략합니다.

 

  1. JWT 생성하기
  2. JWT에 권한 추가해주기 
  3. 생성한 JWT에 대해 인증/인가 하기 (진행)
  4. JWT 재발급 해주기

구현

 

JwtFilter

class JwtFilter(
    private val jwtProvider: JwtProvider
) : OncePerRequestFilter() {
    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        val jwt = jwtProvider.resolveToken(request)

        if (StringUtils.hasText(jwt) && jwtProvider.validateAccessToken(jwt)) {
            val authentication = jwtProvider.findAuthentication(jwt)
            SecurityContextHolder.getContext().authentication = authentication
        }

        filterChain.doFilter(request, response)
    }
}

 

JWT 검증은 Filter를 통해서 진행됩니다.

 

resolveToken은 header에서 bearer를 제외한 순수 토큰만을 가져옵니다.

JwtProvider

@Component
class JwtProvider(
    private val memberRepository: MemberRepository
) {

    companion object {
        private const val AUTHORITIES_KEY = "auth"
        private const val BEARER_TYPE = "bearer"
        private const val ACCESS_TOKEN_EXPIRE_TIME = (1000 * 60 * 30)
        private const val REFRESH_TOKEN_EXPIRE_TIME = (1000 * 60 * 60 * 24 * 7)
    }

    private val key: Key by lazy {
        val secretKey: String = "ZVc3Z0g4bm5TVzRQUDJxUXBIOGRBUGtjRVg2WDl0dzVYVkMyWWs1Qlk3NkZBOXh1UzNoRWUzeTd6cVdEa0x2eQo=" // base64Encoded
        Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey))
    }

    fun generateJwtDto(oAuth2User: OAuth2User) : JwtDto {
        val now = Date().time
        val accessTokenExpiresIn: Date = Date(now + ACCESS_TOKEN_EXPIRE_TIME)

        val member = memberRepository.findByEmail(oAuth2User.attributes["email"] as String)

        val accessToken = Jwts.builder()
            .setSubject(member?.email) // payload "sub": "email"
            .claim(AUTHORITIES_KEY, member?.role)  // payload "auth": "ROLE_USER"
            .setExpiration(accessTokenExpiresIn) // payload "exp": 1516239022 (예시)
            .signWith(key, SignatureAlgorithm.HS512) // header "alg": "HS512"
            .compact()

        val refreshToken = Jwts.builder()
            .setSubject(member?.email)
            .setExpiration(Date(now + REFRESH_TOKEN_EXPIRE_TIME))
            .signWith(key, SignatureAlgorithm.HS512)
            .compact()

        return JwtDto(
            grantType = BEARER_TYPE,
            accessToken = accessToken,
            refreshToken = refreshToken,
            accessTokenExpiresIn = accessTokenExpiresIn.time
        )
    }

    fun resolveToken(request: HttpServletRequest): String {
        val bearerToken = request.getHeader(HttpHeaders.AUTHORIZATION)

        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(AccessTokenType.BEARER.value)) {
            return bearerToken.substring(7)
        }

        return ""
    }

    fun validateAccessToken(token: String): Boolean {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token)
            return true
        } catch (e: SecurityException) {
            println("올바르지 못한 토큰입니다.")
        } catch (e: MalformedJwtException) {
            println("올바르지 못한 토큰입니다.")
        } catch (e: ExpiredJwtException) {
            println("만료된 토큰입니다.")
        } catch (e: UnsupportedJwtException) {
            println("지원되지 않는 토큰입니다.")
        } catch (e: IllegalArgumentException) {
            println("잘못된 토큰입니다.")
        }
        return true
    }

    fun findAuthentication(accessToken: String): Authentication {
        val claims = parseClaims(accessToken)

        val authorities = mutableListOf(claims[AUTHORITIES_KEY] as String).map { role -> SimpleGrantedAuthority(role) }
        val user = User(claims[Claims.SUBJECT] as String, "", authorities)

        return UsernamePasswordAuthenticationToken(user, "", authorities)
    }

    private fun parseClaims(accessToken: String): Claims {
        return try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).body
        } catch (e: ExpiredJwtException) {
            e.claims
        }
    }
}

 

resolveToken, validateAccessToken, findAuthentication, parseClaims 함수가 추가되었습니다.

 

SecurityConfig

@Configuration
class SecurityConfig(
    private val customUserDetailService: CustomUserDetailService,
    private val jwtProvider: JwtProvider
) : WebSecurityConfigurerAdapter() {

    override fun configure(http: HttpSecurity) {
        http
            .csrf {
                it.disable()
            }
            .httpBasic {
                it.disable()
            }
            .sessionManagement {
                it.sessionCreationPolicy(SessionCreationPolicy.NEVER)
            }
            .authorizeRequests {
                it.antMatchers("/guest").hasRole("USER")
                it.antMatchers("/admin").hasRole("ADMIN")
            }
            .oauth2Login {
                it.userInfoEndpoint().userService(customUserDetailService)
                it.defaultSuccessUrl("/auth/login")
                it.failureUrl("/fail")
            }
            .addFilterBefore(JwtFilter(jwtProvider), UsernamePasswordAuthenticationFilter::class.java)
    }
}

/guest 와 /admin을 추가해주었습니다.

 

MainController

@RestController
class MainController {


    @GetMapping("/success")
    fun success(): String {
        return "success"
    }

    @GetMapping("/fail")
    fun fail(): String {
        return "fail"
    }

    @GetMapping("/guest")
    fun guest(): String {
        return "guest"
    }

    @GetMapping("/admin")
    fun admin(): String {
        return "admin"
    }
}

 


실행

1. 구글 계정으로 로그인을 한 후, jwt을 발급 받습니다.

 

2. 발급 받은 토큰은 User로 /guest로 접근이 가능합니다.

 

3. 이번엔 Admin이 접근 가능한 /admin로 접근을 시도해봅니다.

 

4. Forbidden이 뜨는 걸 확인할 수 있고, 접근을 하고 싶으면 DB에 저장된 ROLE_USER를 ROLE_ADMIN으로 바꾸면 됩니다.

댓글
댓글쓰기 폼
공지사항
Total
208,300
Today
615
Yesterday
833
링크
«   2022/08   »
  1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31      
글 보관함