아이엠 !나이롱맨😎
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 재발급 해주기
반응형

article prev thumbnail
article next thumbnail
profile on loading

Loading...