Skip to content

Commit

Permalink
Merge pull request #8 from DDD-Community/feat/login
Browse files Browse the repository at this point in the history
feat: login 기능 개발
  • Loading branch information
jhkang1517 authored Jan 25, 2024
2 parents 3fdbea7 + aca3ef1 commit cd8741d
Show file tree
Hide file tree
Showing 26 changed files with 753 additions and 17 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ jobs:
spring.datasource.url: ${{ secrets.DB_URL }}
spring.datasource.username: ${{ secrets.DB_USERNAME }}
spring.datasource.password: ${{ secrets.DB_PASSWORD }}
oauth.kakao.client-id: ${{ secrets.KAKAO_CLIENT_ID }}
oauth.kakao.client-secret: ${{ secrets.KAKAO_CLIENT_SECRET}}
oauth.kakao.app-admin-key: ${{ secrets.KAKAO_APP_ADMIN_KEY }}
oauth.google.client-id: ${{ secrets.GOOGLE_CLIENT_ID }}
oauth.google.client-secret: ${{ secrets.GOOGLE_CLIENT_SECRET }}
jwt.secret-key: ${{ secrets.JWT_SECRET_KEY }}

- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
Expand Down
10 changes: 9 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,17 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
// Swagger
// implementation("io.springfox:springfox-boot-starter:3.0.0")
// implementation("io.springfox:springfox-boot-starter:3.0.0")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0")

// security
implementation("org.springframework.boot:spring-boot-starter-security:3.2.0")

// jwt
implementation("io.jsonwebtoken:jjwt-api:0.11.2")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.2")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.2")

// test
testImplementation("org.springframework.boot:spring-boot-starter-test")
runtimeOnly("com.h2database:h2:2.1.214")
Expand Down
13 changes: 13 additions & 0 deletions src/main/kotlin/mara/server/auth/ClientConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package mara.server.auth

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.client.RestTemplate

@Configuration
class ClientConfig {
@Bean
fun restTemplate(): RestTemplate {
return RestTemplate()
}
}
90 changes: 90 additions & 0 deletions src/main/kotlin/mara/server/auth/google/GoogleApiClient.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package mara.server.auth.google

import mara.server.util.logger
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.HttpEntity
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpMethod
import org.springframework.http.MediaType
import org.springframework.stereotype.Component
import org.springframework.util.LinkedMultiValueMap
import org.springframework.util.MultiValueMap
import org.springframework.web.client.RestTemplate

@Component
class GoogleApiClient(
private val restTemplate: RestTemplate,
@Value("\${oauth.google.url.auth}")
private val authUrl: String,

@Value("\${oauth.google.url.api}")
private val apiUrl: String,

@Value("\${oauth.google.client-id}")
private val clientId: String,

@Value("\${oauth.google.client-secret}")
private val secret: String,

) {

val log = logger()

fun getRedirectUri(status: String): String {
val os = System.getProperty("os.name")
log.info("OS : {}", os)
if (status == "local")return ""
if (status == "prod") return ""
return "http://localhost:8080/users/google-login"
}

fun requestAccessToken(authorizedCode: String, status: String): String {
val url = "$authUrl/token"
val httpHeaders = HttpHeaders()
httpHeaders.contentType = MediaType.APPLICATION_FORM_URLENCODED
val body: MultiValueMap<String, String> = LinkedMultiValueMap()
body.add("code", authorizedCode)
body.add("grant_type", "authorization_code")
body.add("client_id", clientId)
body.add("client_secret", secret)
body.add("redirect_uri", getRedirectUri(status))

val request = HttpEntity(body, httpHeaders)

val response = restTemplate.postForObject(url, request, GoogleTokens::class.java)
?: throw IllegalStateException("GoogleTokens response is null")
return response.accessToken
}

fun requestOauthInfo(accessToken: String): GoogleInfoResponse {
val url = "$apiUrl/oauth2/v1/userinfo"

val httpHeaders = HttpHeaders()
httpHeaders.contentType = MediaType.APPLICATION_FORM_URLENCODED
httpHeaders.set("Authorization", "Bearer $accessToken")

val body = LinkedMultiValueMap<String, String>()

val request = HttpEntity(body, httpHeaders)

return restTemplate.exchange(url, HttpMethod.GET, request, GoogleInfoResponse::class.java).body
?: throw IllegalStateException("GoogleInfoResponse is null")
}

// fun logout(kaKaoId: Long): Boolean {
// val url = "$apiUrl/v1/user/logout"
//
// val httpHeaders = HttpHeaders()
// httpHeaders.contentType = MediaType.APPLICATION_FORM_URLENCODED
// httpHeaders.set("Authorization", "KakaoAK $appAdminKey")
//
// val body = LinkedMultiValueMap<String, String>()
// body.add("target_id_type", "user_id")
// body.add("target_id", kaKaoId.toString())
// val request = HttpEntity(body, httpHeaders)
// val response = restTemplate.postForObject(url, request, KaKaoUserLogout::class.java)
// ?: throw IllegalStateException("KakaoInfoResponse is null")
//
// return response.id == kaKaoId
// }
}
7 changes: 7 additions & 0 deletions src/main/kotlin/mara/server/auth/google/GoogleInfoResponse.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package mara.server.auth.google
import com.fasterxml.jackson.annotation.JsonProperty

