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

Seventh assginment #10

Open
wants to merge 7 commits into
base: Seminars
Choose a base branch
from
Open
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
1 change: 0 additions & 1 deletion Assignments/README.md

This file was deleted.

4 changes: 4 additions & 0 deletions ThirdSeminar/.gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
HELP.md
.gradle
build/
src/main/resources/application.yml
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
Expand All @@ -25,6 +26,9 @@ bin/
out/
!**/src/main/**/out/
!**/src/test/**/out/
*.yml
*.yaml
application.properties

### NetBeans ###
/nbproject/private/
Expand Down
16 changes: 16 additions & 0 deletions ThirdSeminar/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,25 @@ repositories {

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'mysql:mysql-connector-java:8.0.33'
implementation 'org.springframework.boot:spring-boot-starter-validation'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'

// Health Check
implementation 'org.springframework.boot:spring-boot-starter-actuator'

//JWT
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
implementation group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
implementation group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'

implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-aws', version: '2.2.6.RELEASE'

//swagger
implementation 'org.springdoc:springdoc-openapi-ui:1.7.0'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing
@SpringBootApplication
public class ThirdSeminarApplication {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package sopt.org.ThirdSeminar.common.advice;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import sopt.org.ThirdSeminar.common.dto.ApiResponseDto;
import sopt.org.ThirdSeminar.exception.ErrorStatus;
import sopt.org.ThirdSeminar.exception.model.SoptException;

import java.util.Objects;

@RestControllerAdvice
public class ControllerExceptionAdvice {

/**
* 400 BAD_REQUEST
*/

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
protected ApiResponseDto handleMethodArgumentNotValidException(final MethodArgumentNotValidException e) {
FieldError fieldError = Objects.requireNonNull(e.getFieldError());
return ApiResponseDto.error(ErrorStatus.REQUEST_VALIDATION_EXCEPTION, String.format("%s. (%s)", fieldError.getDefaultMessage(), fieldError.getField()));
}

/**
* 500 Internal Server
*/
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Exception.class)
protected ApiResponseDto<Object> handleException(final Exception e) {
e.printStackTrace();
return ApiResponseDto.error(ErrorStatus.INTERNAL_SERVER_ERROR);
}

/**
* Sopt custom error
*/
@ExceptionHandler(SoptException.class)
protected ResponseEntity<ApiResponseDto> handleSoptException(SoptException e) {
return ResponseEntity.status(e.getHttpStatus())
.body(ApiResponseDto.error(e.getError(), e.getMessage()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package sopt.org.ThirdSeminar.common.dto;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import sopt.org.ThirdSeminar.exception.ErrorStatus;
import sopt.org.ThirdSeminar.exception.SuccessStatus;

@Getter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class ApiResponseDto<T> {
private final int code;
private final String message;
private T data;

public static ApiResponseDto success(SuccessStatus successStatus) {
return new ApiResponseDto<>(successStatus.getHttpStatus().value(), successStatus.getMessage());
}

public static <T> ApiResponseDto<T> success(SuccessStatus successStatus, T data) {
return new ApiResponseDto<T>(successStatus.getHttpStatus().value(), successStatus.getMessage(), data);
}

public static ApiResponseDto error(ErrorStatus errorStatus) {
return new ApiResponseDto<>(errorStatus.getHttpStatus().value(), errorStatus.getMessage());
}

public static ApiResponseDto error(ErrorStatus errorStatus, String message) {
return new ApiResponseDto<>(errorStatus.getHttpStatusCode(), message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package sopt.org.ThirdSeminar.config;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import sopt.org.ThirdSeminar.config.resolver.UserIdResolver;

import java.util.List;

@RequiredArgsConstructor
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final UserIdResolver userIdResolver;

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(userIdResolver);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package sopt.org.ThirdSeminar.config.jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import sopt.org.ThirdSeminar.exception.ErrorStatus;
import sopt.org.ThirdSeminar.exception.model.UnauthorizedException;

import javax.annotation.PostConstruct;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Base64;
import java.util.Date;

@Service
public class JwtService {
@Value("${jwt.secret}")
private String jwtSecret;

@PostConstruct
protected void init() {
jwtSecret = Base64.getEncoder()
.encodeToString(jwtSecret.getBytes(StandardCharsets.UTF_8));
}

// JWT 토큰 발급
public String issuedToken(String userId) {
final Date now = new Date();

// 클레임 생성
final Claims claims = Jwts.claims()
.setSubject("access_token")
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + 120 * 60 * 1000L));

//private claim 등록
claims.put("userId", userId);

return Jwts.builder()
.setHeaderParam(Header.TYPE , Header.JWT_TYPE)
.setClaims(claims)
.signWith(getSigningKey())
.compact();
}

private Key getSigningKey() {
final byte[] keyBytes = jwtSecret.getBytes(StandardCharsets.UTF_8);
return Keys.hmacShaKeyFor(keyBytes);
}

// JWT 토큰 검증
public boolean verifyToken(String token) {
try {
final Claims claims = getBody(token);
return true;
} catch (RuntimeException e) {
if (e instanceof ExpiredJwtException) {
throw new UnauthorizedException(ErrorStatus.TOKEN_TIME_EXPIRED_EXCEPTION, ErrorStatus.TOKEN_TIME_EXPIRED_EXCEPTION.getMessage());
}
return false;
}
}

private Claims getBody(final String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}

// JWT 토큰 내용 확인
public String getJwtContents(String token) {
final Claims claims = getBody(token);
return (String) claims.get("userId");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package sopt.org.ThirdSeminar.config.resolver;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserId {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package sopt.org.ThirdSeminar.config.resolver;

import lombok.RequiredArgsConstructor;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import sopt.org.ThirdSeminar.config.jwt.JwtService;

import javax.servlet.http.HttpServletRequest;

@RequiredArgsConstructor
@Component
public class UserIdResolver implements HandlerMethodArgumentResolver {
private final JwtService jwtService;


@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(UserId.class) && Long.class.equals(parameter.getParameterType());
}

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
final HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
final String token = request.getHeader("Authorization").split(" ")[1];

// 토큰 검증
if (!jwtService.verifyToken(token)) {
throw new RuntimeException(String.format("USER_ID를 가져오지 못했습니다. (%s - %s)", parameter.getClass(), parameter.getMethod()));
}

// 유저 아이디 반환
final String tokenContents = jwtService.getJwtContents(token);
try {
return Long.parseLong(tokenContents);
} catch (NumberFormatException e) {
throw new RuntimeException(String.format("USER_ID를 가져오지 못했습니다. (%s - %s)", parameter.getClass(), parameter.getMethod()));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package sopt.org.ThirdSeminar.config.swagger;

import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@SecurityScheme(
name = "JWT Auth",
type = SecuritySchemeType.HTTP,
bearerFormat = "JWT",
scheme = "bearer"
)
public class SwaggerConfig {

@Bean
public OpenAPI openAPI() {
Info info = new Info()
.title("SOPT32nd Seminar project")
.description("SOPT32nd Seminar project API Document")
.version("1.0.0");

return new OpenAPI()
.components(new Components())
.info(info);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package sopt.org.ThirdSeminar.controller;


import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import sopt.org.ThirdSeminar.common.dto.ApiResponseDto;
import sopt.org.ThirdSeminar.config.jwt.JwtService;
import sopt.org.ThirdSeminar.config.resolver.UserId;
import sopt.org.ThirdSeminar.controller.dto.request.BoardImageListRequestDto;
import sopt.org.ThirdSeminar.exception.SuccessStatus;
import sopt.org.ThirdSeminar.external.client.aws.S3Service;
import sopt.org.ThirdSeminar.service.BoardService;

import javax.validation.Valid;
import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping("/board")
@SecurityRequirement(name = "JWT Auth")
public class BoardController {

private final BoardService boardService;
private final JwtService jwtService;
private final S3Service s3Service;

@PostMapping(value = "/create", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.CREATED)
public ApiResponseDto create(
@UserId Long userId,
@ModelAttribute @Valid final BoardImageListRequestDto request) {
List<String> boardThumbnailImageUrlList = s3Service.uploadImages(request.getBoardImages(), "board");
boardService.create(userId, boardThumbnailImageUrlList, request);
return ApiResponseDto.success(SuccessStatus.CREATE_BOARD_SUCCESS);
}
}
Loading