๋กœ์ผ“๐Ÿพ
article thumbnail

 

์ด์–ด์„œ ๊ณ„์† ๊ตฌํ˜„ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

 

๊ตฌํ˜„ ์ž์ฒด๊ฐ€ ๋ชฉ์ ์ด๋ฏ€๋กœ ์ž์„ธํ•œ ์„ค๋ช…์„ ์ƒ๋žตํ•ฉ๋‹ˆ๋‹ค.

 

  1. JWT ์ƒ์„ฑํ•˜๊ธฐ
  2. JWT์— ๊ถŒํ•œ ์ถ”๊ฐ€ํ•ด์ฃผ๊ธฐ 
  3. ์ƒ์„ฑํ•œ JWT์— ๋Œ€ํ•ด ์ธ์ฆ/์ธ๊ฐ€ ํ•˜๊ธฐ 
  4. JWT ์žฌ๋ฐœ๊ธ‰ ํ•ด์ฃผ๊ธฐ (์ง„ํ–‰)

๊ตฌํ˜„

AuthController

@RestController
@RequestMapping("/auth")
class AuthController(
    private val authService: AuthService
) {

    /**
     * token ์ƒ์„ฑํ•ด์„œ ๋ณด๋‚ด์ฃผ๊ธฐ
     */
    @GetMapping("/login")
    fun login(@AuthenticationPrincipal oAuth2User: OAuth2User): ResponseEntity<JwtDto> {
        return ResponseEntity.ok(authService.login(oAuth2User))
    }

    @PostMapping("/reissue")
    fun reissue(@RequestBody reissueRequestDto: ReissueRequestDto): ResponseEntity<JwtDto> {
        return ResponseEntity.ok(authService.reissue(reissueRequestDto))
    }
}

 

AuthService

@Service
class AuthService(
    private val memberRepository: MemberRepository,
    private val jwtProvider: JwtProvider
) {

    @Transactional
    fun login(oAuth2User: OAuth2User) : JwtDto {
        //TODO: 1. ํšŒ์›์ด ์•„๋‹ˆ๋ผ๋ฉด ํšŒ์› ๊ฐ€์ž…์„ ์‹œ์ผœ์ค€๋‹ค.
        if(!memberRepository.existsByEmail(oAuth2User.attributes["email"] as String)) {
            val member = Member(
                email = oAuth2User.attributes["email"] as String,
                role = AuthType.ROLE_USER
            )
            memberRepository.save(member)
        }

        //TODO: 2. token ์„ ์ƒ์„ฑํ•ด์ค€๋‹ค.
        return jwtProvider.generateJwtDto(oAuth2User.attributes["email"] as String)
    }

    @Transactional(readOnly = true)
    fun reissue(reissueRequestDto: ReissueRequestDto): JwtDto {
        jwtProvider.validateRefreshToken(reissueRequestDto.refreshToken)

        val authentication = jwtProvider.findAuthentication(reissueRequestDto.refreshToken)

        return jwtProvider.generateJwtDto(authentication.name)
    }
}

 

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(email: String) : JwtDto {
        val now = Date().time
        val accessTokenExpiresIn: Date = Date(now + ACCESS_TOKEN_EXPIRE_TIME)

        val member = memberRepository.findByEmail(email)

        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)
            .claim(AUTHORITIES_KEY, member?.role)
            .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 validateRefreshToken(token: String) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token)
        } catch (e: SecurityException) {
            println("์˜ฌ๋ฐ”๋ฅด์ง€ ๋ชปํ•œ ํ† ํฐ์ž…๋‹ˆ๋‹ค.")
        } catch (e: MalformedJwtException) {
            println("์˜ฌ๋ฐ”๋ฅด์ง€ ๋ชปํ•œ ํ† ํฐ์ž…๋‹ˆ๋‹ค.")
        } catch (e: ExpiredJwtException) {
            println("๋งŒ๋ฃŒ๋œ ํ† ํฐ์ž…๋‹ˆ๋‹ค.")
        } catch (e: UnsupportedJwtException) {
            println("์ง€์›๋˜์ง€ ์•Š๋Š” ํ† ํฐ์ž…๋‹ˆ๋‹ค.")
        } catch (e: IllegalArgumentException) {
            println("์ž˜๋ชป๋œ ํ† ํฐ์ž…๋‹ˆ๋‹ค.")
        }
    }

    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
        }
    }
}

 

ReissueRequestDto

data class ReissueRequestDto(
    val refreshToken: String
) {
}

 

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("/auth/reissue").permitAll()
                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)
    }
}

 


์‹คํ–‰

 

1. POST /auth/reissue๋กœ ์ ‘๊ทผํ•ฉ๋‹ˆ๋‹ค. body์—๋Š” refreshToken์ด ๋‹ด๊น๋‹ˆ๋‹ค.

 

์ด๋ ‡๊ฒŒ Spring Security๋ฅผ ์ด์šฉํ•ด์„œ Oauth2, JWT๋ฅผ ๊ตฌํ˜„ํ•ด๋ณด์•˜์Šต๋‹ˆ๋‹ค.

 

์ฝ”๋“œ๋Š” Best Practice๋Š” ์•„๋‹ˆ๋ฉฐ ์˜ˆ์™ธ์ฒ˜๋ฆฌ, ์ŠคํŠธ๋ง ์•ˆํ‹ฐ ํŒจํ„ด ๋“ฑ์„ ํ™œ์šฉํ•˜์—ฌ์„œ ์ถ”๊ฐ€ ๊ฐœ๋ฐœํ•˜์‹œ๊ธธ ๋ฐ”๋ž๋‹ˆ๋‹ค.

 

์ „์ฒด ์ฝ”๋“œ๋Š” ๊นƒํ—™์—์„œ ํ™•์ธํ•˜์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

profile on loading

Loading...