data class GoogleInfoResponse(
@JsonProperty("email")
var email: String = "",
)
23 changes: 23 additions & 0 deletions src/main/kotlin/mara/server/auth/google/GoogleTokens.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package mara.server.auth.google

import com.fasterxml.jackson.annotation.JsonProperty

data class GoogleTokens(
@JsonProperty("access_token")
var accessToken: String = "",

@JsonProperty("token_type")
var tokenType: String = "",

@JsonProperty("refresh_token")
var refreshToken: String = "",

@JsonProperty("expires_in")
var expiresIn: String = "",

@JsonProperty("id_token")
var idToken: String = "",

@JsonProperty("scope")
var scope: String = "",
)
33 changes: 33 additions & 0 deletions src/main/kotlin/mara/server/auth/jwt/JwtFilter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package mara.server.auth.jwt

import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter

@Component
class JwtFilter(private val jwtProvider: JwtProvider) : OncePerRequestFilter() {

private fun HttpServletRequest.getToken(): String? {
val rawToken = this.getHeader("Authorization")
return if (rawToken != null && rawToken.startsWith("Bearer"))
rawToken.replace("Bearer ", "")
else null
}

override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain,
) {
val jwt = request.getToken()

if (jwt != null && jwtProvider.validate(jwt)) {
SecurityContextHolder.getContext().authentication =
jwtProvider.getAuthentication(jwt)
}
filterChain.doFilter(request, response)
}
}
57 changes: 57 additions & 0 deletions src/main/kotlin/mara/server/auth/jwt/JwtHandlers.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package mara.server.auth.jwt

import com.fasterxml.jackson.databind.ObjectMapper
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import mara.server.common.CommonResponse
import mara.server.util.getIp
import mara.server.util.logger
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.security.access.AccessDeniedException
import org.springframework.security.core.AuthenticationException
import org.springframework.security.web.AuthenticationEntryPoint
import org.springframework.security.web.access.AccessDeniedHandler
import org.springframework.stereotype.Component

@Component
class JwtAuthenticationEntryPoint(private val objectMapper: ObjectMapper) : AuthenticationEntryPoint {
val log = logger()
override fun commence(
request: HttpServletRequest,
response: HttpServletResponse,
e: AuthenticationException,
) {
log.warn("{} {} {} (401) - {}", request.getIp(), request.method, request.requestURI, e.message)
response.contentType = MediaType.APPLICATION_JSON_VALUE
response.characterEncoding = "utf-8"
response.status = HttpStatus.UNAUTHORIZED.value()
val body = objectMapper.writeValueAsString(
CommonResponse<Any>(
message = "authentication failed, please recheck JWT.",
),
)
response.writer.write(body)
}
}

@Component
class JwtAccessDeniedHandler(private val objectMapper: ObjectMapper) : AccessDeniedHandler {
val log = logger()
override fun handle(
request: HttpServletRequest,
response: HttpServletResponse,
e: AccessDeniedException,
) {
log.warn("(403) Access is not granted = {}", e.message)
response.contentType = MediaType.APPLICATION_JSON_VALUE
response.characterEncoding = "utf-8"
response.status = HttpStatus.FORBIDDEN.value()
val body = objectMapper.writeValueAsString(
CommonResponse<Any>(
message = "access denied, user is not granted.",
),
)
response.writer.write(body)
}
}
69 changes: 69 additions & 0 deletions src/main/kotlin/mara/server/auth/jwt/JwtProvider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package mara.server.auth.jwt

import io.jsonwebtoken.Jwts
import io.jsonwebtoken.security.Keys
import mara.server.auth.security.PrincipalDetailsService
import mara.server.domain.user.User
import mara.server.util.logger
import org.springframework.beans.factory.annotation.Value
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.Authentication
import org.springframework.stereotype.Component
import java.security.Key
import java.util.Date

@Component
class JwtProvider(
@Value("\${jwt.secret-key}") private val secretKey: String,
@Value("\${jwt.access-duration-mils}") private val accessDurationMils: Long,
private val principalDetailsService: PrincipalDetailsService,
) {
val key: Key = Keys.hmacShaKeyFor(secretKey.toByteArray())
val log = logger()

fun generateToken(user: User): String {
val now = Date(System.currentTimeMillis())
var claimName = "kakaoId"
var claimValue = user.kakaoId.toString()
if (user.googleEmail != null) {
claimName = "googleEmail"
claimValue = user.googleEmail!!
}

return Jwts.builder()
.setSubject(user.userId.toString())
.claim(claimName, claimValue)
.setIssuedAt(now)
.setExpiration(Date(now.time + accessDurationMils))
.signWith(key)
.compact()
}

fun validate(token: String): Boolean {
return try {
Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
true
} catch (e: Exception) {
log.warn("JWT 오류 발생 [{}] {}", e.javaClass.simpleName, e.message)
false
}
}

fun getAuthentication(token: String): Authentication {
val body = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.body

val userDetails = principalDetailsService.loadUserByUsername(userId = body.subject)
return UsernamePasswordAuthenticationToken(
userDetails.username,
userDetails.password,
userDetails.authorities,
)
}
}
Loading

0 comments on commit cd8741d

Please sign in to comment.