jwt secret 생성
https://jwtsecret.com/generate
JwtSecret.com - Generate JWT Secrets Online
Full secret is hidden for security.
jwtsecret.com
- 위사이트 접속후 32자 클릭, Generate 선택후 복사
application.yml
spring:
# H2 Console 설정
h2:
console:
enabled: true # H2 Console을 사용할지 여부
path: /h2-console # H2 Console의 접근 경로
# 데이터베이스 설정
datasource:
driver-class-name: org.h2.Driver # H2 드라이버 사용
url: jdbc:h2:mem:management # 메모리 내 데이터베이스 (테스트용)
username: sa # 접속할 사용자명
password: # 비밀번호 (없으면 공백으로 설정)
# JPA 설정
jpa:
hibernate:
ddl-auto: create # 테이블 자동 생성 및 업데이트 (설정에 따라 'none', 'update', 'create', 'create-drop' 등이 가능)
show-sql: true # SQL 쿼리를 로그에 출력
database-platform: org.hibernate.dialect.H2Dialect # H2 데이터베이스용 Hibernate Dialect 설정
properties:
hibernate:
format_sql: true # SQL을 보기 쉽게 포맷
jwt:
expiration_time: 86400000 #1일
secret: 위에 복사한 key를 여기에 넣으세요
jwt, expiration_time, secret 추가
이번 포스팅에 추가, 변경된 파일들
build.gradle.kts
plugins {
id("org.springframework.boot") version "3.4.2"
id("io.spring.dependency-management") version "1.1.7"
kotlin("jvm") version "1.9.25"
kotlin("plugin.spring") version "1.9.25"
kotlin("plugin.jpa") version "1.9.25"
kotlin("kapt") version "1.9.25"
}
group = "com.contact"
version = "0.0.1-SNAPSHOT"
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.boot:spring-boot-starter-validation")
//jwt
implementation("io.jsonwebtoken:jjwt-api:0.11.5")
implementation("io.jsonwebtoken:jjwt-impl:0.11.5")
implementation("io.jsonwebtoken:jjwt-jackson:0.11.5")
// QueryDSL 의존성 추가
implementation("com.querydsl:querydsl-jpa:5.1.0:jakarta")
kapt("com.querydsl:querydsl-apt:5.1.0:jakarta")
kapt("jakarta.annotation:jakarta.annotation-api")
kapt("jakarta.persistence:jakarta.persistence-api")
implementation ("org.springframework.boot:spring-boot-starter-security")
runtimeOnly("com.h2database:h2")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
// Querydsl 설정부 추가 - start
val generated = file("src/main/generated")
// querydsl QClass 파일 생성 위치를 지정
tasks.withType<JavaCompile> {
options.generatedSourceOutputDirectory.set(generated)
}
// kotlin source set 에 querydsl QClass 위치 추가
sourceSets {
main {
kotlin.srcDirs += generated
}
}
// gradle clean 시에 QClass 디렉토리 삭제
tasks.named("clean") {
doLast {
generated.deleteRecursively()
}
}
kapt {
generateStubs = true
}
- jwt 부분 추가
각 의존성의 역할
- jjwt-api (io.jsonwebtoken:jjwt-api:0.11.5)
JWT를 생성, 서명, 검증할 수 있는 API를 제공하는 라이브러리
예를 들어, JWT를 생성할 때 Jwts.builder() 같은 기능을 사용할 수 있도록 해줌
- jjwt-impl (io.jsonwebtoken:jjwt-impl:0.11.5)
jjwt-api에서 제공하는 기능의 실제 구현체
API만 추가하면 동작하지 않으므로 반드시 함께 추가
- jjwt-jackson (io.jsonwebtoken:jjwt-jackson:0.11.5)
JSON 파싱을 위해 Jackson을 활용하는 구현체
JWT의 Payload(내용) 을 JSON 형식으로 변환하거나, JSON을 객체로 변환할 때 필요
Jackson을 사용하여 claims(클레임, JWT의 데이터) 정보를 다룰 수 있도록 해줌
User.kt
package com.contact.management.entity
import com.contact.management.audit.BaseEntity
import com.contact.management.dto.SignUpRequest
import com.contact.management.dto.UserUpdateRequest
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.Table
import org.springframework.security.crypto.password.PasswordEncoder
@Entity
@Table(name = "users")
class User (
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long?= null,
var password: String,
var name: String,
var age: Int,
var email: String,
@Enumerated(EnumType.STRING)
@Column(name = "ROLE", nullable = false)
var role: RoleType // Adding the role field
) : BaseEntity(){
companion object {
fun from(request: SignUpRequest, encoder: PasswordEncoder) = User(
password = encoder.encode(request.password),
name = request.name,
age = request.age,
email = request.email,
role = RoleType.ROLE_USER
)
}
fun update(request: UserUpdateRequest, encoder: PasswordEncoder){
this.password = encoder.encode(request.password)
this.name = request.name
this.age = request.age
}
}
- 사용자 권한 role 추가
RoleType.kt
package com.contact.management.entity
enum class RoleType {
ROLE_USER, // 일반 사용자
ROLE_ADMIN // 관리자
}
- 권한은 간단하게 일반사용자, 관리자
JwtAccessDeniedHandler.kt
package com.contact.management.jwt
import jakarta.servlet.ServletException
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.access.AccessDeniedException
import org.springframework.security.web.access.AccessDeniedHandler
import org.springframework.stereotype.Component
import java.io.IOException
/**
* 필요한 권한이 존재하지 않는 경우에 403 Forbidden 에러 return
*/
@Component
class JwtAccessDeniedHandler : AccessDeniedHandler {
@Throws(IOException::class, ServletException::class)
override fun handle(request: HttpServletRequest, response: HttpServletResponse, accessDeniedException: AccessDeniedException) {
response.sendError(HttpServletResponse.SC_FORBIDDEN)
}
}
- 이 코드는 Spring Security에서 JWT 기반 인증 및 권한 관리를 할 때, 사용자가 필요한 권한이 없을 경우 403 Forbidden 응답을 반환하는 역할을 하는 클래스
동작방식
1. 사용자가 보호된 API 엔드포인트에 요청
2. Spring Security가 사용자의 JWT 토큰을 검증
3. 사용자가 필요한 권한이 없으면 JwtAccessDeniedHandler의 handle() 메서드가 호출됨
4. 클라이언트에게 403 Forbidden 응답 반환
JwtAuthenticationEntryPoint.kt
package com.contact.management.jwt
import jakarta.servlet.ServletException
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.core.AuthenticationException
import org.springframework.security.web.AuthenticationEntryPoint
import org.springframework.stereotype.Component
import java.io.IOException
/**
* 유효한 자격증명을 제공하지 않고 접근하려 할때 401 Unauthorized 에러 리턴
*/
@Component
class JwtAuthenticationEntryPoint : AuthenticationEntryPoint {
@Throws(IOException::class, ServletException::class)
override fun commence(request: HttpServletRequest, response: HttpServletResponse, authException: AuthenticationException) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED)
}
}
JwtAuthenticationEntryPoint는 로그인하지 않은 사용자가 보호된 API에 접근하면 401 응답을 반환
@Component를 사용하여 Spring Bean으로 등록되며, Security 설정에서 사용됩
AuthenticationEntryPoint 인터페이스를 구현하여 Spring Security에서 인증되지 않은 요청을 처리
JwtAuthFilter.kt
package com.contact.management.jwt
import com.contact.management.security.CustomUserDetailsService
import jakarta.servlet.FilterChain
import jakarta.servlet.ServletException
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.web.filter.OncePerRequestFilter
import java.io.IOException
class JwtAuthFilter(
private val customUserDetailsService: CustomUserDetailsService,
private val jwtUtil: JwtUtil
) : OncePerRequestFilter() { // OncePerRequestFilter -> 한 번 실행 보장
@Throws(ServletException::class, IOException::class)
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {
val authorizationHeader = request.getHeader("Authorization")
// JWT가 헤더에 있는 경우
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
val token = authorizationHeader.substring(7)
// JWT 유효성 검증
if (jwtUtil.validateToken(token)) {
val userId = jwtUtil.getUserId(token)
// 유저와 토큰 일치 시 userDetails 생성
val userDetails: UserDetails = customUserDetailsService.loadUserByUsername(userId.toString())
val usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.authorities
)
SecurityContextHolder.getContext().authentication = usernamePasswordAuthenticationToken
}
}
filterChain.doFilter(request, response) // 다음 필터로 넘기기
}
}
1. HTTP 요청의 "Authorization" 헤더에서 JWT를 추출.
2. JWT가 "Bearer "로 시작하는지 확인.
3. JWT의 유효성을 검사.
4. 유효하면 userId를 추출하여 UserDetails 객체를 생성.
5. UsernamePasswordAuthenticationToken을 생성하고 SecurityContextHolder에 저장.
6. 인증이 완료된 요청으로 설정한 후, 다음 필터로 넘김.
요약
JWT 인증을 처리하는 필터로, 요청을 가로채어 JWT를 검증하고 SecurityContext에 인증 정보를 저장하는 역할.
JWT를 헤더에서 추출하고 유효성을 검사한 후, 해당 사용자 정보를 SecurityContextHolder에 저장하여 Spring Security가 인증된 사용자로 인식하도록 설정.
Spring Security 설정에서 필터로 등록해야 동작.
JwtUtil.kt
package com.contact.management.jwt
import com.contact.management.exception.CommonException
import com.contact.management.exception.CommonExceptionCode
import com.contact.management.security.CustomUserDto
import io.jsonwebtoken.*
import io.jsonwebtoken.io.Decoders
import io.jsonwebtoken.security.Keys
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
import java.security.Key
import java.time.ZonedDateTime
import java.util.*
/**
* [JWT 관련 메서드를 제공하는 클래스]
*/
@Component
class JwtUtil(
@Value("\${jwt.secret}") secretKey: String,
@Value("\${jwt.expiration_time}") private val accessTokenExpTime: Long
) {
private val log: Logger = LoggerFactory.getLogger(JwtUtil::class.java)
private val key: Key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey))
/**
* Access Token 생성
* @param user
* @return Access Token String
*/
fun createAccessToken(user: CustomUserDto): String {
return createToken(user, accessTokenExpTime)
}
/**
* JWT 생성
* @param user
* @param expireTime
* @return JWT String
*/
private fun createToken(user: CustomUserDto, expireTime: Long): String {
val claims: Claims = Jwts.claims().apply {
put("id", user.id)
put("email", user.email)
put("authorities", user.role)
}
val now = ZonedDateTime.now()
val tokenValidity = now.plusSeconds(expireTime)
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(Date.from(now.toInstant()))
.setExpiration(Date.from(tokenValidity.toInstant()))
.signWith(key, SignatureAlgorithm.HS256)
.compact()
}
/**
* Token에서 User ID 추출
* @param token
* @return User ID
*/
fun getUserId(token: String): Long {
val id = parseClaims(token)["id", Integer::class.java] // Read the claim as an Integer
return id?.toLong() ?: throw CommonException(CommonExceptionCode.INVALID_AUTH_TOKEN) // Convert Integer to Long
}
/**
* JWT 검증
* @param token
* @return IsValidate
*/
fun validateToken(token: String): Boolean {
return try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token)
true
} catch (e: io.jsonwebtoken.security.SecurityException) {
log.info("Invalid JWT Token", e)
false
} catch (e: MalformedJwtException) {
log.info("Invalid JWT Token", e)
false
} catch (e: ExpiredJwtException) {
log.info("Expired JWT Token", e)
false
} catch (e: UnsupportedJwtException) {
log.info("Unsupported JWT Token", e)
false
} catch (e: IllegalArgumentException) {
log.info("JWT claims string is empty.", e)
false
}
}
/**
* JWT Claims 추출
* @param accessToken
* @return JWT Claims
*/
fun parseClaims(accessToken: String): Claims {
return try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).body
} catch (e: ExpiredJwtException) {
e.claims
}
}
}
이 코드는 JWT(JSON Web Token) 관련 유틸리티 클래스로,
JWT 생성
JWT 검증
JWT에서 사용자 정보 추출
하는 기능을 제공
Spring Security 기반으로 JWT 인증을 처리하는 핵심 로직
JWT 인증 흐름
클라이언트가 로그인 → JwtUtil.createAccessToken(user) 실행 → JWT 생성 후 반환.
클라이언트가 JWT를 Authorization 헤더에 담아 요청.
JwtAuthFilter가 요청을 가로채 JwtUtil.validateToken(token) 실행 → JWT가 유효하면 SecurityContext에 인증 정보 저장.
Spring Security는 SecurityContext에서 인증된 사용자 정보를 가져와 요청을 처리.
CustomUserDetails.kt
package com.contact.management.security
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.UserDetails
class CustomUserDetails(
private val member: CustomUserDto // Change from CustomUserInfoDto to CustomUserDto
) : UserDetails {
override fun getAuthorities(): Collection<GrantedAuthority> {
// RoleType에 따라 권한 리스트를 생성
val roles = listOf(member.role).map { role -> SimpleGrantedAuthority(role.name) }
return roles
}
override fun getPassword(): String = member.password // Ensure the DTO has password if needed
override fun getUsername(): String = member.id.toString() // or member.name if that's preferred
override fun isAccountNonExpired(): Boolean = true
override fun isAccountNonLocked(): Boolean = true
override fun isCredentialsNonExpired(): Boolean = true
override fun isEnabled(): Boolean = true
}
CustomUserDetails 클래스는 사용자 정보를 Spring Security에 맞는 형태로 변환하여 제공
이를 통해 Spring Security의 인증(Authentication) 및 인가(Authorization) 절차가 정상적으로 처리
CustomUserDetailsService.kt
package com.contact.management.security
import com.contact.management.entity.User
import com.contact.management.repository.UserRepository
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@Service
@Transactional(readOnly = true)
class CustomUserDetailsService(
private val userRepository: UserRepository
) : UserDetailsService {
override fun loadUserByUsername(id: String): UserDetails {
val user: User = userRepository.findById(id.toLong())
.orElseThrow { UsernameNotFoundException("해당하는 유저가 없습니다.") }
// entity -> dto 변환
val dto = CustomUserDto.from(user)
return CustomUserDetails(dto)
}
}
주요기능
DB에서 사용자 조회 (UserRepository 이용)
찾은 User 엔티티를 CustomUserDto로 변환
Spring Security의 UserDetails 객체(CustomUserDetails)로 반환
CustomUserDto.kt
package com.contact.management.security
//내부에서 사용하는 dto
import com.contact.management.entity.RoleType
import com.contact.management.entity.User
data class CustomUserDto(
val id: Long?= null,
var password: String,
val name: String,
val age: Int,
val email: String,
val role: RoleType
) {
companion object {
// Factory method for creating CustomUserDto from User entity
fun from(user: User): CustomUserDto {
return CustomUserDto(
id = user.id,
password = user.password,
name = user.name,
age = user.age,
email = user.email,
role = user.role
)
}
}
}
SecurityConfig.kt
package com.contact.management.security
import com.contact.management.jwt.JwtAccessDeniedHandler
import com.contact.management.jwt.JwtAuthFilter
import com.contact.management.jwt.JwtAuthenticationEntryPoint
import com.contact.management.jwt.JwtUtil
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.autoconfigure.security.servlet.PathRequest
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
@Configuration
@EnableWebSecurity
//@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true, prePostEnabled = true) //PreAuthorize 사용하기위한
class SecurityConfig(
private val customUserDetailsService: CustomUserDetailsService,
private val jwtUtil: JwtUtil,
private val accessDeniedHandler: JwtAccessDeniedHandler,
private val authenticationEntryPoint: JwtAuthenticationEntryPoint
) {
companion object {
private val AUTH_WHITELIST = arrayOf(
"/api/users/**", // Allows unrestricted access to this endpoint
)
}
@Bean
@ConditionalOnProperty(name = ["spring.h2.console.enabled"], havingValue = "true")
fun configureH2ConsoleEnable(): WebSecurityCustomizer {
return WebSecurityCustomizer { web -> web.ignoring().requestMatchers(PathRequest.toH2Console()) }
}
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http
.csrf { it.disable() }
.cors { }
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
.authorizeHttpRequests {
it
.requestMatchers(*AUTH_WHITELIST).permitAll()
.requestMatchers("/api/test/users/**").hasAuthority("ROLE_USER") //USER 역할만 접근 허용
.requestMatchers("/api/test/admin/**").hasAuthority("ROLE_ADMIN") // ADMIN 역할만 접근 허용
.anyRequest().authenticated()
}
.exceptionHandling {
it.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler)
}
.addFilterBefore(JwtAuthFilter(customUserDetailsService, jwtUtil), UsernamePasswordAuthenticationFilter::class.java)
.formLogin { it.disable() }
.httpBasic { it.disable() }
return http.build()
}
@Bean
fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()
}
주요기능
JWT 기반 인증 적용 (JwtAuthFilter 추가)
세션 사용 안 함 (STATELESS 설정)
CSRF 및 기본 로그인 기능 비활성화
예외 처리 설정 (JwtAccessDeniedHandler, JwtAuthenticationEntryPoint)
H2 콘솔 보안 설정 (ConditionalOnProperty)
비밀번호 암호화 (BCryptPasswordEncoder)
주요설정
CSRF 보호 비활성화 (csrf { it.disable() })
JWT 기반 인증에서는 CSRF 토큰이 필요하지 않음.
CORS 설정 (cors { })
기본 CORS 설정 (필요시 CorsFilter 추가 가능).
세션 미사용 (SessionCreationPolicy.STATELESS)
JWT 기반 인증 방식에서는 세션을 사용하지 않음.
기본 로그인 방식 비활성화
formLogin { it.disable() } → 기본 폼 로그인 X.
httpBasic { it.disable() } → HTTP Basic 인증 X.
Spring Security 인증흐름
1. 사용자 로그인 요청
/api/users/login 엔드포인트로 로그인 요청 (AUTH_WHITELIST에 포함됨).
2. 로그인 성공 시 JWT 토큰 발급.
요청 헤더에 JWT 포함
클라이언트는 이후 요청마다 Authorization: Bearer <JWT> 헤더를 포함하여 보냄.
3. JwtAuthFilter에서 JWT 검증
JWT가 유효하면 SecurityContext에 사용자 정보 저장.
4. 요청 인증 처리
SecurityFilterChain에서 설정한 대로 인증이 필요한 요청인지 확인 후 처리.
UserController.kt
package com.contact.management.controller
import com.contact.management.dto.LoginRequest
import com.contact.management.dto.SignUpRequest
import com.contact.management.dto.UserResponse
import com.contact.management.dto.UserUpdateRequest
import com.contact.management.service.QuerydslUserService
import com.contact.management.service.UserService
import jakarta.validation.Valid
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.web.bind.annotation.*
import kotlin.math.log
@RestController
@RequestMapping("/api/users")
class UserController(
private val userService: UserService
,private val querydslUserService: QuerydslUserService
) {
@GetMapping
fun getUsers(): ResponseEntity<List<UserResponse>> {
val users = userService.getAllUsers()
return ResponseEntity.ok(users)
}
@GetMapping("/{id}")
fun getUser(@PathVariable id: Long): ResponseEntity<UserResponse> {
return ResponseEntity.ok(userService.getUserById(id))
}
@PostMapping
fun createUser(@Valid @RequestBody request: SignUpRequest): ResponseEntity<UserResponse> {
val createdUser = userService.createdUser(request)
return ResponseEntity
.status(HttpStatus.CREATED) // 201 Created
.header(HttpHeaders.LOCATION, "/api/users/${createdUser.id}") // 생성된 리소스의 URI 반환
.body(createdUser)
}
@PutMapping("/{id}")
fun updateUser(@PathVariable id: Long,@Valid @RequestBody request: UserUpdateRequest): ResponseEntity<UserResponse> {
val updatedUser = userService.updateUser(id, request)
return ResponseEntity.ok(updatedUser)
}
@DeleteMapping("/{id}")
fun deleteUser(@PathVariable id: Long): ResponseEntity<Void> {
userService.deleteUser(id)
return ResponseEntity.noContent().build() // 204 No Content
}
@GetMapping("/paged")
fun getUsersWithPaging(pageable: Pageable, @RequestParam searchVal: String?): Page<UserResponse> {
// 페이징된 사용자 목록 반환
return querydslUserService.getUsersWithPaging(pageable, searchVal)
}
@PostMapping("/login")
fun login(@Valid @RequestBody loginRequest: LoginRequest): ResponseEntity<String>{
val token = userService.login(loginRequest)
return ResponseEntity.ok(token)
}
}
/login 추가
UserService.kt
package com.contact.management.service
import com.contact.management.dto.LoginRequest
import com.contact.management.dto.SignUpRequest
import com.contact.management.dto.UserResponse
import com.contact.management.dto.UserUpdateRequest
import com.contact.management.entity.QUser
import com.contact.management.entity.User
import com.contact.management.exception.CommonException
import com.contact.management.exception.CommonExceptionCode.EMAIL_ALREADY_EXISTS
import com.contact.management.exception.CommonExceptionCode.USER_NOT_FOUND
import com.contact.management.jwt.JwtUtil
import com.contact.management.repository.UserRepository
import com.contact.management.security.CustomUserDto
import com.querydsl.jpa.impl.JPAQueryFactory
import org.springframework.data.domain.Page
import org.springframework.data.domain.PageImpl
import org.springframework.data.domain.Pageable
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@Service
@Transactional
class UserService(
private val userRepository: UserRepository
,private val queryFactory: JPAQueryFactory
,private val encoder: PasswordEncoder
,private val jwtUtil: JwtUtil
) {
@Transactional(readOnly = true)
fun getAllUsers() : List<UserResponse> {
return userRepository.findAll().map { UserResponse.from(it) }
}
@Transactional(readOnly = true)
fun getUserById(id: Long): UserResponse {
val user = userRepository.findById(id).orElseThrow{ CommonException(USER_NOT_FOUND) }
return UserResponse.from(user)
}
fun createdUser(request: SignUpRequest): UserResponse {
userRepository.findByEmail(request.email)?.let {
throw CommonException(EMAIL_ALREADY_EXISTS)
}
val user = userRepository.save(User.from(request, encoder))
return UserResponse.from(user)
}
// 사용자 업데이트
fun updateUser(id: Long, request: UserUpdateRequest): UserResponse{
val user = userRepository.findById(id).orElseThrow{ CommonException(USER_NOT_FOUND) }
user.update(request, encoder)
return UserResponse.from(user)
}
fun deleteUser(id: Long){
if(!userRepository.existsById(id)){
throw CommonException(USER_NOT_FOUND)
}
userRepository.deleteById(id)
}
@Transactional(readOnly = true)
fun getUsersWithPaging(pageable: Pageable): Page<UserResponse> {
val qUser = QUser.user
// JPAQueryFactory를 사용하여 쿼리 작성
val query = queryFactory
.selectFrom(qUser)
.orderBy(qUser.id.asc()) // ID 기준으로 오름차순 정렬 (필요에 따라 변경 가능)
// 실제 데이터 조회
val users = query
.offset(pageable.offset) // 페이지의 시작 인덱스
.limit(pageable.pageSize.toLong()) // 페이지 크기
.fetch() // 결과 가져오기
// 총 개수를 구하는 쿼리 작성 (countQuery)
val countQuery = queryFactory
.select(qUser.count()) // 총 개수 계산
.from(qUser)
val total = countQuery.fetchOne() ?: 0L // fetchOne()으로 단일 값 가져오기
// Page 객체로 반환
val userDtos = users.map { UserResponse.from(it) }
return PageImpl(userDtos, pageable, total)
}
@Transactional(readOnly = true)
fun login(request: LoginRequest): String {
val email = request.email
val password = request.password
val user = userRepository.findByEmail(email) ?: throw CommonException(USER_NOT_FOUND)
// 암호화된 password를 디코딩한 값과 입력한 패스워드 값이 다르면 예외를 던짐
if (!encoder.matches(password, user.password)) {
throw CommonException(USER_NOT_FOUND)
}
val info = CustomUserDto.from(user)
return jwtUtil.createAccessToken(info)
}
}
login 메소드 추가
로그인시 해당 계정이 존재하지않는경우 USER_NOT_FOUND
암호화된 패스워드를 디코딩한 값과 입력한 패스워드값이 다를경우 USER_NOT_FOUND ERROR
통과시 jwt access token 생성 후 return
결과
사용자 등록
사용자 로그인
token값 return
TestController.kt
package com.contact.management.controller
import com.contact.management.dto.LoginRequest
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController
@RestController
class TestController {
@GetMapping("/test")
fun test(): String{
return "ok";
}
@PostMapping("/api/test/user")
// @PreAuthorize("hasRole('ROLE_USER')")
fun userTest(@RequestBody loginRequest: LoginRequest): LoginRequest {
return loginRequest
}
@PostMapping("/api/test/admin")
// @PreAuthorize("hasRole('ROLE_ADMIN')")
fun adminTest(@RequestBody loginRequest: LoginRequest): LoginRequest {
return loginRequest
}
}
users 계정으로 /api/test/user 호출
user계정으로 /api/test/admin 호출
user 계정으로 admin api를 호출하였을때, 403이 나와야할것 같은데, 401이 나옴 원인 파악중..