Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[fix] QA 에러 수정 #173

Merged
merged 4 commits into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class WebSecurityConfig(
http
.csrf { c -> c.disable() }
.cors { c -> c.disable() }
// frontend error 처리 401, 403
.exceptionHandling {
e ->
e.authenticationEntryPoint(jwtAuthenticationEntryPoint)
Expand All @@ -50,6 +51,7 @@ class WebSecurityConfig(
c.requestMatchers("/users/eid").permitAll()
c.requestMatchers("/users/reissue").permitAll()
c.requestMatchers("/users/password-reset").permitAll()
c.requestMatchers("/users/phone/*/*").permitAll()
c.requestMatchers("/users/phone/*").permitAll()
c.requestMatchers("/users/email/search").permitAll()
c.requestMatchers("/users/password").permitAll()
Expand Down
1 change: 0 additions & 1 deletion src/main/kotlin/com/psr/psr/global/jwt/JwtFilter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ class JwtFilter(private val jwtUtils: JwtUtils, private val redisService: RedisS
request.setAttribute("exception", BaseResponse<Any>(e.baseResponseCode))
}
filterChain.doFilter(request, response);

}

/**
Expand Down
32 changes: 23 additions & 9 deletions src/main/kotlin/com/psr/psr/global/jwt/utils/JwtUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import io.jsonwebtoken.*
import io.jsonwebtoken.io.Decoders
import io.jsonwebtoken.security.Keys
import io.jsonwebtoken.security.SecurityException
import mu.KotlinLogging
import org.springframework.beans.factory.annotation.Value
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.Authentication
Expand Down Expand Up @@ -44,24 +43,30 @@ class JwtUtils(
* 토큰 생성
*/
fun createToken(authentication: Authentication, type: Type): TokenDto {
// authorities 생성
val authorities = authentication.authorities.stream()
.map { obj: GrantedAuthority -> obj.authority }
.collect(Collectors.joining(","))

val now = Date().time

// accessToken 생성
val accessToken: String = Jwts.builder()
.setSubject(authentication.name)
.claim(AUTHORIZATION_HEADER, authorities)
.setExpiration(Date(now + ACCESS_TOKEN_EXPIRE_TIME))
.signWith(key, SignatureAlgorithm.HS512)
.compact()

// refreshToken 생성
val refreshToken: String = Jwts.builder()
.setExpiration(Date(now + REFRESH_TOKEN_EXPIRE_TIME))
.signWith(key, SignatureAlgorithm.HS512)
.compact()

// redis 에 RefreshToken 저장
redisService.setValue(authentication.name, refreshToken, Duration.ofMillis(REFRESH_TOKEN_EXPIRE_TIME))
// return token
return TokenDto(BEARER_PREFIX + accessToken, BEARER_PREFIX + refreshToken, type.value)
}

Expand All @@ -72,15 +77,15 @@ class JwtUtils(
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token)
return true
} catch (e: SecurityException) {
} catch (e: SecurityException) { // 유효하지 않은 토큰 값인 경우
throw BaseException(BaseResponseCode.INVALID_TOKEN)
} catch (e: MalformedJwtException) {
} catch (e: MalformedJwtException) { // 잘못된 구조의 토큰인 경우
throw BaseException(BaseResponseCode.MALFORMED_TOKEN)
} catch (e: ExpiredJwtException) {
} catch (e: ExpiredJwtException) { // 토큰이 만료가 된 경우
throw BaseException(BaseResponseCode.EXPIRED_TOKEN)
} catch (e: UnsupportedJwtException) {
} catch (e: UnsupportedJwtException) { // 잘못된 형식의 토큰 값인 경우
throw BaseException(BaseResponseCode.UNSUPPORTED_TOKEN)
} catch (e: IllegalArgumentException) {
} catch (e: IllegalArgumentException) { // 토큰 값이 없는 경우
throw BaseException(BaseResponseCode.NULL_TOKEN)
}
}
Expand All @@ -90,15 +95,23 @@ class JwtUtils(
* 토큰 복호화
*/
fun getAuthentication(accessToken: String): Authentication {
// token에서 사용자 id 값 가져오기
val userId = getUserIdFromJWT(accessToken)
// user 불러오기
val userDetails: UserDetails = userDetailsService.loadUserById(userId)
return UsernamePasswordAuthenticationToken(userDetails, null, userDetails.authorities)
}

/**
* token 에서 사용자 id 값 불러오기
*/
fun getUserIdFromJWT(token: String): Long {
return parseClaims(token).subject.toLong()
}

/**
* token 내 저장된 정보 불러오기
*/
private fun parseClaims(token: String): Claims {
return try {
Jwts.parserBuilder()
Expand All @@ -112,7 +125,7 @@ class JwtUtils(
}

/**
* 토큰 만료
* blacklist 토큰 만료
*/
fun expireToken(token: String, status: String){
val accessToken = token.replace(BEARER_PREFIX, "")
Expand All @@ -121,9 +134,10 @@ class JwtUtils(
}

/**
* Token 남은 시간 return
* black list를 위한 유효시간 없앤 후 return
*/
private fun getExpiration(token: String): Long {
// 유효시간 불러오기
val expiration = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).body.expiration
val now = Date().time
return (expiration.time - now)
Expand All @@ -137,7 +151,7 @@ class JwtUtils(
}

/**
* check Token in Redis DB
* Redis DB 에서 refreshToken 확인
*/
fun validateRefreshToken(userId: Long, refreshToken: String) {
val redisToken = redisService.getValue(userId.toString())
Expand Down
20 changes: 16 additions & 4 deletions src/main/kotlin/com/psr/psr/user/controller/UserController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ class UserController(
/**
* 사용자 프로필 변경하기
*/
@PostMapping("/profile")
fun postProfile(@AuthenticationPrincipal userAccount: UserAccount, @RequestBody @Validated profileReq: ProfileReq) : BaseResponse<Any> {
userService.postProfile(userAccount.getUser(), profileReq)
@PatchMapping("/profile")
fun patchProfile(@AuthenticationPrincipal userAccount: UserAccount, @RequestBody @Validated profileReq: ProfileReq) : BaseResponse<Any> {
userService.patchProfile(userAccount.getUser(), profileReq)
return BaseResponse(BaseResponseCode.SUCCESS)
}

Expand Down Expand Up @@ -146,14 +146,26 @@ class UserController(
}

/**
* 휴대폰번호 유효
* 휴대폰번호 전송
*/
@PostMapping("/phone/check")
fun checkValidPhone(@RequestBody @Validated validPhoneReq: ValidPhoneReq) : BaseResponse<Any>{
userService.checkValidPhone(validPhoneReq)
return BaseResponse(BaseResponseCode.SUCCESS)
}

/**
* 회원가입을 위한 휴대폰번호 전송
*/
@PostMapping("/phone/check/signup")
fun checkValidPhoneForSignUp(@RequestBody @Validated validPhoneReq: ValidPhoneReq) : BaseResponse<Any>{
// 이미 있는 휴대폰 번호인지 확인
if(userService.checkDuplicatePhone(validPhoneReq.phone)) throw BaseException(BaseResponseCode.EXISTS_PHONE)
// 휴대폰 번호 전송
userService.checkValidPhone(validPhoneReq)
return BaseResponse(BaseResponseCode.SUCCESS)
}

/**
* 휴대폰 인증번호 조회
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ data class ProfileReq(
message = "한글, 영어, 숫자만 입력해주세요. (10글자)"
)
val nickname: String,
val profileImgUrl: String? = null
val imgUrl: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import java.util.*
interface UserRepository: JpaRepository<User, Long> {
fun findByIdAndStatus(id: Long, status: String) : User?
fun existsByNicknameAndStatus(nickname: String, status: String): Boolean
fun existsByNicknameAndStatusAndIdNot(nickname: String, status: String, userId: Long) : Boolean
fun existsByPhoneAndStatus(phone: String, status: String): Boolean
fun existsByEmailAndStatus(nickname: String, status: String): Boolean
fun findByEmailAndStatus(email:String, status: String): Optional<User>
Expand Down
55 changes: 42 additions & 13 deletions src/main/kotlin/com/psr/psr/user/service/UserService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class UserService(
// 회원가입
@Transactional
fun signUp(signUpReq: SignUpReq): TokenDto {
// 중복 값 확인
// 카테고리 내 중복 값 확인
val categoryCheck = signUpReq.interestList.groupingBy { it }.eachCount().any { it.value > 1 }
if(categoryCheck) throw BaseException(INVALID_USER_INTEREST_COUNT)
// category 의 사이즈 확인
Expand All @@ -87,32 +87,34 @@ class UserService(
if(userRepository.existsByPhoneAndStatus(signUpReq.phone, ACTIVE_STATUS)) throw BaseException(EXISTS_PHONE)
if(userRepository.existsByNicknameAndStatus(signUpReq.nickname, ACTIVE_STATUS)) throw BaseException(EXISTS_NICKNAME)


// 암호화되지 않은 password 값 저장
// 암호화되지 않은 password 값 변수에 저장
val password = signUpReq.password
// password 암호화
val encodedPassword = passwordEncoder.encode(signUpReq.password)
signUpReq.password = encodedPassword
// user 저장
val user = userRepository.save(User.toEntity(signUpReq))
userInterestRepository.saveAll(UserInterest.toInterestListEntity(user, signUpReq))

// 사업자인경우
if (user.type == Type.ENTREPRENEUR){
if(signUpReq.entreInfo == null) throw BaseException(NOT_EMPTY_EID)
businessInfoRepository.save(BusinessInfo.toBusinessEntity(user, signUpReq))
}

// token 생성
return createToken(user, password)
}

// 로그인
@Transactional
fun login(loginReq: LoginReq) : TokenDto{
// 이메일이 일치 확인
val user = userRepository.findByEmailAndStatus(loginReq.email, ACTIVE_STATUS).orElseThrow{BaseException(NOT_EXIST_EMAIL)}
// 비밀번호 일치 확인
if(!passwordEncoder.matches(loginReq.password, user.password)) throw BaseException(INVALID_PASSWORD)
// 알림을 위한 사용자 디바이스 토큰 저장
if (loginReq.deviceToken != null) user.deviceToken = loginReq.deviceToken

// token 생성
return createToken(user, loginReq.password)
}

Expand All @@ -121,6 +123,11 @@ class UserService(
return userRepository.existsByNicknameAndStatus(nickname, ACTIVE_STATUS)
}

// 휴대폰번호 중복체크
fun checkDuplicatePhone(phone: String): Boolean{
return userRepository.existsByPhoneAndStatus(phone, ACTIVE_STATUS)
}

// token 생성 extract method
private fun createToken(user: User, password: String): TokenDto {
val authenticationToken = UsernamePasswordAuthenticationToken(user.id.toString(), password)
Expand All @@ -135,17 +142,21 @@ class UserService(

// 사용자 프로필 변경
@Transactional
fun postProfile(user: User, profileReq: ProfileReq) {
fun patchProfile(user: User, profileReq: ProfileReq) {
// 닉네임이 변경이 되었으면
if(user.nickname != profileReq.nickname) {
if(userRepository.existsByNicknameAndStatus(profileReq.nickname, ACTIVE_STATUS)) throw BaseException(EXISTS_NICKNAME)
if(userRepository.existsByNicknameAndStatusAndIdNot(profileReq.nickname, ACTIVE_STATUS, user.id!!)) throw BaseException(EXISTS_NICKNAME)
user.nickname = profileReq.nickname
}
if(user.imgUrl != profileReq.profileImgUrl) user.imgUrl = profileReq.profileImgUrl
// 프로필 이미지가 변경이 되었으면
if(user.imgUrl != profileReq.imgUrl) user.imgUrl = profileReq.imgUrl
// 변경된 값 저장
userRepository.save(user)
}

// 토큰 자동 토큰 만료 및 RefreshToken 삭제
fun blackListToken(user: User, request: HttpServletRequest, loginStatus: String) {
// header 에서 token 불러오기
val token = getHeaderAuthorization(request)
// 토큰 만료
jwtUtils.expireToken(token, loginStatus);
Expand All @@ -161,18 +172,26 @@ class UserService(

// 사업자 정보 확인 API
fun validateEid(userEidReq: UserEidReq) {
// 공공데이터 Open API 사용자 정보 불러오기
val eidInfo = getEidInfo(userEidReq)
// 검증을 할 수 없는 경우 (사업자를 찾지 못한 경우)
if(eidInfo.valid_cnt == null) throw BaseException(NOT_FOUND_EID)
// 정상 사업자가 아닌 경우
if(eidInfo.data[0].status!!.b_stt_cd != PAY_STATUS) throw BaseException(INVALID_EID)
}

// 공공데이터포털에서 사용자 불러오기
private fun getEidInfo(userEidReq: UserEidReq): BusinessListRes {
val url = EID_URL + serviceKey
// open api 정보 전달 객체 형태로 변경
val businesses = BusinessListReq.toUserEidList(userEidReq)
// json 형식으로 변경
val json = ObjectMapper().writeValueAsString(businesses)
// -> 이미 인코딩이 되어 있는 serviceKey이 재인코딩이 되지 않기 위해
val factory = DefaultUriBuilderFactory(url)
factory.encodingMode = DefaultUriBuilderFactory.EncodingMode.VALUES_ONLY;
factory.encodingMode = DefaultUriBuilderFactory.EncodingMode.VALUES_ONLY

// open api 접근
return WebClient.builder()
.uriBuilderFactory(factory)
.baseUrl(url)
Expand Down Expand Up @@ -227,6 +246,7 @@ class UserService(
// 비밀번호 재설정
@Transactional
fun resetPassword(passwordReq: ResetPasswordReq) {
// 사용자 이메일 있는 경우
val user = userRepository.findByEmailAndStatus(passwordReq.email, ACTIVE_STATUS).orElseThrow{BaseException(NOT_EXIST_EMAIL)}
if(user.phone != passwordReq.phone) throw BaseException(INVALID_PHONE)
if(passwordEncoder.matches(passwordReq.password, user.password)) throw BaseException(DUPLICATE_PASSWORD)
Expand All @@ -239,9 +259,13 @@ class UserService(
// 관심 목록 변경
@Transactional
fun patchWatchLists(user: User, userInterestListReq: UserInterestListDto) {
// 카테고리 내 중복 값 확인
val reqLists = userInterestListReq.interestList!!.map { i -> Category.getCategoryByValue(i.category) }
// category 의 사이즈 확인
if(reqLists.isEmpty() || reqLists.size > 3) throw BaseException(INVALID_USER_INTEREST_COUNT)
// 사용자 관심 목록 불러오기
val userWatchLists = userInterestRepository.findByUserAndStatus(user, ACTIVE_STATUS)
// string list로 변경
val categoryLists = userWatchLists.map { c -> c.category }

// 사용자 관심 목록에 최근 추가한 리스트(REQUEST) 관심 목록이 없다면? => 저장
Expand All @@ -258,7 +282,6 @@ class UserService(
// 최근 추가한 리스트(REQUEST)에 사용자 관심 목록이 없다면? => 삭제
userWatchLists.stream()
.forEach { interest ->
// todo: cascade 적용 후 모두 삭제 되었는지 확인 필요
if(!reqLists.contains(interest.category)) userInterestRepository.delete(interest)
}
}
Expand All @@ -273,16 +296,18 @@ class UserService(
fun checkValidPhone(validPhoneReq: ValidPhoneReq) {
val time = System.currentTimeMillis()
val url = FIRST_URL + MIDDLE_URL + serviceId + FINAL_URL
// -> 이미 인코딩이 되어 있는 serviceKey이 재인코딩이 되지 않기 위해
val factory = DefaultUriBuilderFactory(url)
factory.encodingMode = DefaultUriBuilderFactory.EncodingMode.VALUES_ONLY
// 인증번호 생성
val smsKey = createSmsKey()

// open api 접근
WebClient.builder()
.uriBuilderFactory(factory)
.baseUrl(url)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(TIMESTAMP_HEADER, time.toString())
.defaultHeader(ACCESS_KEY_HEADER, accessKey)
.defaultHeader(ACCESS_KEY_HEADER, accessKey) // accessKey
.defaultHeader(SIGNATURE_HEADER, makeSignature(time))
.build().post()
.bodyValue(SMSReq.toSMSReqDto(validPhoneReq, smsKey, sendPhone))
Expand All @@ -292,11 +317,13 @@ class UserService(
}
.bodyToMono(String::class.java)
.block()
// 휴대폰 smsKey 만료시간
smsUtils.createSmsKey(validPhoneReq.phone, smsKey)
}

// 휴대폰 인증번호 조회
fun checkValidSmsKey(phone: String, smsKey: String) {
// 휴대폰 인증 번호 불러오기
val sms = smsUtils.getSmsKey(phone)
// 인증코드가 같지 않은 경우 예외처리 발생
if(sms != smsKey) throw BaseException(INVALID_SMS_KEY)
Expand All @@ -307,6 +334,7 @@ class UserService(
// 인증번호 확인
checkValidSmsKey(findIdPwReq.phone, findIdPwReq.smsKey)
val user: User = userRepository.findByNameAndPhoneAndStatus(findIdPwReq.name!!, findIdPwReq.phone, ACTIVE_STATUS) ?: throw BaseException(NOT_FOUND_USER)
// 사용자 이메일 전달
return EmailRes.toEmailResDto(user)
}

Expand All @@ -325,7 +353,7 @@ class UserService(
return PostNotiRes.toDto(user.notification)
}

// signature
// signature For post sms
private fun makeSignature(time: Long): String {
val message = StringBuilder()
.append(METHOD)
Expand All @@ -345,6 +373,7 @@ class UserService(
return Base64.encodeBase64String(rawHmac)
}

// 인증번호 생성
fun createSmsKey() : String{
return RandomStringUtils.random(5, false, true);
}
Expand Down
Loading