이번 글에서는 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 재발급 해주기
'...' 카테고리의 다른 글
[Spring] Security +Google Oauth2 + JWT 구현하기 (3) - 생성한 JWT에 대해 인증/인가 하기 (0) | 2022.06.05 |
---|---|
[Spring] Security +Google Oauth2 + JWT 구현하기 (2) - JWT에 권한 추가해주기 (0) | 2022.06.04 |
[Kotlin] 제네릭 타입과 variance 한정자를 활용하라 (2) | 2022.04.29 |
[Java] 스프링을 왜 사용할까?(2) - OCP와 DIP 해결 By 순수 자바 (3) | 2022.02.01 |
[Java] 스프링을 왜 사용할까?(1) - OCP와 DIP의 위배 (4) | 2022.01.20 |