Skip to content

Commit

Permalink
Merge pull request #2 from 28th-meetup/feat/auth
Browse files Browse the repository at this point in the history
feat: 인증/인가 추가
  • Loading branch information
eckrin committed Oct 15, 2023
2 parents 4c14a98 + cdd13f4 commit c8f1363
Show file tree
Hide file tree
Showing 34 changed files with 1,066 additions and 1 deletion.
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
//spring web
implementation 'org.springframework.boot:spring-boot-starter-web'
//spring security
implementation 'org.springframework.boot:spring-boot-starter-security'
//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'
//javax validation
implementation 'javax.validation:validation-api:2.0.1.Final'
//swagger (spring 3.x버전이라 springfox 적용불가, springdoc 중에서 호환되는 종속성 사용)
Expand Down
47 changes: 47 additions & 0 deletions src/main/java/com/kusitms/jipbap/auth/AuthController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.kusitms.jipbap.auth;

import com.kusitms.jipbap.auth.dto.*;
import com.kusitms.jipbap.security.Auth;
import com.kusitms.jipbap.security.AuthInfo;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

@RestController
@RequiredArgsConstructor
@RequestMapping("/auth")
public class AuthController {

private final AuthService authService;

@Operation(summary = "일반 회원 가입")
@PostMapping("/signUp")
@ResponseStatus(HttpStatus.OK)
public void signUp(@Valid @RequestBody SignUpRequestDto dto) {
authService.signUp(dto);
}

@Operation(summary = "로그인")
@PostMapping("/signIn")
@ResponseStatus(HttpStatus.OK)
public SignInResponseDto signIn(@Valid @RequestBody SignInRequestDto dto) {
return authService.signIn(dto.getEmail(), dto.getPassword());
}

@Operation(summary = "카카오 회원 가입(로그인)")
@PostMapping("/kakao")
@ResponseStatus(HttpStatus.OK)
public SignInResponseDto kakaoVerification(@RequestBody KakaoSignInRequestDto dto) {
return authService.kakaoAutoSignIn(authService.getKakaoProfile(dto.getToken()));
}

@Operation(summary = "액세스 토큰 재발급 - 헤더에 refreshToken 정보 포함하여 요청")
@PostMapping("/reissue")
public ReissueResponseDto reissue(@Auth AuthInfo authInfo) {
return authService.reissue(authInfo.getEmail(), authInfo.getToken());
}

}
50 changes: 50 additions & 0 deletions src/main/java/com/kusitms/jipbap/auth/AuthExceptionHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.kusitms.jipbap.auth;

