๋กœ์ผ“๐Ÿพ
article thumbnail
๋ฐ˜์‘ํ˜•

 

์ด๋ฒˆ ๊ธ€์—์„œ๋Š” Spring Security + Google Oauth2 + JWT๋ฅผ ๊ตฌํ˜„ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

 

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

 

์ง„ํ–‰ํ•˜๊ธฐ ์•ž์„œ Google Oauth2 Client_ID, Client_Secret๋Š” ๊ฐœ์ธ์ ์œผ๋กœ ๋ฐ›์œผ์‹œ๊ธธ ๋ฐ”๋ž๋‹ˆ๋‹ค :)

 

์•„๋ž˜์™€ ๊ฐ™์€ ์ˆœ์„œ๋กœ ์ง„ํ–‰๋ฉ๋‹ˆ๋‹ค.

 

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

 

์ด๋ฒˆ ๊ธ€์—์„œ๋Š” JWT ์ƒ์„ฑํ•˜๋Š” ๋ถ€๋ถ„์— ๋Œ€ํ•ด ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค.


๊ตฌํ˜„

 

build.gradle.kts

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    id("org.springframework.boot") version "2.6.8"
    id("io.spring.dependency-management") version "1.0.11.RELEASE"
    kotlin("jvm") version "1.6.21"
    kotlin("plugin.spring") version "1.6.21"
    kotlin("plugin.jpa") version "1.6.21"
}

group = "lunit.io"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
    implementation("org.springframework.boot:spring-boot-starter-security")
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    runtimeOnly("com.h2database:h2")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("org.springframework.security:spring-security-test")

    //JWT Dependency
    compileOnly("io.jsonwebtoken:jjwt-api:0.11.2")
    runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.2")
    runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.2")
}

tasks.withType<KotlinCompile> {
    kotlinOptions {
        freeCompilerArgs = listOf("-Xjsr305=strict")
        jvmTarget = "11"
    }
}

tasks.withType<Test> {
    useJUnitPlatform()
}

 

application.yaml

spring:
  datasource:
    url: jdbc:h2:tcp://localhost/~/test
    username: sa
    password:
    driver-class-name: org.h2.Driver
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: ${GOOGLE_CLIENT_ID}
            client-secret: ${GOOGLE_CLIENT_SECRET}
            scope:
              - email
              - profile
  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
    hibernate:
      ddl-auto: create-drop
    properties:
      hibernate:
        format_sql: true
        show_sql: true

๋ฐ์ดํ„ฐ ๋ฒ ์ด์Šค๋Š” H2๋ฅผ ์‚ฌ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.

 

SecurityConfig

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

    override fun configure(http: HttpSecurity) {
        http
            .csrf {
                it.disable()
            }
            .httpBasic {
                it.disable()
            }
            .sessionManagement {
                it.sessionCreationPolicy(SessionCreationPolicy.NEVER) // #1
            }
            .oauth2Login {
                it.userInfoEndpoint().userService(customUserDetailService) // #2
                it.defaultSuccessUrl("/auth/login") // #3
                it.failureUrl("/fail")
            }
    }
}

#1

Google ๋กœ๊ทธ์ธ์„ ์ง„ํ–‰ํ•  ๋•Œ ์„ธ์…˜์ด ์‚ด์•„์žˆ์–ด์•ผ ํ•˜๋ฏ€๋กœ NEVER๋กœ ํ•ด์ค๋‹ˆ๋‹ค.

NEVER ์˜ต์…˜์€ ์„ธ์…˜์„ ์‚ฌ์šฉํ•˜์ง„ ์•Š์ง€๋งŒ ๋งŒ์•ฝ ํ•„์š”ํ•˜๋‹ค๋ฉด ์„ธ์…˜์„ ์ƒ์„ฑํ•ด์ฃผ๋Š” ์˜ต์…˜์ž…๋‹ˆ๋‹ค.

 

๋งŒ์•ฝ STATELESS๋กœ ํ•œ๋‹ค๋ฉด ์„ธ์…˜์ด ์œ ์ง€๋˜์ง€ ์•Š์•„ ๊ตฌ๊ธ€ ๊ณ„์ • ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค.

 

#2

