Feat: 로그인 기능

This commit is contained in:
김용수 2024-08-28 09:16:51 +09:00
commit 63834ccbb9
9 changed files with 220 additions and 11 deletions

31
.gitignore vendored
View File

@ -1,2 +1,33 @@
.DS_Store
application.yml
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/

View File

@ -55,6 +55,9 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
//Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'
}
tasks.named('test') {

View File

@ -0,0 +1,58 @@
package com.worlabel.global.config;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@OpenAPIDefinition(
info = @Info(
title = "auto labeling API",
description = "auto labeling API 목록입니다.",
version = "v1.0"
)
)
@SecurityScheme(
name = "Authorization",
type = SecuritySchemeType.HTTP,
bearerFormat = "JWT",
in = SecuritySchemeIn.HEADER,
scheme = "Bearer",
description = "access token"
)
public class SwaggerConfig {
private final String[] noRequiredTokenApi = {"/register", "/login", "/reissue","/login/oauth", "/oauth2/**",
"/login/oauth2/**", "/error", "login/oauth2/code/kakao", "/register/duplicate", "/test"};
private final OperationCustomizer operationCustomizer;
public SwaggerConfig(OperationCustomizer operationCustomizer) {
this.operationCustomizer = operationCustomizer;
}
@Bean
public GroupedOpenApi nonSecurityGroup(){ //jwt 토큰 불필요한 api
return GroupedOpenApi.builder()
.group("token 불필요 API")
.pathsToMatch(noRequiredTokenApi)
.addOperationCustomizer(operationCustomizer)
.build();
}
@Bean
public GroupedOpenApi securityGroup(){ //jwt 토큰 필요한 api
return GroupedOpenApi.builder()
.group("token 필요 API")
.pathsToExclude(noRequiredTokenApi)
.addOperationCustomizer(operationCustomizer)
.build();
}
}

View File

@ -0,0 +1,71 @@
package com.worlabel.global.config.swagger;
import com.worlabel.global.exception.CustomException;
import com.worlabel.global.response.BaseResponse;
import com.worlabel.global.response.CustomError;
import com.worlabel.global.response.ErrorResponse;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.examples.Example;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.responses.ApiResponses;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import com.worlabel.global.exception.ErrorCode;
@Component
@AllArgsConstructor
public class CustomOperationCustomizer implements OperationCustomizer {
@Override
public Operation customize(Operation operation, HandlerMethod handlerMethod) {
SwaggerApiError swaggerApiError = handlerMethod.getMethodAnnotation(SwaggerApiError.class);
operation.getResponses().remove("500");
if (swaggerApiError != null) {
generateErrorCodeResponse(operation, swaggerApiError.value());
}
return operation;
}
//에러 코드로 error response 만들어 ApiResponse 넣음
private void generateErrorCodeResponse(Operation operation, ErrorCode[] errorCodes) {
ApiResponses responses = operation.getResponses();
Map<Integer, List<BaseResponse<CustomError>>> statusWithErrorResponse = Arrays.stream(errorCodes)
.map(errorCode -> ErrorResponse.of(new CustomException(errorCode)))
.collect(Collectors.groupingBy(BaseResponse::getStatus));
addErrorCodesToResponse(responses, statusWithErrorResponse);
}
//ApiResponses에 error response 추가
private void addErrorCodesToResponse(ApiResponses apiResponses, Map<Integer, List<BaseResponse<CustomError>>> responses) {
responses.forEach((status, value) -> {
Content content = new Content();
MediaType mediaType = new MediaType();
ApiResponse apiResponse = new ApiResponse();
value.forEach(
errorInfoResponse -> {
Example example = new Example();
example.setValue(errorInfoResponse);
mediaType.addExamples(String.valueOf(errorInfoResponse.getCode()), example);
});
content.addMediaType("application/json", mediaType);
apiResponse.setContent(content);
apiResponses.addApiResponse(String.valueOf(status), apiResponse);
});
}
}

View File

@ -0,0 +1,16 @@
package com.worlabel.global.config.swagger;
import com.worlabel.global.exception.ErrorCode;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SwaggerApiError {
ErrorCode[] value();
}

View File

@ -0,0 +1,20 @@
package com.worlabel.global.config.swagger;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@ApiResponses(value = {
@ApiResponse(responseCode = "200")
})
public @interface SwaggerApiSuccess {
String description() default "";
}

View File

@ -22,6 +22,11 @@ public abstract class BaseResponse<T> {
@Getter(onMethod_ = @JsonProperty("isSuccess"))
private boolean isSuccess;
/**
* 상태 코드
*/
private int status;
/**
* 응답 코드
*/
@ -42,8 +47,9 @@ public abstract class BaseResponse<T> {
*/
protected List<CustomError> errors;
public BaseResponse(boolean isSuccess, int code, String message) {
public BaseResponse(boolean isSuccess, int status, int code, String message) {
this.isSuccess = isSuccess;
this.status = status;
this.code = code;
this.message = message;
this.data = null;

View File

@ -1,6 +1,7 @@
package com.worlabel.global.response;
import com.worlabel.global.exception.CustomException;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.Errors;
@ -16,19 +17,22 @@ import java.util.stream.Collectors;
* 에러 발생시 리턴 에러 응답 객체
*/
@Slf4j
public class ErrorResponse extends BaseResponse<Void> {
@Getter
public class ErrorResponse extends BaseResponse<CustomError> {
public ErrorResponse(boolean isSuccess, int code, String message, Errors errors) {
super(isSuccess, code, message);
public ErrorResponse(boolean isSuccess, int status, int code, String message, Errors errors) {
super(isSuccess, status, code, message);
super.errors = parseErrors(errors);
}
public ErrorResponse(CustomException exception) {
this(false, exception.getErrorCode().getCode(), exception.getMessage(), exception.getErrors());
this(false, exception.getErrorCode().getStatus().value(), exception.getErrorCode().getCode(), exception.getMessage(), exception.getErrors());
}
public ErrorResponse(CustomException exception, String message) {
this(false, 1, message, exception.getErrors());
this(false, exception.getErrorCode().getStatus().value(), 1, message, exception.getErrors());
}
public static ErrorResponse of(CustomException exception) {
@ -40,7 +44,7 @@ public class ErrorResponse extends BaseResponse<Void> {
}
public static ErrorResponse of(Exception exception) {
return new ErrorResponse(false, HttpStatus.INTERNAL_SERVER_ERROR.value(), exception.getMessage(), null);
return new ErrorResponse(false, HttpStatus.INTERNAL_SERVER_ERROR.value(), HttpStatus.INTERNAL_SERVER_ERROR.value(), exception.getMessage(), null);
}
/**
@ -59,8 +63,8 @@ public class ErrorResponse extends BaseResponse<Void> {
// 글로벌 에러 리스트 생성
List<CustomError> globalErrors = errors.getGlobalErrors().stream()
.map(e -> new CustomError(null, // 필드 이름이 없으므로 null
e.getCode(),
e.getDefaultMessage(),
e.getCode(),
e.getDefaultMessage(),
e.getObjectName()
))
.toList();

View File

@ -17,7 +17,7 @@ public class SuccessResponse<T> extends BaseResponse<T> {
* 성공 응답 객체 생성자
*/
public SuccessResponse() {
super(true, HttpStatus.OK.value(), "success");
super(true, HttpStatus.OK.value(), 200, "success");
}
/**
@ -26,7 +26,7 @@ public class SuccessResponse<T> extends BaseResponse<T> {
* @param data 성공시 반환하는 데이터
*/
public SuccessResponse(T data) {
super(true, 200, "success");
super(true, 200, 200, "success");
super.data = data;
}