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

Feat/#13 이메일 중복 체크 메서드 및 예외 처리 추가 #14

Merged
merged 6 commits into from
Jul 3, 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
@@ -1,5 +1,6 @@
package org.sophy.sophy.common.advice;

import io.jsonwebtoken.ExpiredJwtException;
import org.sophy.sophy.common.dto.ApiResponseDto;
import org.sophy.sophy.exception.ErrorStatus;
import org.springframework.http.HttpStatus;
Expand All @@ -8,7 +9,6 @@
import org.springframework.web.bind.annotation.ResponseStatus;
import org.sophy.sophy.exception.model.SophyException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
Expand All @@ -30,6 +30,12 @@ protected ApiResponseDto handleMethodArgumentNotValidException(final MethodArgum
// return ApiResponseDto.error(ErrorStatus.INTERNAL_SERVER_ERROR);
// }

@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler(ExpiredJwtException.class)
protected ApiResponseDto handleExpiredRefreshTokenException(final ExpiredJwtException e) {
return ApiResponseDto.error(ErrorStatus.REFRESH_TOKEN_TIME_EXPIRED_EXCEPTION);
}

/**
* Sopt custom error
*/
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/org/sophy/sophy/config/JwtSecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.sophy.sophy.config;

import lombok.RequiredArgsConstructor;
import org.sophy.sophy.jwt.JwtExceptionFilter;
import org.sophy.sophy.jwt.JwtFilter;
import org.sophy.sophy.jwt.TokenProvider;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
Expand All @@ -12,13 +13,15 @@
@RequiredArgsConstructor
public class JwtSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private final TokenProvider tokenProvider;
private final JwtExceptionFilter jwtExceptionFilter;

//TokenProvider를 주입받아서 JwtFillter를 통해 Security 로직에 필터를 등록
//HttpSecurity의 userpassword인증필터에 filter 추가
@Override
public void configure(HttpSecurity http) {
JwtFilter customFilter = new JwtFilter(tokenProvider);
http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(jwtExceptionFilter, JwtFilter.class);
}

}
6 changes: 5 additions & 1 deletion src/main/java/org/sophy/sophy/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import lombok.RequiredArgsConstructor;
import org.sophy.sophy.jwt.JwtAccessDeniedHandler;
import org.sophy.sophy.jwt.JwtAuthenticationEntryPoint;
import org.sophy.sophy.jwt.JwtExceptionFilter;
import org.sophy.sophy.jwt.TokenProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -19,6 +20,8 @@ public class SecurityConfig {
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;

private final JwtExceptionFilter jwtExceptionFilter;

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
Expand Down Expand Up @@ -51,11 +54,12 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.antMatchers("/auth/**").permitAll()
.antMatchers("/profile/**").permitAll()
.antMatchers("/actuator/**").permitAll()
.antMatchers("/health/**").permitAll()
.anyRequest().authenticated() //나머지 API는 전부 인증 필요

//JwtFilter 를 addFilterBefore 로 등록했던 JwtSecurityConfig 클래스를 적용
.and()
.apply(new JwtSecurityConfig(tokenProvider));
.apply(new JwtSecurityConfig(tokenProvider, jwtExceptionFilter));

return http.build();
}
Expand Down
13 changes: 10 additions & 3 deletions src/main/java/org/sophy/sophy/controller/AuthController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

import lombok.RequiredArgsConstructor;
import org.sophy.sophy.common.dto.ApiResponseDto;
import org.sophy.sophy.controller.dto.request.DuplCheckDto;
import org.sophy.sophy.controller.dto.request.MemberLoginRequestDto;
import org.sophy.sophy.controller.dto.request.MemberRequestDto;
import org.sophy.sophy.controller.dto.request.TokenRequestDto;
import org.sophy.sophy.controller.dto.response.MemberResponseDto;
import org.sophy.sophy.controller.dto.response.TokenDto;
import org.sophy.sophy.exception.SuccessStatus;
import org.sophy.sophy.service.AuthService;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