oauth2Login์„ ์ง„ํ–‰ํ•˜๊ฒŒ ๋˜๋ฉด loadUser๋ผ๋Š” ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๊ฒŒ ๋˜๋Š”๋ฐ ์ด๊ฒƒ์„ customUserDetailService์—์„œ ํ˜ธ์ถœํ•œ๋‹ค๋Š” ์˜ต์…˜์ž…๋‹ˆ๋‹ค.

 

#3

Oauth2 ๋กœ๊ทธ์ธ์— ์„ฑ๊ณตํ•œ๋‹ค๋ฉด ์œ„์™€ ๊ฐ™์€ url๋กœ ๋ฆฌ๋‹ค์ด๋ ‰์…˜ ๋ฉ๋‹ˆ๋‹ค.

 

CustomUserDetailService

@Service
class CustomUserDetailService : DefaultOAuth2UserService() {

    override fun loadUser(userRequest: OAuth2UserRequest): OAuth2User {
        return super.loadUser(userRequest)
    }
}

 

JwtProvider

@Component
class JwtProvider {

    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 accessToken = Jwts.builder()
            .setSubject(oAuth2User.attributes["email"] as String) // payload "sub": "email"
            .setExpiration(accessTokenExpiresIn) // payload "exp": 1516239022 (์˜ˆ์‹œ)
            .signWith(key, SignatureAlgorithm.HS512) // header "alg": "HS512"
            .compact()

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

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

OAuth2User.attributes ์—๋Š” ์œ„์™€ ๊ฐ™์€ ์ •๋ณด๋“ค์ด ๋‹ด๊ฒจ์žˆ์Šต๋‹ˆ๋‹ค.

 

์‹œํฌ๋ฆฟ ํ‚ค ๊ฐ™์€ ๊ฒฝ์šฐ ์•Œ๊ณ ๋ฆฌ์ฆ˜์€ HS512๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— 512bit ์ด์ƒ์ด์—ฌ์•ผ ํ•˜๊ณ , base64๋กœ ์ธ์ฝ”๋”ฉํ•œ ๊ฐ’์ž…๋‹ˆ๋‹ค.

 

๊ถŒํ•œ ๋ถ€๋ถ„๋„ payload ๋ถ€๋ถ„์— ๋‹ด์•„์•ผ ํ•˜์ง€๋งŒ ์šฐ์„ ์€ ์ธ์ฆ์— ๋Œ€ํ•ด์„œ๋งŒ ๊ตฌํ˜„ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

JwtDto

data class JwtDto(
   val grantType: String,
   val accessToken: String,
   val refreshToken: String,
   val accessTokenExpiresIn: Long
) {
}

 

Member

@Entity
class Member(
    @Id @GeneratedValue
    var id: Long = 0,

    var email: String,

    @Enumerated(EnumType.STRING)
    var role: AuthType
) {
}

 

AuthType

enum class AuthType {
    ROLE_USER, ROLE_ADMIN
}

 

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

๋งŒ์•ฝ NEVER๊ฐ€ ์•„๋‹Œ STATELESS๋กœ ํ•ด์ฃผ์—ˆ๋‹ค๋ฉด ์„ธ์…˜์ด ์œ ์ง€๋˜์ง€ ์•Š์•„ oAuth2User๊ฐ€ null์ด ๋ฉ๋‹ˆ๋‹ค.

 

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

 

MemberRepository

interface MemberRepository : JpaRepository<Member, Long> {

    fun findByEmail(email: String) : Member?
    fun existsByEmail(email: String) : Boolean
}

 


์‹คํ–‰

์ด๋กœ์จ token์„ ์ƒ์„ฑํ•ด์ฃผ๋Š” ๋กœ์ง์€ ๋์ด ๋‚ฌ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ํ…Œ์ŠคํŠธ ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

 

1. http://localhost:8080/oauth2/authorization/google ๋กœ ์ ‘์† ํ›„, ๊ณ„์ •์„ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค.

2. JWT๋ฅผ ๋ถ€์—ฌ๋ฐ›์Šต๋‹ˆ๋‹ค.

 

 

3. https://jwt.io/ ์—์„œ ํ™•์ธํ•ด๋ด…๋‹ˆ๋‹ค.

์‹œํฌ๋ฆฟ ํ‚ค๋ฅผ ๋„ฃ์—ˆ์„ ๋•Œ JWT๊ฐ€ ๋™์ผํ•œ ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

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

Loading...