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

 

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

 

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

 

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

 

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

 

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

 

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


1. ๊ตฌํ˜„

 

1.0.1. build.gradle.kts

<kotlin />
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() }

1.0.2.  

1.0.3. application.yaml

<kotlin />
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๋ฅผ ์‚ฌ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.

 

1.0.4. SecurityConfig

<kotlin />
@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๋กœ ๋ฆฌ๋‹ค์ด๋ ‰์…˜ ๋ฉ๋‹ˆ๋‹ค.

 

1.0.5. CustomUserDetailService

<kotlin />
@Service class CustomUserDetailService : DefaultOAuth2UserService() { override fun loadUser(userRequest: OAuth2UserRequest): OAuth2User { return super.loadUser(userRequest) } }

 

1.0.6. JwtProvider

<kotlin />
@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 ๋ถ€๋ถ„์— ๋‹ด์•„์•ผ ํ•˜์ง€๋งŒ ์šฐ์„ ์€ ์ธ์ฆ์— ๋Œ€ํ•ด์„œ๋งŒ ๊ตฌํ˜„ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

1.0.7. JwtDto

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

 

1.0.8. Member

<kotlin />
@Entity class Member( @Id @GeneratedValue var id: Long = 0, var email: String, @Enumerated(EnumType.STRING) var role: AuthType ) { }

1.0.9.  

1.0.10. AuthType

<kotlin />
enum class AuthType { ROLE_USER, ROLE_ADMIN }

1.0.11.  

1.0.12. AuthController

<kotlin />
@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์ด ๋ฉ๋‹ˆ๋‹ค.

 

1.0.13. AuthService

<kotlin />
@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) } }

 

1.0.14. MemberRepository

<kotlin />
interface MemberRepository : JpaRepository<Member, Long> { fun findByEmail(email: String) : Member? fun existsByEmail(email: String) : Boolean }

 


2. ์‹คํ–‰

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

 

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

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

 

 

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

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

 

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