@RestController
Expand All @@ -19,8 +20,9 @@ public class AuthController {
private final AuthService authService;

@PostMapping("/signup")
public ResponseEntity<MemberResponseDto> signup(@RequestBody MemberRequestDto memberRequestDto) {
return ResponseEntity.ok(authService.signup(memberRequestDto));
@ResponseStatus(HttpStatus.CREATED)
public ApiResponseDto<MemberResponseDto> signup(@RequestBody MemberRequestDto memberRequestDto) {
return ApiResponseDto.success(SuccessStatus.SIGNUP_SUCCESS, authService.signup(memberRequestDto));
}

@PostMapping("/login")
Expand All @@ -32,4 +34,9 @@ public ApiResponseDto<TokenDto> login(@RequestBody MemberLoginRequestDto memberL
public ApiResponseDto<TokenDto> reissue(@RequestBody TokenRequestDto tokenRequestDto) {
return ApiResponseDto.success(SuccessStatus.REISSUE_SUCCESS, authService.reissue(tokenRequestDto));
}

@GetMapping("/dupl-check")
public ApiResponseDto<String> duplCheck(@RequestBody DuplCheckDto email) {
return ApiResponseDto.success(SuccessStatus.CHECK_DUPL_EMAIL_SUCCESS, authService.duplCheck(email));
}
}
15 changes: 15 additions & 0 deletions src/main/java/org/sophy/sophy/controller/TestController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.sophy.sophy.controller;

import org.sophy.sophy.common.dto.ApiResponseDto;
import org.sophy.sophy.exception.SuccessStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {

@GetMapping("/test")
public ApiResponseDto<String> test() {
return ApiResponseDto.success(SuccessStatus.TEST_SUCCESS, SuccessStatus.TEST_SUCCESS.getMessage());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.sophy.sophy.controller.dto.request;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotNull;

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class DuplCheckDto {
@Email(message = "이메일 형식에 맞지 않습니다.")
@NotNull
String email;
}
8 changes: 7 additions & 1 deletion src/main/java/org/sophy/sophy/exception/ErrorStatus.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,20 @@ public enum ErrorStatus {
/**
* 401 UNAUTHORIZED
*/
TOKEN_TIME_EXPIRED_EXCEPTION(HttpStatus.UNAUTHORIZED, "만료된 토큰입니다."),
INVALID_ACCESS_TOKEN_EXCEPTION(HttpStatus.UNAUTHORIZED, "유효하지 않은 액세스 토큰입니다."),
REFRESH_TOKEN_TIME_EXPIRED_EXCEPTION(HttpStatus.UNAUTHORIZED, "만료된 리프레시 토큰입니다."),
/**
* 404 NOT FOUND
*/
NOT_FOUND_USER_EXCEPTION(HttpStatus.NOT_FOUND, "존재하지 않는 유저입니다"),
NOT_FOUND_SAVE_IMAGE_EXCEPTION(HttpStatus.NOT_FOUND, "이미지 저장에 실패했습니다"),
NOT_FOUND_IMAGE_EXCEPTION(HttpStatus.NOT_FOUND, "이미지 이름을 찾을 수 없습니다"),

/**
* 409 CONFLICT
*/
ALREADY_EXIST_USER_EXCEPTION(HttpStatus.CONFLICT, "이미 존재하는 유저입니다"),

/**
* 500 INTERNAL SERVER ERROR
*/
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/org/sophy/sophy/exception/SuccessStatus.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public enum SuccessStatus {
*/
LOGIN_SUCCESS(HttpStatus.OK, "로그인에 성공했습니다."),
REISSUE_SUCCESS(HttpStatus.OK, "토큰 재발행에 성공했습니다."),
CHECK_DUPL_EMAIL_SUCCESS(HttpStatus.OK, "사용 가능한 이메일 주소입니다."),
TEST_SUCCESS(HttpStatus.OK, "Test :: OK"),
/*
* 201 created
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.sophy.sophy.exception.model;

import org.sophy.sophy.exception.ErrorStatus;

public class ExistEmailException extends SophyException {

public ExistEmailException(ErrorStatus errorStatus, String message) {
super(errorStatus, message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.sophy.sophy.exception.model;

import org.sophy.sophy.exception.ErrorStatus;

public class ExpiredRefreshTokenException extends SophyException {

public ExpiredRefreshTokenException(ErrorStatus errorStatus, String message) {
super(errorStatus, message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
import org.sophy.sophy.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Repository
@Transactional
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByEmail(String email);
boolean existsByEmail(String email);
Expand Down
39 changes: 39 additions & 0 deletions src/main/java/org/sophy/sophy/jwt/JwtExceptionFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.sophy.sophy.jwt;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.JwtException;
import org.sophy.sophy.common.dto.ApiResponseDto;
import org.sophy.sophy.exception.ErrorStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class JwtExceptionFilter extends OncePerRequestFilter {

@Autowired
private ObjectMapper objectMapper;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} catch (JwtException exception) {
setErrorResponse(HttpStatus.UNAUTHORIZED, response, exception);
}
}

public void setErrorResponse(HttpStatus status, HttpServletResponse res, Throwable ex) throws IOException {
res.setStatus(status.value());
res.setContentType("application/json; charset=UTF-8");

res.getWriter().write(objectMapper.writeValueAsString(ApiResponseDto.error(ErrorStatus.INVALID_ACCESS_TOKEN_EXCEPTION, ex.getMessage())));
}
}
16 changes: 11 additions & 5 deletions src/main/java/org/sophy/sophy/jwt/JwtFilter.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.sophy.sophy.jwt;

import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
Expand All @@ -23,17 +25,21 @@ public class JwtFilter extends OncePerRequestFilter {
// 실제 필터링 로직은 doFilterInternal 에 들어감
// JWT 토큰의 인증 정보를 현재 쓰레드의 SecurityContext 에 저장하는 역할 수행
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException{

// 1. Request Header에서 토큰을 꺼냄
String jwt = resolveToken(request);

// 2. validateToken 으로 토큰 유효성 검사
// 정상 토큰이면 해당 토큰으롤 Authentication을 가져와서 SecurityContext에 저장
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
Authentication authentication = tokenProvider.getAuthentication(jwt);
//Context에 저장할 때 auth를 설정하며 저장
SecurityContextHolder.getContext().setAuthentication(authentication);
try {
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
Authentication authentication = tokenProvider.getAuthentication(jwt);
//Context에 저장할 때 auth를 설정하며 저장
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (ExpiredJwtException e) { //access token이 만료되었을 때
throw new JwtException("만료된 액세스 토큰입니다.");
}

filterChain.doFilter(request, response);
Expand Down
14 changes: 8 additions & 6 deletions src/main/java/org/sophy/sophy/jwt/TokenProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@
public class TokenProvider {
private static final String AUTHORITIES_KEY = "auth";
private static final String BEARER_TYPE = "Bearer";
private static final long ACCESS_TOKEN_EXPIRE_TIME = 1000 * 60 * 30;
private static final long REFRESH_TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 24 * 7;
private static final long ACCESS_TOKEN_EXPIRE_TIME = 1000 * 10;
// private static final long ACCESS_TOKEN_EXPIRE_TIME = 1000 * 60 * 30;
private static final long REFRESH_TOKEN_EXPIRE_TIME = 1000 * 30;
// private static final long REFRESH_TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 24 * 7;

private final Key key;

Expand Down Expand Up @@ -78,7 +80,7 @@ public Authentication getAuthentication(String accessToken) {
Claims claims = parseClaims(accessToken);

if (claims.get(AUTHORITIES_KEY) == null) {
throw new RuntimeException("권한 정보가 없는 토큰입니다.");
throw new JwtException("권한 정보가 없는 토큰입니다.");
}

//클레임에서 권한 정보 가져오기
Expand Down Expand Up @@ -115,13 +117,13 @@ public boolean validateToken(String token) {
return true;
} catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
log.info("잘못된 JWT 서명입니다.");
} catch (ExpiredJwtException e) {
log.info("만료된 JWT 토큰입니다.");
throw new JwtException("잘못된 JWT 서명입니다.");
} catch (UnsupportedJwtException e) {
log.info("지원되지 않는 JWT 토큰입니다.");
throw new JwtException("지원되지 않는 JWT 토큰입니다.");
} catch (IllegalStateException e) {
log.info("JWT 토큰이 잘못되었습니다.");
throw new JwtException("JWT 토큰이 잘못되었습니다.");
}
return false;
}
}
20 changes: 15 additions & 5 deletions src/main/java/org/sophy/sophy/service/AuthService.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package org.sophy.sophy.service;

import lombok.RequiredArgsConstructor;
import org.sophy.sophy.controller.dto.request.DuplCheckDto;
import org.sophy.sophy.controller.dto.request.MemberLoginRequestDto;
import org.sophy.sophy.controller.dto.request.MemberRequestDto;
import org.sophy.sophy.controller.dto.request.TokenRequestDto;
import org.sophy.sophy.controller.dto.response.MemberResponseDto;
import org.sophy.sophy.controller.dto.response.TokenDto;
import org.sophy.sophy.domain.Member;
import org.sophy.sophy.exception.ErrorStatus;
import org.sophy.sophy.exception.model.ExistEmailException;
import org.sophy.sophy.infrastructure.MemberRepository;
import org.sophy.sophy.jwt.TokenProvider;
import org.springframework.data.redis.core.RedisTemplate;
Expand All @@ -33,13 +36,22 @@ public class AuthService {
@Transactional
public MemberResponseDto signup(MemberRequestDto memberRequestDto) {
if (memberRepository.existsByEmail(memberRequestDto.getEmail())) {
throw new RuntimeException("이미 가입되어 있는 유저입니다.");
throw new ExistEmailException(ErrorStatus.ALREADY_EXIST_USER_EXCEPTION, ErrorStatus.ALREADY_EXIST_USER_EXCEPTION.getMessage());
}

Member member = memberRequestDto.toMember(passwordEncoder);
return MemberResponseDto.of(memberRepository.save(member));
}

@Transactional
public String duplCheck(DuplCheckDto email) {

if (memberRepository.existsByEmail(email.getEmail())) {
throw new ExistEmailException(ErrorStatus.ALREADY_EXIST_USER_EXCEPTION, ErrorStatus.ALREADY_EXIST_USER_EXCEPTION.getMessage());
}
return "사용 가능한 이메일입니다.";
}

@Transactional
public TokenDto login(MemberLoginRequestDto memberLoginRequestDto) {
// 1. Login ID/PW 를 기반으로 AuthenticationToken 생성
Expand All @@ -64,11 +76,9 @@ public TokenDto login(MemberLoginRequestDto memberLoginRequestDto) {
}

@Transactional
public TokenDto reissue(TokenRequestDto tokenRequestDto) {
public TokenDto reissue(TokenRequestDto tokenRequestDto){
// 1. Refresh Token 검증
if (!tokenProvider.validateToken(tokenRequestDto.getRefreshToken())) {
throw new RuntimeException("Refresh Token 이 유효하지 않습니다.");
}
tokenProvider.validateToken(tokenRequestDto.getRefreshToken());

// 2. Access Token 에서 Member ID 가져오기
Authentication authentication = tokenProvider.getAuthentication(tokenRequestDto.getAccessToken());
Expand Down