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

[201800422 정세희] Level6 미션 제출합니다. #34

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
34 changes: 19 additions & 15 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,35 +1,39 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.2'
id 'io.spring.dependency-management' version '1.1.4'
id 'java'
id 'org.springframework.boot' version '3.2.2'
id 'io.spring.dependency-management' version '1.1.4'
}

group = 'backend.likelion'
version = '0.0.1-SNAPSHOT'

java {
sourceCompatibility = '21'
sourceCompatibility = '21'
}

configurations {
compileOnly {
extendsFrom annotationProcessor
}
compileOnly {
extendsFrom annotationProcessor
}
}

repositories {
mavenCentral()
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.rest-assured:rest-assured'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'

// JWT
implementation 'com.auth0:java-jwt:4.4.0'

compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.rest-assured:rest-assured'
}

tasks.named('test') {
useJUnitPlatform()
useJUnitPlatform()
}
8 changes: 5 additions & 3 deletions src/main/java/backend/likelion/todos/TodosApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;

@ConfigurationPropertiesScan
@SpringBootApplication
public class TodosApplication {

public static void main(String[] args) {
SpringApplication.run(TodosApplication.class, args);
}
public static void main(String[] args) {
SpringApplication.run(TodosApplication.class, args);
}

}
11 changes: 11 additions & 0 deletions src/main/java/backend/likelion/todos/auth/Auth.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package backend.likelion.todos.auth;

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 Auth {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package backend.likelion.todos.auth;

import backend.likelion.todos.auth.jwt.JwtService;
import backend.likelion.todos.common.UnAuthorizedException;
import lombok.RequiredArgsConstructor;
import org.apache.tomcat.util.http.parser.Authorization;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
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 javax.security.sasl.AuthorizeCallback;

@RequiredArgsConstructor
@Component
public class AuthArgumentResolver implements HandlerMethodArgumentResolver {

private final JwtService jwtService;

@Override
public boolean supportsParameter(MethodParameter parameter) {
// TODO [6단계] parameter가 @Auth 어노테이션을 갖고 있고, 파라미터 타입이 Long.class인 경우 true를 반환하는 조건을 구현하세요.
if (parameter.hasParameterAnnotation(Auth.class)) {
return parameter.getParameterType().equals(Long.class);
}
return false;
}

@Override
public Object resolveArgument(
MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory
) {
// TODO [6단계] webRequest로부터 accessToken을 추출하고, jwtService를 사용하여 memberId를 추출하여 반환하는 로직을 구현하세요.
String accessToken = extractAccessToken(webRequest);
return jwtService.extractMemberId(accessToken);
}

private static String extractAccessToken(NativeWebRequest request) {
// TODO [6단계] request 헤더에서 "Authorization" 헤더 값을 추출하여 "Bearer "로 시작하는 accessToken을 반환하세요. 유효하지 않을 경우 "로그인 후 접근할 수 있습니다." 메시지와 함께 UnAuthorizedException을 발생시키는 로직을 구현하세요.
String accessToken = request.getHeader("Authorization");
try {
if (accessToken.startsWith("Bearer ")) {
return accessToken;
}
} catch (UnAuthorizedException e) {
// e.getMessage("로그인 후 접근할 수 있습니다.");
throw new UnAuthorizedException("로그인 후 접근할 수 있습니다.");
}
return null;
}

}
21 changes: 21 additions & 0 deletions src/main/java/backend/likelion/todos/auth/AuthConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package backend.likelion.todos.auth;

import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@RequiredArgsConstructor
@Configuration
public class AuthConfig implements WebMvcConfigurer {

private final AuthArgumentResolver authArgumentResolver;

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
// TODO [6단계] resolvers 리스트에 authArgumentResolver를 추가하세요.
resolvers.add(authArgumentResolver);
}

}
17 changes: 17 additions & 0 deletions src/main/java/backend/likelion/todos/auth/AuthTestController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package backend.likelion.todos.auth;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/test")
public class AuthTestController {

@GetMapping("/auth")
public String test(
@Auth Long memberId
) {
return "아이디가 " + memberId + "인 회원 인증 성공!";
}
}
10 changes: 10 additions & 0 deletions src/main/java/backend/likelion/todos/auth/jwt/JwtProperty.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package backend.likelion.todos.auth.jwt;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("jwt")
public record JwtProperty(
String secretKey,
Long accessTokenExpirationDay
) {
}
46 changes: 46 additions & 0 deletions src/main/java/backend/likelion/todos/auth/jwt/JwtService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package backend.likelion.todos.auth.jwt;

