Skip to content

Commit

Permalink
Merge pull request #28 from KNU-HAEDAL-Website/feat-exeption-login-is…
Browse files Browse the repository at this point in the history
…sue-22

Feat: 로그인, 로그아웃, reissue 예외처리
  • Loading branch information
tfer2442 authored Apr 24, 2024
2 parents d042498 + 2705d38 commit 967c8b9
Show file tree
Hide file tree
Showing 19 changed files with 326 additions and 265 deletions.
Binary file added haedal-web-0.0.1-SNAPSHOT.jar
Binary file not shown.
16 changes: 11 additions & 5 deletions src/main/java/com/haedal/haedalweb/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.haedal.haedalweb.config;

import com.haedal.haedalweb.constants.LoginConstants;
import com.haedal.haedalweb.exception.FilterExceptionHandler;
import com.haedal.haedalweb.jwt.CustomLogoutFilter;
import com.haedal.haedalweb.jwt.JWTFilter;
import com.haedal.haedalweb.jwt.JWTUtil;
import com.haedal.haedalweb.jwt.LoginFilter;
import com.haedal.haedalweb.service.RedisService;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand Down Expand Up @@ -40,11 +40,10 @@ public PasswordEncoder passwordEncoder() {
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http, AuthenticationConfiguration authenticationConfiguration, JWTUtil jwtUtil, RedisService redisService) throws Exception {
public SecurityFilterChain filterChain(HttpSecurity http, AuthenticationConfiguration authenticationConfiguration, JWTUtil jwtUtil) throws Exception {

http
.cors((corsCustomizer -> corsCustomizer.configurationSource(new CorsConfigurationSource() {

@Override
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {

Expand Down Expand Up @@ -83,10 +82,17 @@ public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
.addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class);

http
.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil, redisService), UsernamePasswordAuthenticationFilter.class);
.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil), UsernamePasswordAuthenticationFilter.class);

http
.addFilterBefore(new FilterExceptionHandler(), LogoutFilter.class);

http
.addFilterBefore(new CustomLogoutFilter(jwtUtil, redisService), LogoutFilter.class);
.logout((auth) -> auth.disable());

http
.addFilterAfter(new CustomLogoutFilter(jwtUtil), LogoutFilter.class);


http
.sessionManagement((session) -> session
Expand Down
9 changes: 8 additions & 1 deletion src/main/java/com/haedal/haedalweb/constants/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@
public enum ErrorCode {
DUPLICATED_USER_ID(HttpStatus.CONFLICT, "중복된 아이디가 존재합니다."),
DUPLICATED_STUDENT_NUMBER(HttpStatus.CONFLICT, "중복된 학번이 존재합니다."),
INVALID_LOGIN_CONTENTS_TYPE(HttpStatus.BAD_REQUEST, "지원하지 않는 형식입니다.");
INVALID_LOGIN_CONTENTS_TYPE(HttpStatus.BAD_REQUEST, "지원하지 않는 형식입니다."),
FAILED_LOGIN(HttpStatus.UNAUTHORIZED, "아이디 또는 비밀번호가 일치하지 않습니다."),
EXPIRED_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "Access Token has expired."),
INVALID_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "Access Token is invalid."),
NULL_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, "Refresh Token is null."),
EXPIRED_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, "Refresh Token has expired."),
INVALID_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, "Refresh Token is invalid."),
INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "Parameter is invalid.");

