์ด๋ฒ ๊ธ์์๋ Spring Security + Google Oauth2 + JWT๋ฅผ ๊ตฌํํด๋ณด๊ฒ ์ต๋๋ค.
๊ตฌํ ์์ฒด๊ฐ ๋ชฉ์ ์ด๋ฏ๋ก ์์ธํ ์ค๋ช ์ ์๋ตํฉ๋๋ค.
์งํํ๊ธฐ ์์ Google Oauth2 Client_ID, Client_Secret๋ ๊ฐ์ธ์ ์ผ๋ก ๋ฐ์ผ์๊ธธ ๋ฐ๋๋๋ค :)
์๋์ ๊ฐ์ ์์๋ก ์งํ๋ฉ๋๋ค.
- JWT ์์ฑํ๊ธฐ (์งํ)
- JWT์ ๊ถํ ์ถ๊ฐํด์ฃผ๊ธฐ
- ์์ฑํ JWT์ ๋ํด ์ธ์ฆ/์ธ๊ฐ ํ๊ธฐ
- 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๊ฐ ๋์ผํ ๊ฒ์ ๋ณผ ์ ์์ต๋๋ค.
- JWT ์์ฑํ๊ธฐ (์๋ฃ)
- JWT์ ๊ถํ ์ถ๊ฐํด์ฃผ๊ธฐ
- ์์ฑํ JWT์ ๋ํด ์ธ์ฆ/์ธ๊ฐ ํ๊ธฐ
- JWT ์ฌ๋ฐ๊ธ ํด์ฃผ๊ธฐ