Skip to content
42 changes: 42 additions & 0 deletions src/main/kotlin/com/snuxi/config/SecurityConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.snuxi.config

import com.snuxi.user.service.GoogleOAuth2UserService
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpMethod
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.web.SecurityFilterChain

@Configuration
@EnableWebSecurity
class SecurityConfig(
val googleOAuth2UserService: GoogleOAuth2UserService
) {
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http
.csrf { it.disable() }
.authorizeHttpRequests {
it.requestMatchers("/", "/login").permitAll()
it.anyRequest().authenticated()
}
.oauth2Login {
it.loginPage("/login")
it.userInfoEndpoint {
endpoint ->
endpoint.userService(googleOAuth2UserService)
}
it.defaultSuccessUrl("/", true)
}
.logout {
it.logoutSuccessUrl("/")
it.logoutUrl("/logout")
it.invalidateHttpSession(true)
}
return http.build()
}



}
12 changes: 12 additions & 0 deletions src/main/kotlin/com/snuxi/user/controller/AuthController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.snuxi.user.controller

import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.GetMapping

@Controller
class AuthController {
@GetMapping("/login")
fun login(): String {
return "redirect:/oauth2/authorization/google"
}
}
26 changes: 26 additions & 0 deletions src/main/kotlin/com/snuxi/user/controller/UserController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.snuxi.user.controller

import com.snuxi.user.dto.UserResponse
import com.snuxi.user.service.UserService
import org.springframework.http.ResponseEntity
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.security.oauth2.core.user.OAuth2User
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/users")
class UserController(
private val userService: UserService
) {
@GetMapping("/profile")
fun getMyProfile(
@AuthenticationPrincipal
oAuth2User: OAuth2User
): ResponseEntity<UserResponse> {
val email = oAuth2User.attributes["email"] as String
val profile = userService.getProfile(email)
return ResponseEntity.ok(profile)
}
}
18 changes: 18 additions & 0 deletions src/main/kotlin/com/snuxi/user/dto/UserResponse.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.snuxi.user.dto

import com.snuxi.user.model.Role
import com.snuxi.user.model.User

data class UserResponse(
val email: String,
val username: String,
val profileImageUrl: String?,
val role: Role
) {
constructor(user: User) : this(
email = user.email,
username = user.username,
profileImageUrl = user.profileImageUrl,
role = user.role
)
}
37 changes: 37 additions & 0 deletions src/main/kotlin/com/snuxi/user/model/User.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.snuxi.user.model

import jakarta.persistence.*
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedDate
import java.time.Instant


@Entity
@Table(name = "users")
class User(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,

@Column(nullable = false, unique = true)
val email: String,

@Column(nullable = false)
val username: String,

var profileImageUrl: String? = null,

@Enumerated(EnumType.STRING)
val role: Role = Role.USER
) {
@CreatedDate
var createdAt: Instant? = null

@LastModifiedDate
var updatedAt: Instant? = null
}

enum class Role {
USER, ADMIN
}

8 changes: 8 additions & 0 deletions src/main/kotlin/com/snuxi/user/repository/UserRepository.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.snuxi.user.repository

import com.snuxi.user.model.User
import org.springframework.data.jpa.repository.JpaRepository

interface UserRepository: JpaRepository<User, Long> {
fun findByEmail(email: String): User?
}
64 changes: 64 additions & 0 deletions src/main/kotlin/com/snuxi/user/service/GoogleOAuth2UserService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.snuxi.user.service

import com.snuxi.user.model.User
import com.snuxi.user.repository.UserRepository
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService
import org.springframework.security.oauth2.core.OAuth2AuthenticationException
import org.springframework.security.oauth2.core.OAuth2Error
import org.springframework.security.oauth2.core.user.DefaultOAuth2User
import org.springframework.security.oauth2.core.user.OAuth2User
import org.springframework.stereotype.Service
import java.util.Collections

@Service
class GoogleOAuth2UserService(
private val userRepository: UserRepository,
) : OAuth2UserService<OAuth2UserRequest, OAuth2User> {
override fun loadUser(userRequest: OAuth2UserRequest): OAuth2User {
val delegate = DefaultOAuth2UserService()
val oAuth2User = delegate.loadUser(userRequest)
val attributes = oAuth2User.attributes

val email = attributes["email"] as String
if (!email.endsWith("@snu.ac.kr")) {
throw OAuth2AuthenticationException(
OAuth2Error("Email is not valid"),
"서울대학교(@snu.ac.kr) 계정만 로그인 가능합니다."
)
}

val user = getOrSave(attributes)
val userNameAttributeName = userRequest.clientRegistration.providerDetails.userInfoEndpoint.userNameAttributeName

return DefaultOAuth2User(
Collections.singleton(SimpleGrantedAuthority("ROLE_${user.role.name}")),
attributes,
userNameAttributeName
)
}

private fun getOrSave(attributes: Map<String, Any>): User {
val email = attributes["email"] as String

val existingUser = userRepository.findByEmail(email)

if(existingUser != null) {
return existingUser
}

val name = attributes["name"] as String
val picture = attributes["picture"] as? String

return userRepository.save(
User(
email = email,
username = name,
profileImageUrl = picture
)
)
}

}
19 changes: 19 additions & 0 deletions src/main/kotlin/com/snuxi/user/service/UserService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.snuxi.user.service

import com.snuxi.user.dto.UserResponse
import com.snuxi.user.repository.UserRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
@Transactional(readOnly = true)
class UserService(
val userRepository: UserRepository
) {
fun getProfile(email: String): UserResponse {
val user = userRepository.findByEmail(email)
?: throw IllegalStateException("User not found")

return UserResponse(user)
}
}
2 changes: 1 addition & 1 deletion src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@ spring:
###
app:
s3:
bucket: snuxi-dev-assets
bucket: snuxi-dev-assets