import com.kusitms.jipbap.auth.exception.*;
import com.kusitms.jipbap.common.response.ErrorCode;
import com.kusitms.jipbap.common.response.ErrorResponse;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice
public class AuthExceptionHandler {
@ExceptionHandler(EmailExistsException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse<?> handleEmailExistsException(EmailExistsException e, HttpServletRequest request) {
log.warn("Auth-001> 요청 URI: " + request.getRequestURI() + ", 에러 메세지: " + e.getMessage());
return new ErrorResponse<>(ErrorCode.EMAIL_EXISTS_ERROR);
}

@ExceptionHandler(InvalidEmailException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse<?> handleInvalidEmailException(InvalidEmailException e, HttpServletRequest request) {
log.warn("Auth-002> 요청 URI: " + request.getRequestURI() + ", 에러 메세지: " + e.getMessage());
return new ErrorResponse<>(ErrorCode.INVALID_EMAIL_ERROR);
}

@ExceptionHandler(InvalidPasswordException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse<?> handleInvalidPasswordException(InvalidPasswordException e, HttpServletRequest request) {
log.warn("Auth-003> 요청 URI: " + request.getRequestURI() + ", 에러 메세지: " + e.getMessage());
return new ErrorResponse<>(ErrorCode.INVALID_PASSWORD_ERROR);
}

@ExceptionHandler(RefreshTokenNotFoundException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse<?> handleRefreshTokenNotFoundException(RefreshTokenNotFoundException e, HttpServletRequest request) {
log.warn("Auth-004> 요청 URI: " + request.getRequestURI() + ", 에러 메세지: " + e.getMessage());
return new ErrorResponse<>(ErrorCode.INVALID_REFRESH_TOKEN_ERROR);
}

@ExceptionHandler(JsonException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse<?> handleJsonException(JsonException e, HttpServletRequest request) {
log.warn("Auth-005> 요청 URI: " + request.getRequestURI() + ", 에러 메세지: " + e.getMessage());
return new ErrorResponse<>(ErrorCode.INTERNAL_SERVER_ERROR);
}
}
185 changes: 185 additions & 0 deletions src/main/java/com/kusitms/jipbap/auth/AuthService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package com.kusitms.jipbap.auth;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kusitms.jipbap.auth.dto.KakaoProfileDto;
import com.kusitms.jipbap.auth.dto.ReissueResponseDto;
import com.kusitms.jipbap.auth.dto.SignInResponseDto;
import com.kusitms.jipbap.auth.dto.SignUpRequestDto;
import com.kusitms.jipbap.auth.exception.*;
import com.kusitms.jipbap.security.jwt.JwtTokenProvider;
import com.kusitms.jipbap.security.jwt.TokenInfo;
import com.kusitms.jipbap.user.Role;
import com.kusitms.jipbap.user.User;
import com.kusitms.jipbap.user.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import java.lang.reflect.Member;

@Slf4j
@Service
@RequiredArgsConstructor
public class AuthService {

private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider tokenProvider;

@Value("${secret.pwd}")
private String KAKAO_SECRET_SERVER_PWD;
private final String INAPP = "IN_APP";
private final String KAKAO = "KAKAO";

/**
* 회원 가입
* @param dto
*/
@Transactional
public void signUp(SignUpRequestDto dto) {

if(userRepository.existsByEmail(dto.getEmail())) throw new EmailExistsException("이미 가입한 이메일입니다.");
userRepository.save(
User.builder()
.id(null)
.email(dto.getEmail())
.password(passwordEncoder.encode(dto.getPassword()))
.username(dto.getUsername())
.address(dto.getAddress())
.phoneNum(dto.getPhoneNum())
.role(dto.getRole())
.refreshToken(null)
.oauth(INAPP)
.build()
);

}

/**
* 로그인
* @param email
* @param password
* @return
*/
@Transactional
public SignInResponseDto signIn(String email, String password) {
User user = userRepository.findByEmail(email).orElseThrow(()->new InvalidEmailException("회원정보가 존재하지 않습니다."));
if(!passwordEncoder.matches(password, user.getPassword())) {
if(user.getOauth().equals("KAKAO")) {
throw new InvalidPasswordException("카카오 계정입니다. 카카오 로그인으로 시도해보세요.");
}
throw new InvalidPasswordException("잘못된 비밀번호입니다.");
}

TokenInfo accessToken = tokenProvider.createAccessToken(user.getEmail(), user.getRole());
TokenInfo refreshToken = tokenProvider.createRefreshToken(user.getEmail(), user.getRole());
user.updateRefreshToken(refreshToken.getToken());
return new SignInResponseDto(
user.getId(), user.getEmail(), user.getImage(), user.getRole(), accessToken.getToken(), refreshToken.getToken(), accessToken.getExpireTime(), refreshToken.getExpireTime()
);

}

/**
* 카카오 api 사용하여 유저 정보 받기
* @param token
* @return
*/
@Transactional
public KakaoProfileDto getKakaoProfile(String token) {
RestTemplate rt = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer "+token);
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

HttpEntity<MultiValueMap<String, String>> kakaoProfileRequest = new HttpEntity<>(headers);
ResponseEntity<String> response = rt.exchange(
"https://kapi.kakao.com/v2/user/me",
HttpMethod.POST,
kakaoProfileRequest,
String.class
);

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
KakaoProfileDto profile;

try {
profile = objectMapper.readValue(response.getBody(), KakaoProfileDto.class);
} catch(JsonMappingException e) {
throw new JsonException("Json Mapping 오류가 발생했습니다.");
} catch(JsonProcessingException e) {
throw new JsonException("Json Processing 오류가 발생했습니다.");
}

return profile;
}

/**
* 카카오 회원가입, 로그인
* @param profile
* @return
*/
@Transactional
public SignInResponseDto kakaoAutoSignIn(KakaoProfileDto profile) {
User kakaoUser = User.builder()
.email(profile.getKakao_account().getEmail())
.username(profile.getProperties().getNickname())
.image(profile.getKakao_account().getProfile().getProfile_image_url())
.password(KAKAO_SECRET_SERVER_PWD)
.oauth(KAKAO)
.build();

User findUser = userRepository.findByEmail(kakaoUser.getEmail()).orElse(null);

if(findUser != null) {
log.info(profile.getKakao_account().getEmail()+": 기존 회원이 아니므로 자동 회원가입 후 로그인을 진행합니다.");
signUp(new SignUpRequestDto(
kakaoUser.getEmail(),
kakaoUser.getPassword(),
kakaoUser.getUsername(),
kakaoUser.getAddress(),
kakaoUser.getPhoneNum(),
Role.USER,
kakaoUser.getImage()
));
findUser.updateOAuth(KAKAO);
} else {
log.info(profile.getKakao_account().getEmail()+": 기존 회원이므로 자동 로그인을 진행합니다.");
}
return signIn(kakaoUser.getEmail(), kakaoUser.getPassword());
}

/**
* refreshtoken 갱
* @param email
* @param refreshToken
* @return
*/
@Transactional
public ReissueResponseDto reissue(String email, String refreshToken) {
User user = userRepository.findByEmail(email).orElseThrow(()->new InvalidEmailException("회원정보가 존재하지 않습니다."));
if(!user.getRefreshToken().equals(refreshToken)) {
throw new RefreshTokenNotFoundException("리프레쉬 토큰에서 유저정보를 찾을 수 없습니다.");
}
tokenProvider.validateToken(refreshToken);

TokenInfo newAccessToken = tokenProvider.createAccessToken(user.getEmail(), user.getRole());
TokenInfo newRefreshToken = tokenProvider.createRefreshToken(user.getEmail(), user.getRole());
return new ReissueResponseDto(
newAccessToken.getToken(), newRefreshToken.getToken()
);
}
}
32 changes: 32 additions & 0 deletions src/main/java/com/kusitms/jipbap/auth/dto/KakaoProfileDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.kusitms.jipbap.auth.dto;

import lombok.Data;

@Data
public class KakaoProfileDto {

private Long id;
private Properties properties;
private KakaoAccount kakao_account;
private String connected_at;

@Data
public class Properties {
private String nickname;
}

@Data
public class KakaoAccount {
private Boolean has_email;
private Boolean email_needs_agreement;
private Boolean is_email_valid;
private Boolean is_email_verified;
private String email;
private Profile profile;

@Data
public class Profile {
private String profile_image_url;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.kusitms.jipbap.auth.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.validation.constraints.NotBlank;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class KakaoSignInRequestDto {
@NotBlank
private String token;
}
13 changes: 13 additions & 0 deletions src/main/java/com/kusitms/jipbap/auth/dto/ReissueResponseDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.kusitms.jipbap.auth.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ReissueResponseDto {
private String accessToken;
private String refreshToken;
}
19 changes: 19 additions & 0 deletions src/main/java/com/kusitms/jipbap/auth/dto/SignInRequestDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.kusitms.jipbap.auth.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class SignInRequestDto {
@Email
@NotBlank
private String email;
@NotBlank
private String password;
}

0 comments on commit c8f1363

Please sign in to comment.