Skip to content
Merged
40 changes: 33 additions & 7 deletions src/main/java/in/koreatech/koin/domain/auth/JwtProvider.java
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
package in.koreatech.koin.domain.auth;

import in.koreatech.koin.domain.auth.exception.AuthException;
import in.koreatech.koin.domain.user.exception.UserNotFoundException;
import in.koreatech.koin.domain.user.model.User;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.time.Instant;
import java.util.Base64;
import java.util.Date;

import javax.crypto.SecretKey;

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import in.koreatech.koin.domain.user.model.User;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;

@Component
@RequiredArgsConstructor
public class JwtProvider {

private static final String BEARER_PREFIX = "BEARER ";

@Value("${jwt.secret-key}")
private String secretKey;

Expand All @@ -25,7 +29,7 @@ public class JwtProvider {

public String createToken(User user) {
if (user == null) {
throw new IllegalArgumentException("존재하지 않는 사용자입니다.");
throw new UserNotFoundException("존재하지 않는 사용자입니다. user: " + user);
}

Key key = getSecretKey();
Expand All @@ -40,8 +44,30 @@ public String createToken(User user) {
.compact();
}

public Long getUserId(String requestToken) {
if (requestToken == null || !requestToken.toUpperCase().startsWith(BEARER_PREFIX)) {
throw AuthException.withDetail("token: " + requestToken);
}
String token = requestToken.substring(BEARER_PREFIX.length());

try {
String userId = Jwts.parser()
.verifyWith(getSecretKey())
.build()
.parseSignedClaims(token)
.getPayload()
.get("id")
.toString();
return Long.parseLong(userId);

} catch (JwtException e) {
throw AuthException.withDetail("token: " + token);
}
}

private SecretKey getSecretKey() {
String encoded = Base64.getEncoder().encodeToString(secretKey.getBytes());
return Keys.hmacShaKeyFor(encoded.getBytes());
}

}
14 changes: 14 additions & 0 deletions src/main/java/in/koreatech/koin/domain/auth/StudentAuth.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package in.koreatech.koin.domain.auth;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

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

@Target({PARAMETER, FIELD})
@Retention(RUNTIME)
public @interface StudentAuth {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package in.koreatech.koin.domain.auth.exception;

public class AuthException extends RuntimeException {

private static final String DEFAULT_MESSAGE = "올바르지 않은 인증정보입니다.";

public AuthException() {
}

public AuthException(String message) {
super(message);
}

public static AuthException withDetail(String detail) {
String message = String.format("%s %s", DEFAULT_MESSAGE, detail);
return new AuthException(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package in.koreatech.koin.domain.auth.resolver;

import in.koreatech.koin.domain.auth.JwtProvider;
import in.koreatech.koin.domain.auth.StudentAuth;
import in.koreatech.koin.domain.auth.exception.AuthException;
import in.koreatech.koin.domain.user.exception.UserNotFoundException;
import in.koreatech.koin.domain.user.repository.StudentRepository;
import jakarta.servlet.http.HttpServletRequest;
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;

@Component
@RequiredArgsConstructor
public class StudentArgumentResolver implements HandlerMethodArgumentResolver {

private static final String AUTHORIZATION = "Authorization";

private final JwtProvider jwtProvider;
private final StudentRepository studentRepository;

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(StudentAuth.class);
}

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {

HttpServletRequest nativeRequest = webRequest.getNativeRequest(HttpServletRequest.class);
if (nativeRequest == null) {
throw new AuthException("요청 값이 비어있습니다.");
}

String authorizationHeader = nativeRequest.getHeader(AUTHORIZATION);
if (authorizationHeader == null) {
throw new AuthException("인증 헤더값이 비어있습니다. authorizationHeader: " + nativeRequest);
}
Long userId = jwtProvider.getUserId(authorizationHeader);
return studentRepository.findById(userId)
.orElseThrow(() -> UserNotFoundException.withDetail("authorizationHeader: " + authorizationHeader));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package in.koreatech.koin.domain.user.controller;

import in.koreatech.koin.domain.auth.StudentAuth;
import in.koreatech.koin.domain.user.model.Student;
import in.koreatech.koin.domain.user.dto.StudentResponse;
import in.koreatech.koin.domain.user.service.StudentService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class StudentController {

private final StudentService studentService;

@GetMapping("/user/student/me")
public ResponseEntity<StudentResponse> getStudent(@StudentAuth Student student) {
StudentResponse studentResponse = studentService.getStudent(student);
return ResponseEntity.ok().body(studentResponse);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package in.koreatech.koin.domain.user.dto;

import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import in.koreatech.koin.domain.user.model.Student;
import in.koreatech.koin.domain.user.model.User;

@JsonNaming(value = SnakeCaseStrategy.class)
public record StudentResponse(
String anonymousNickname,
String email,
String gender,
String major,
String name,
String nickname,
String phoneNumber,
String studentNumber
) {

public static StudentResponse from(Student student) {
User user = student.getUser();
return new StudentResponse(
student.getAnonymousNickname(),
user.getEmail(),
user.getGender().name(),
student.getDepartment(),
user.getName(),
user.getNickname(),
user.getPhoneNumber(),
student.getStudentNumber()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package in.koreatech.koin.domain.user.exception;

public class UserNotFoundException extends RuntimeException {

private static final String DEFAULT_MESSAGE = "존재하지 않는 사용자입니다.";

public UserNotFoundException(String message) {
super(message);
}

public static UserNotFoundException withDetail(String detail) {
String message = String.format("%s %s", DEFAULT_MESSAGE, detail);
return new UserNotFoundException(message);
}
}
24 changes: 23 additions & 1 deletion src/main/java/in/koreatech/koin/domain/user/model/Student.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,24 @@
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Id;
import jakarta.persistence.MapsId;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import jakarta.validation.constraints.Size;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@Table(name = "students")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Student {

@Id
private Long userId;
@Column(name = "user_id")
private Long id;

@Size(max = 255)
@Column(name = "anonymous_nickname")
Expand All @@ -35,4 +42,19 @@ public class Student {

@Column(name = "is_graduated")
private Boolean isGraduated;

@OneToOne
@MapsId
private User user;

@Builder
public Student(String anonymousNickname, String studentNumber, String department, UserIdentity userIdentity,
Boolean isGraduated, User user) {
this.anonymousNickname = anonymousNickname;
this.studentNumber = studentNumber;
this.department = department;
this.userIdentity = userIdentity;
this.isGraduated = isGraduated;
this.user = user;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package in.koreatech.koin.domain.user.repository;

import in.koreatech.koin.domain.user.model.Student;
import java.util.Optional;
import org.springframework.data.repository.Repository;

public interface StudentRepository extends Repository<Student, Long> {

Student save(Student student);

Optional<Student> findById(Long id);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package in.koreatech.koin.domain.user.service;

import in.koreatech.koin.domain.user.exception.UserNotFoundException;
import in.koreatech.koin.domain.user.model.Student;
import in.koreatech.koin.domain.user.dto.StudentResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class StudentService {

public StudentResponse getStudent(Student student) {
if (student == null || student.getId() == null) {
throw new UserNotFoundException("학생 정보가 비어있습니다.");
}
return StudentResponse.from(student);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R

이번 PR에서의 dto, repository, service 계층 경로가 이상해요 😵‍💫

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

패키지 변경작업 전에 stash해놨던게 여기저기 흩뿌려졌네요 ㅠㅠ
꼼꼼한리뷰 감사해요~ 반영하겠습니다!

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package in.koreatech.koin.domain.user.service;

import in.koreatech.koin.domain.user.exception.UserNotFoundException;
import java.time.LocalDateTime;
import java.util.UUID;

Expand Down Expand Up @@ -27,10 +28,10 @@ public class UserService {
@Transactional
public UserLoginResponse login(UserLoginRequest request) {
User user = userRepository.findByEmail(request.email())
.orElseThrow(() -> new IllegalArgumentException("잘못된 로그인 정보입니다."));
.orElseThrow(() -> UserNotFoundException.withDetail("request: " + request));

if (!user.isSamePassword(request.password())) {
throw new IllegalArgumentException("잘못된 로그인 정보입니다.");
throw new IllegalArgumentException("비밀번호가 틀렸습니다. request: " + request);
}

String accessToken = jwtProvider.createToken(user);
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/in/koreatech/koin/global/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package in.koreatech.koin.global.config;

import in.koreatech.koin.domain.auth.resolver.StudentArgumentResolver;
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;

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {

private final StudentArgumentResolver studentArgumentResolver;

@Override
public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(studentArgumentResolver);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package in.koreatech.koin.global.exception;

import lombok.Getter;

@Getter
public class ErrorResponse {

private final int code;
Copy link
Collaborator

@songsunkook songsunkook Jan 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A

현재까지는 code를 수정하는 곳이 없어서 에러 발생 시 항상 code 0을 반환하는 것으로 보이는데, 혹시 해당 변수의 의미가 무엇인지 알 수 있을까요??

작업 내용에 써있었네요 죄송합니다!

private final String message;

private ErrorResponse(int code, String message) {
this.code = code;
this.message = message;
}

public static ErrorResponse of(int code, String message) {
return new ErrorResponse(code, message);
}

public static ErrorResponse from(String message) {
return new ErrorResponse(0, message);
}
}
Loading