diff --git a/src/main/kotlin/com/psr/psr/global/config/WebSecurityConfig.kt b/src/main/kotlin/com/psr/psr/global/config/WebSecurityConfig.kt index 498a2c3..529a76a 100644 --- a/src/main/kotlin/com/psr/psr/global/config/WebSecurityConfig.kt +++ b/src/main/kotlin/com/psr/psr/global/config/WebSecurityConfig.kt @@ -33,6 +33,7 @@ class WebSecurityConfig( http .csrf { c -> c.disable() } .cors { c -> c.disable() } + // frontend error 처리 401, 403 .exceptionHandling { e -> e.authenticationEntryPoint(jwtAuthenticationEntryPoint) diff --git a/src/main/kotlin/com/psr/psr/global/jwt/JwtFilter.kt b/src/main/kotlin/com/psr/psr/global/jwt/JwtFilter.kt index 2243dc9..55b9004 100644 --- a/src/main/kotlin/com/psr/psr/global/jwt/JwtFilter.kt +++ b/src/main/kotlin/com/psr/psr/global/jwt/JwtFilter.kt @@ -39,7 +39,6 @@ class JwtFilter(private val jwtUtils: JwtUtils, private val redisService: RedisS request.setAttribute("exception", BaseResponse(e.baseResponseCode)) } filterChain.doFilter(request, response); - } /** diff --git a/src/main/kotlin/com/psr/psr/global/jwt/utils/JwtUtils.kt b/src/main/kotlin/com/psr/psr/global/jwt/utils/JwtUtils.kt index b6dfafa..fb8635a 100644 --- a/src/main/kotlin/com/psr/psr/global/jwt/utils/JwtUtils.kt +++ b/src/main/kotlin/com/psr/psr/global/jwt/utils/JwtUtils.kt @@ -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 @@ -44,11 +43,14 @@ 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) @@ -56,12 +58,15 @@ class JwtUtils( .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) } @@ -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) } } @@ -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() @@ -112,7 +125,7 @@ class JwtUtils( } /** - * 토큰 만료 + * blacklist 토큰 만료 */ fun expireToken(token: String, status: String){ val accessToken = token.replace(BEARER_PREFIX, "") @@ -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) @@ -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()) diff --git a/src/main/kotlin/com/psr/psr/user/controller/UserController.kt b/src/main/kotlin/com/psr/psr/user/controller/UserController.kt index bbb6206..0bb3d5b 100644 --- a/src/main/kotlin/com/psr/psr/user/controller/UserController.kt +++ b/src/main/kotlin/com/psr/psr/user/controller/UserController.kt @@ -61,8 +61,8 @@ class UserController( * 사용자 프로필 변경하기 */ @PostMapping("/profile") - fun postProfile(@AuthenticationPrincipal userAccount: UserAccount, @RequestBody @Validated profileReq: ProfileReq) : BaseResponse { - userService.postProfile(userAccount.getUser(), profileReq) + fun patchProfile(@AuthenticationPrincipal userAccount: UserAccount, @RequestBody @Validated profileReq: ProfileReq) : BaseResponse { + userService.patchProfile(userAccount.getUser(), profileReq) return BaseResponse(BaseResponseCode.SUCCESS) } diff --git a/src/main/kotlin/com/psr/psr/user/dto/request/ProfileReq.kt b/src/main/kotlin/com/psr/psr/user/dto/request/ProfileReq.kt index 76bb518..4a308b6 100644 --- a/src/main/kotlin/com/psr/psr/user/dto/request/ProfileReq.kt +++ b/src/main/kotlin/com/psr/psr/user/dto/request/ProfileReq.kt @@ -10,5 +10,5 @@ data class ProfileReq( message = "한글, 영어, 숫자만 입력해주세요. (10글자)" ) val nickname: String, - val profileImgUrl: String? = null + val imgUrl: String ) \ No newline at end of file diff --git a/src/main/kotlin/com/psr/psr/user/service/UserService.kt b/src/main/kotlin/com/psr/psr/user/service/UserService.kt index 6ecc5a7..0242332 100644 --- a/src/main/kotlin/com/psr/psr/user/service/UserService.kt +++ b/src/main/kotlin/com/psr/psr/user/service/UserService.kt @@ -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 의 사이즈 확인 @@ -87,8 +87,7 @@ 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) @@ -96,13 +95,11 @@ class UserService( // 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) } @@ -110,9 +107,14 @@ class UserService( // 로그인 @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) } @@ -135,17 +137,22 @@ class UserService( // 사용자 프로필 변경 @Transactional - fun postProfile(user: User, profileReq: ProfileReq) { + fun patchProfile(user: User, profileReq: ProfileReq) { + // 닉네임이 변경이 되었으면 if(user.nickname != profileReq.nickname) { + // todo: 코드 변경 필요 -> 자기 자신 닉네임 제외 if(userRepository.existsByNicknameAndStatus(profileReq.nickname, ACTIVE_STATUS)) 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); @@ -161,18 +168,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) @@ -227,6 +242,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) @@ -239,9 +255,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) 관심 목록이 없다면? => 저장 @@ -258,7 +278,6 @@ class UserService( // 최근 추가한 리스트(REQUEST)에 사용자 관심 목록이 없다면? => 삭제 userWatchLists.stream() .forEach { interest -> - // todo: cascade 적용 후 모두 삭제 되었는지 확인 필요 if(!reqLists.contains(interest.category)) userInterestRepository.delete(interest) } } @@ -273,16 +292,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)) @@ -292,11 +313,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) @@ -307,6 +330,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) } @@ -325,7 +349,7 @@ class UserService( return PostNotiRes.toDto(user.notification) } - // signature + // signature For post sms private fun makeSignature(time: Long): String { val message = StringBuilder() .append(METHOD) @@ -345,6 +369,7 @@ class UserService( return Base64.encodeBase64String(rawHmac) } + // 인증번호 생성 fun createSmsKey() : String{ return RandomStringUtils.random(5, false, true); }