package me.wypark.blogbackend.domain.auth import me.wypark.blogbackend.api.dto.LoginRequest import me.wypark.blogbackend.api.dto.SignupRequest import me.wypark.blogbackend.api.dto.TokenDto import me.wypark.blogbackend.core.config.jwt.JwtProvider import me.wypark.blogbackend.domain.user.Member import me.wypark.blogbackend.domain.user.MemberRepository import me.wypark.blogbackend.domain.user.Role import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @Service @Transactional(readOnly = true) class AuthService( private val memberRepository: MemberRepository, private val passwordEncoder: PasswordEncoder, private val authenticationManagerBuilder: AuthenticationManagerBuilder, private val jwtProvider: JwtProvider, private val refreshTokenRepository: RefreshTokenRepository, private val emailService: EmailService, // ๐Ÿ‘‡ ์ถ”๊ฐ€: DB์—์„œ ์œ ์ € ์ •๋ณด๋ฅผ ๋‹ค์‹œ ๋กœ๋“œํ•˜๊ธฐ ์œ„ํ•ด ํ•„์š” private val userDetailsService: UserDetailsService ) { /** * ํšŒ์›๊ฐ€์ž… */ @Transactional fun signup(request: SignupRequest) { if (memberRepository.existsByEmail(request.email)) { throw IllegalArgumentException("์ด๋ฏธ ๊ฐ€์ž…๋œ ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค.") } if (memberRepository.existsByNickname(request.nickname)) { throw IllegalArgumentException("์ด๋ฏธ ์‚ฌ์šฉ ์ค‘์ธ ๋‹‰๋„ค์ž„์ž…๋‹ˆ๋‹ค.") } val member = Member( email = request.email, password = passwordEncoder.encode(request.password), nickname = request.nickname, role = Role.ROLE_USER, isVerified = false ) memberRepository.save(member) emailService.sendVerificationCode(request.email) } /** * ๋กœ๊ทธ์ธ */ @Transactional fun login(request: LoginRequest): TokenDto { val member = memberRepository.findByEmail(request.email) ?: throw IllegalArgumentException("๊ฐ€์ž…๋˜์ง€ ์•Š์€ ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค.") // ๋น„๋ฐ€๋ฒˆํ˜ธ ์ฒดํฌ if (!passwordEncoder.matches(request.password, member.password)) { throw IllegalArgumentException("๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.") } // ์ด๋ฉ”์ผ ์ธ์ฆ ์—ฌ๋ถ€ ์ฒดํฌ if (!member.isVerified) { throw IllegalStateException("์ด๋ฉ”์ผ ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.") } // 1. ID/PW ๊ธฐ๋ฐ˜์˜ ์ธ์ฆ ํ† ํฐ ์ƒ์„ฑ val authenticationToken = UsernamePasswordAuthenticationToken(request.email, request.password) // 2. ์‹ค์ œ ๊ฒ€์ฆ (์‚ฌ์šฉ์ž ๋น„๋ฐ€๋ฒˆํ˜ธ ์ฒดํฌ) val authentication = authenticationManagerBuilder.`object`.authenticate(authenticationToken) // 3. ์ธ์ฆ ์ •๋ณด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ JWT ํ† ํฐ ์ƒ์„ฑ val tokenDto = jwtProvider.generateTokenDto(authentication) // 4. RefreshToken Redis ์ €์žฅ (RTR: ๊ธฐ์กด ํ† ํฐ ๋ฎ์–ด์“ฐ๊ธฐ) refreshTokenRepository.save(authentication.name, tokenDto.refreshToken) return tokenDto } /** * ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ (RTR ์ ์šฉ) */ @Transactional fun reissue(accessToken: String, refreshToken: String): TokenDto { // 1. ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ๊ฒ€์ฆ (๋งŒ๋ฃŒ ์—ฌ๋ถ€, ์œ„๋ณ€์กฐ ์—ฌ๋ถ€) if (!jwtProvider.validateToken(refreshToken)) { throw IllegalArgumentException("์œ ํšจํ•˜์ง€ ์•Š์€ Refresh Token์ž…๋‹ˆ๋‹ค.") } // 2. ์•ก์„ธ์Šค ํ† ํฐ์—์„œ User ID(Email) ๊ฐ€์ ธ์˜ค๊ธฐ // (์ฃผ์˜: ์—ฌ๊ธฐ์„œ authentication.principal์€ CustomUserDetails๊ฐ€ ์•„๋‹ ์ˆ˜ ์žˆ์Œ) val tempAuthentication = jwtProvider.getAuthentication(accessToken) // 3. Redis์—์„œ ์ €์žฅ๋œ Refresh Token ๊ฐ€์ ธ์˜ค๊ธฐ val savedRefreshToken = refreshTokenRepository.findByEmail(tempAuthentication.name) ?: throw IllegalArgumentException("๋กœ๊ทธ์•„์›ƒ ๋œ ์‚ฌ์šฉ์ž์ž…๋‹ˆ๋‹ค.") // 4. ํ† ํฐ ์ผ์น˜ ์—ฌ๋ถ€ ํ™•์ธ (์žฌ์‚ฌ์šฉ ๋ฐฉ์ง€) if (savedRefreshToken != refreshToken) { refreshTokenRepository.delete(tempAuthentication.name) throw IllegalArgumentException("ํ† ํฐ ์ •๋ณด๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.") } // โœจ 5. [์ˆ˜์ •๋จ] DB์—์„œ ์œ ์ € ์ •๋ณด(CustomUserDetails) ๋‹ค์‹œ ๋กœ๋“œ // JwtProvider.generateTokenDto()๊ฐ€ CustomUserDetails๋ฅผ ํ•„์š”๋กœ ํ•˜๋ฏ€๋กœ ํ•„์ˆ˜ val userDetails = userDetailsService.loadUserByUsername(tempAuthentication.name) // โœจ ๋กœ๋“œํ•œ userDetails๋กœ ์ƒˆ๋กœ์šด Authentication ์ƒ์„ฑ val newAuthentication = UsernamePasswordAuthenticationToken( userDetails, null, userDetails.authorities ) // 6. ์ƒˆ ํ† ํฐ ์ƒ์„ฑ (Rotation) val newTokenDto = jwtProvider.generateTokenDto(newAuthentication) // 7. Redis ์—…๋ฐ์ดํŠธ (ํ•œ๋ฒˆ ์“ด ํ† ํฐ ํ๊ธฐ -> ์ƒˆ ํ† ํฐ ์ €์žฅ) refreshTokenRepository.save(newAuthentication.name, newTokenDto.refreshToken) return newTokenDto } /** * ๋กœ๊ทธ์•„์›ƒ */ @Transactional fun logout(email: String) { refreshTokenRepository.delete(email) } // 3. ์ด๋ฉ”์ผ ์ธ์ฆ ํ™•์ธ @Transactional fun verifyEmail(email: String, code: String) { val member = memberRepository.findByEmail(email) ?: throw IllegalArgumentException("์กด์žฌํ•˜์ง€ ์•Š๋Š” ํšŒ์›์ž…๋‹ˆ๋‹ค.") if (member.isVerified) { throw IllegalArgumentException("์ด๋ฏธ ์ธ์ฆ๋œ ํšŒ์›์ž…๋‹ˆ๋‹ค.") } // ์ฝ”๋“œ ๊ฒ€์ฆ if (!emailService.verifyCode(email, code)) { throw IllegalArgumentException("์ธ์ฆ ์ฝ”๋“œ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š๊ฑฐ๋‚˜ ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.") } // ์ธ์ฆ ์ƒํƒœ ์—…๋ฐ์ดํŠธ member.verify() } }