-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #8 from DDD-Community/feat/login
feat: login 기능 개발
- Loading branch information
Showing
26 changed files
with
753 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
90
src/main/kotlin/mara/server/auth/google/GoogleApiClient.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
7
src/main/kotlin/mara/server/auth/google/GoogleInfoResponse.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 = "", | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 = "", | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) | ||
} | ||
} |
Oops, something went wrong.