import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;

import backend.likelion.todos.common.UnAuthorizedException;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import java.util.Date;
import org.springframework.stereotype.Service;

@Service
public class JwtService {

private final long accessTokenExpirationDayToMills;
private final Algorithm algorithm;

public JwtService(JwtProperty jwtProperty) {
this.accessTokenExpirationDayToMills =
MILLISECONDS.convert(jwtProperty.accessTokenExpirationDay(), DAYS);
this.algorithm = Algorithm.HMAC512(jwtProperty.secretKey());
}

public String createToken(Long memberId) {
return JWT.create()
.withExpiresAt(new Date(
accessTokenExpirationDayToMills + System.currentTimeMillis()
))
.withIssuedAt(new Date())
.withClaim("memberId", memberId)
.sign(algorithm);
}

public Long extractMemberId(String token) {
try {
return JWT.require(algorithm)
.build()
.verify(token)
.getClaim("memberId")
.asLong();
} catch (JWTVerificationException e) {
throw new UnAuthorizedException("유효하지 않은 토큰입니다.");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package backend.likelion.todos.common;

public class BadRequestException extends RuntimeException {

public BadRequestException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package backend.likelion.todos.common;

public class ConflictException extends RuntimeException {

public ConflictException(String message) {
super(message);
}
}
55 changes: 55 additions & 0 deletions src/main/java/backend/likelion/todos/common/ExceptionAdvice.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package backend.likelion.todos.common;

import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class ExceptionAdvice {

@ExceptionHandler(BadRequestException.class)
public ResponseEntity<ExceptionResponse> handle(BadRequestException e) {
return ResponseEntity.badRequest()
.body(new ExceptionResponse(e.getMessage()));
}

@ExceptionHandler(UnAuthorizedException.class)
public ResponseEntity<ExceptionResponse> handle(UnAuthorizedException e) {
return new ResponseEntity<>(
new ExceptionResponse(e.getMessage()),
HttpStatusCode.valueOf(HttpStatus.UNAUTHORIZED.value())
);
}

@ExceptionHandler(ForbiddenException.class)
public ResponseEntity<ExceptionResponse> handle(ForbiddenException e) {
return new ResponseEntity<>(
new ExceptionResponse(e.getMessage()),
HttpStatusCode.valueOf(HttpStatus.FORBIDDEN.value())
);
}

@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ExceptionResponse> handle(NotFoundException e) {
return new ResponseEntity<>(
new ExceptionResponse(e.getMessage()),
HttpStatusCode.valueOf(HttpStatus.NOT_FOUND.value())
);
}

@ExceptionHandler(ConflictException.class)
public ResponseEntity<ExceptionResponse> handle(ConflictException e) {
return new ResponseEntity<>(
new ExceptionResponse(e.getMessage()),
HttpStatusCode.valueOf(HttpStatus.CONFLICT.value())
);
}

@ExceptionHandler(InternalServerErrorException.class)
public ResponseEntity<ExceptionResponse> handle(InternalServerErrorException e) {
return ResponseEntity.internalServerError()
.body(new ExceptionResponse(e.getMessage()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package backend.likelion.todos.common;

public record ExceptionResponse(
String message
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package backend.likelion.todos.common;

public class ForbiddenException extends RuntimeException {

public ForbiddenException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package backend.likelion.todos.common;

public class InternalServerErrorException extends RuntimeException {

public InternalServerErrorException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package backend.likelion.todos.common;

public class NotFoundException extends RuntimeException {

public NotFoundException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package backend.likelion.todos.common;

public class UnAuthorizedException extends RuntimeException {

public UnAuthorizedException(String message) {
super(message);
}
}
35 changes: 35 additions & 0 deletions src/main/java/backend/likelion/todos/goal/Goal.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package backend.likelion.todos.goal;

import backend.likelion.todos.common.ForbiddenException;
import backend.likelion.todos.member.Member;
import lombok.Getter;

@Getter
public class Goal {

private Long id;
private String name;
private String color;
private Member member;

public Goal(String name, String color, Member member) {
this.name = name;
this.color = color;
this.member = member;
}

public void setId(Long id) {
this.id = id;
}

public void validateMember(Member member) {
if (!this.member.equals(member)) {
throw new ForbiddenException("해당 목표에 대한 권한이 없습니다.");
}
}

public void update(String name, String color) {
this.name = name;
this.color = color;
}
}
Loading