private final HttpStatus httpStatus;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ public final class LoginConstants {
public static final String REFRESH_TOKEN_EXPIRED = "refresh token expired";
public static final String INVALID_REFRESH_TOKEN = "invalid refresh token";

public static final String ACCESS_TOKEN_EXPIRED = "access token null";
public static final String INVALID_ACCESS_TOKEN = "invalid access token";
// public static final String ACCESS_TOKEN_EXPIRED = "access token null";
// public static final String INVALID_ACCESS_TOKEN = "invalid access token";

public static final String REFRESH_TOKEN = "refreshToken";
public static final String ACCESS_TOKEN = "Authorization";
Expand All @@ -15,7 +15,7 @@ public final class LoginConstants {
public static final String ROLE_CLAIM = "role";
public static final String CATEGORY_CLAIM = "category";

public static final long ACCESS_TOKEN_EXPIRATION_TIME_MS = 3600*1000;
public static final long ACCESS_TOKEN_EXPIRATION_TIME_MS = 36*1000;// 3600*1000;
public static final long REFRESH_TOKEN_EXPIRATION_TIME_MS = 86400*1000;
public static final long REFRESH_TOKEN_EXPIRATION_TIME_S = 86400; // 1 day
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ public enum SuccessCode {
UNIQUE_USER_ID(HttpStatus.OK, true, "사용 가능한 ID입니다."),
DUPLICATED_USER_ID(HttpStatus.OK, false, "중복된 ID입니다. 다른 ID를 입력해 주세요."),
UNIQUE_STUDENT_NUMBER(HttpStatus.OK, true, "사용 가능한 학번입니다."),
DUPLICATED_STUDENT_NUMBER(HttpStatus.OK, false, "중복된 학번입니다. 다시 확인해 주세요.");
DUPLICATED_STUDENT_NUMBER(HttpStatus.OK, false, "중복된 학번입니다. 다시 확인해 주세요."),
LOGIN_SUCCESS(HttpStatus.OK, true, "로그인에 성공했습니다."),
LOGOUT_SUCCESS(HttpStatus.OK, true, "로그아웃에 성공했습니다."),
REISSUE_SUCCESS(HttpStatus.OK, true, "토큰을 재발급했습니다.");

private final HttpStatus httpStatus;
private final Boolean success;
Expand Down
23 changes: 4 additions & 19 deletions src/main/java/com/haedal/haedalweb/controller/JoinController.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.haedal.haedalweb.constants.SuccessCode;
import com.haedal.haedalweb.dto.JoinDTO;
import com.haedal.haedalweb.dto.SuccessResponse;
import com.haedal.haedalweb.util.ResponseUtil;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
Expand All @@ -26,13 +27,7 @@ public JoinController(JoinService joinService) {
public ResponseEntity<SuccessResponse> resisterUser(@RequestBody @Valid JoinDTO joinDTO) {
joinService.createUserAccount(joinDTO);

SuccessCode successCode = SuccessCode.JOIN_SUCCESS;
SuccessResponse successResponse = SuccessResponse.builder()
.success(successCode.getSuccess())
.message(successCode.getMessage())
.build();

return ResponseEntity.status(successCode.getHttpStatus()).body(successResponse);
return ResponseUtil.buildSuccessResponseEntity(SuccessCode.JOIN_SUCCESS);
}

@GetMapping("/check-user-id")
Expand All @@ -42,12 +37,7 @@ public ResponseEntity<SuccessResponse> checkUserIdDuplicate(@RequestParam String
successCode = SuccessCode.DUPLICATED_USER_ID;
}

SuccessResponse successResponse = SuccessResponse.builder()
.success(successCode.getSuccess())
.message(successCode.getMessage())
.build();

return ResponseEntity.status(successCode.getHttpStatus()).body(successResponse);
return ResponseUtil.buildSuccessResponseEntity(successCode);
}

@GetMapping("/check-student-number")
Expand All @@ -57,11 +47,6 @@ public ResponseEntity<SuccessResponse> checkStudentNumberDuplicate(@RequestParam
successCode = SuccessCode.DUPLICATED_STUDENT_NUMBER;
}

SuccessResponse successResponse = SuccessResponse.builder()
.success(successCode.getSuccess())
.message(successCode.getMessage())
.build();

return ResponseEntity.status(successCode.getHttpStatus()).body(successResponse);
return ResponseUtil.buildSuccessResponseEntity(successCode);
}
}
Original file line number Diff line number Diff line change
@@ -1,95 +1,24 @@
package com.haedal.haedalweb.controller;

import com.haedal.haedalweb.constants.LoginConstants;
import com.haedal.haedalweb.jwt.JWTUtil;
import com.haedal.haedalweb.service.RedisService;
import io.jsonwebtoken.ExpiredJwtException;
import jakarta.servlet.http.Cookie;
import com.haedal.haedalweb.constants.SuccessCode;
import com.haedal.haedalweb.service.IssueService;
import com.haedal.haedalweb.util.ResponseUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
public class ReissueController {

private final JWTUtil jwtUtil;
private final RedisService redisService;
private final IssueService issueService;

@PostMapping("/reissue")
public ResponseEntity<?> reissue(HttpServletRequest request, HttpServletResponse response) {
//get refresh token
String refreshToken = null;
Cookie[] cookies = request.getCookies();

for (Cookie cookie : cookies) {

if (cookie.getName().equals(LoginConstants.REFRESH_TOKEN)) {

refreshToken = cookie.getValue();
}
}

if (refreshToken == null) {

//response status code
return ResponseEntity.badRequest().body(LoginConstants.REFRESH_TOKEN_NULL);
}

//expired check
try {
jwtUtil.isExpired(refreshToken);
} catch (ExpiredJwtException e) {

//response status code
return ResponseEntity.badRequest().body(LoginConstants.REFRESH_TOKEN_EXPIRED);
}

// 토큰이 refresh인지 확인 (발급시 페이로드에 명시)
String category = jwtUtil.getCategory(refreshToken);

if (!category.equals(LoginConstants.REFRESH_TOKEN)) {

//response status code
return ResponseEntity.badRequest().body(LoginConstants.INVALID_REFRESH_TOKEN);
}

boolean isExist = redisService.existsByRefreshToken(refreshToken);

if (!isExist) {

//response body
return new ResponseEntity<>("invalid refresh token", HttpStatus.BAD_REQUEST);
}

String userId = jwtUtil.getUserId(refreshToken);
String role = jwtUtil.getRole(refreshToken);
//make new JWT
String newAccessToken = jwtUtil.createJwt(LoginConstants.ACCESS_TOKEN, userId, role, LoginConstants.ACCESS_TOKEN_EXPIRATION_TIME_MS);
String newRefreshToken = jwtUtil.createJwt(LoginConstants.REFRESH_TOKEN, userId, role, LoginConstants.REFRESH_TOKEN_EXPIRATION_TIME_MS);

redisService.deleteRefreshToken(refreshToken);
redisService.saveRefreshToken(newRefreshToken, userId);

//response
response.setHeader(LoginConstants.ACCESS_TOKEN, newAccessToken);
response.addCookie(createCookie(LoginConstants.REFRESH_TOKEN, newRefreshToken));

return ResponseEntity.ok().build();
}

private Cookie createCookie(String key, String value) {

Cookie cookie = new Cookie(key, value);
cookie.setMaxAge((int)LoginConstants.REFRESH_TOKEN_EXPIRATION_TIME_S);
//cookie.setSecure(true);
cookie.setPath("/");
cookie.setHttpOnly(true);
issueService.reissueToken(request, response);

return cookie;
return ResponseUtil.buildSuccessResponseEntity(SuccessCode.REISSUE_SUCCESS);
}
}
1 change: 0 additions & 1 deletion src/main/java/com/haedal/haedalweb/dto/JoinDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;

@Getter
@AllArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.haedal.haedalweb.exception;

import com.haedal.haedalweb.constants.ErrorCode;
import com.haedal.haedalweb.dto.ErrorResponse;
import com.haedal.haedalweb.util.ResponseUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.filter.GenericFilterBean;
import java.io.IOException;

public class FilterExceptionHandler extends GenericFilterBean {

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
this.doFilter((HttpServletRequest)request, (HttpServletResponse)response, chain);
}

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
try {
chain.doFilter(request, response);
} catch (BusinessException e) {
sendErrorResponse(response, e);
}
}

private void sendErrorResponse(HttpServletResponse response, BusinessException e) {
ErrorCode errorCode = e.getErrorCode();
response.setStatus(errorCode.getHttpStatus().value());
ErrorResponse errorResponse = ErrorResponse.builder()
.message(errorCode.getMessage())
.build();

ResponseUtil.writeAsJsonResponse(response, errorResponse);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
import com.haedal.haedalweb.constants.ErrorCode;
import com.haedal.haedalweb.dto.ErrorResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.List;
import java.util.stream.Collectors;

@RestControllerAdvice
public class GlobalExceptionHandler {

Expand All @@ -16,6 +20,31 @@ public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e
.message(errorCode.getMessage())
.build();

return ResponseEntity.status(errorCode.getHttpStatus()).body(errorResponse);
return ResponseEntity.status(errorCode.getHttpStatus())
.body(errorResponse);
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidException(MethodArgumentNotValidException e) {
ErrorCode errorCode = ErrorCode.INVALID_PARAMETER;
return handleExceptionInternal(e, errorCode);
}

private ResponseEntity<ErrorResponse> handleExceptionInternal(MethodArgumentNotValidException e, ErrorCode errorCode) {
return ResponseEntity.status(errorCode.getHttpStatus())
.body(makeErrorResponse(e, errorCode));
}

private ErrorResponse makeErrorResponse(MethodArgumentNotValidException e, ErrorCode errorCode) {
List<ErrorResponse.ValidationError> validationErrorList = e.getBindingResult()
.getFieldErrors()
.stream()
.map(ErrorResponse.ValidationError::of)
.collect(Collectors.toList());

return ErrorResponse.builder()
.message(errorCode.getMessage())
.errors(validationErrorList)
.build();
}
}
Loading

0 comments on commit 967c8b9

Please sign in to comment.