-
Notifications
You must be signed in to change notification settings - Fork 1
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
feat: POST /user/login API 구현 #50
Changes from all commits
a06b56f
e75d979
d3a2db3
d58b1c3
fd7c1d3
d2357b7
9dfd948
ad06150
61e703a
7e2acdd
ece89a8
980b80f
d5ef177
4c4f357
538aae0
a7e67d1
f27f9ca
2e779bc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package in.koreatech.koin.auth; | ||
|
||
import in.koreatech.koin.domain.user.User; | ||
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 org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.stereotype.Component; | ||
|
||
@Component | ||
public class JwtProvider { | ||
|
||
@Value("${jwt.secret-key}") | ||
private String secretKey; | ||
|
||
@Value("${jwt.access-token.expiration-time}") | ||
private Long expirationTime; | ||
|
||
public String createToken(User user) { | ||
if (user == null) { | ||
throw new IllegalArgumentException("존재하지 않는 사용자입니다."); | ||
} | ||
|
||
Key key = getSecretKey(); | ||
return Jwts.builder() | ||
.signWith(key) | ||
.header() | ||
.add("typ", "JWT") | ||
.add("alg", key.getAlgorithm()) | ||
.and() | ||
.claim("id", user.getId()) | ||
.expiration(new Date(Instant.now().toEpochMilli() + expirationTime)) | ||
.compact(); | ||
} | ||
|
||
private SecretKey getSecretKey() { | ||
String encoded = Base64.getEncoder().encodeToString(secretKey.getBytes()); | ||
return Keys.hmacShaKeyFor(encoded.getBytes()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package in.koreatech.koin.controller; | ||
|
||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.web.bind.MethodArgumentNotValidException; | ||
import org.springframework.web.bind.annotation.ExceptionHandler; | ||
import org.springframework.web.bind.annotation.RestControllerAdvice; | ||
|
||
@Slf4j | ||
@RestControllerAdvice | ||
public class GlobalExceptionHandler { | ||
|
||
@ExceptionHandler | ||
public ResponseEntity<String> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { | ||
log.warn(e.getMessage()); | ||
return ResponseEntity.badRequest().body(e.getMessage()); | ||
} | ||
|
||
@ExceptionHandler | ||
public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException e) { | ||
log.warn(e.getMessage()); | ||
return ResponseEntity.badRequest().body(e.getMessage()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package in.koreatech.koin.controller; | ||
|
||
import in.koreatech.koin.dto.UserLoginRequest; | ||
import in.koreatech.koin.dto.UserLoginResponse; | ||
import in.koreatech.koin.service.UserService; | ||
import jakarta.validation.Valid; | ||
import java.net.URI; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.PostMapping; | ||
import org.springframework.web.bind.annotation.RequestBody; | ||
import org.springframework.web.bind.annotation.RestController; | ||
|
||
@RestController | ||
@RequiredArgsConstructor | ||
public class UserController { | ||
|
||
private final UserService userService; | ||
|
||
@PostMapping("/user/login") | ||
public ResponseEntity<UserLoginResponse> login(@RequestBody @Valid UserLoginRequest request) { | ||
UserLoginResponse response = userService.login(request); | ||
return ResponseEntity.created(URI.create("/")) | ||
.body(response); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package in.koreatech.koin.domain.user; | ||
|
||
import jakarta.persistence.Column; | ||
import jakarta.persistence.Entity; | ||
import jakarta.persistence.EnumType; | ||
import jakarta.persistence.Enumerated; | ||
import jakarta.persistence.Id; | ||
import jakarta.persistence.Table; | ||
import jakarta.validation.constraints.Size; | ||
import lombok.Getter; | ||
|
||
@Getter | ||
@Entity | ||
@Table(name = "students") | ||
public class Student { | ||
|
||
@Id | ||
private Long userId; | ||
|
||
@Size(max = 255) | ||
@Column(name = "anonymous_nickname") | ||
private String anonymousNickname = "익명_" + System.currentTimeMillis(); | ||
|
||
@Size(max = 20) | ||
@Column(name = "student_number", length = 20) | ||
private String studentNumber; | ||
|
||
@Size(max = 50) | ||
@Column(name = "major", length = 50) | ||
private String department; | ||
|
||
@Column(name = "identity") | ||
@Enumerated(EnumType.STRING) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
private UserIdentity userIdentity; | ||
|
||
@Column(name = "is_graduated") | ||
private Boolean isGraduated; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
package in.koreatech.koin.domain.user; | ||
|
||
import in.koreatech.koin.domain.BaseEntity; | ||
import jakarta.persistence.Column; | ||
import jakarta.persistence.Entity; | ||
import jakarta.persistence.EnumType; | ||
import jakarta.persistence.Enumerated; | ||
import jakarta.persistence.GeneratedValue; | ||
import jakarta.persistence.GenerationType; | ||
import jakarta.persistence.Id; | ||
import jakarta.persistence.Lob; | ||
import jakarta.persistence.Table; | ||
import jakarta.validation.constraints.NotNull; | ||
import jakarta.validation.constraints.Size; | ||
import java.time.LocalDateTime; | ||
import lombok.AccessLevel; | ||
import lombok.Builder; | ||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
|
||
@Getter | ||
@Entity | ||
@Table(name = "users") | ||
@NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
public class User extends BaseEntity { | ||
|
||
@Id | ||
@GeneratedValue(strategy = GenerationType.IDENTITY) | ||
private Long id; | ||
|
||
@NotNull | ||
@Lob | ||
@Column(name = "password", nullable = false) | ||
private String password; | ||
|
||
@Size(max = 50) | ||
@Column(name = "nickname", length = 50) | ||
private String nickname; | ||
|
||
@Size(max = 50) | ||
@Column(name = "name", length = 50) | ||
private String name; | ||
|
||
@Size(max = 20) | ||
@Column(name = "phone_number", length = 20) | ||
private String phoneNumber; | ||
|
||
@NotNull | ||
@Enumerated(EnumType.STRING) | ||
@Column(name = "user_type", nullable = false, length = 20) | ||
private UserType userType; | ||
|
||
@Size(max = 100) | ||
@NotNull | ||
@Column(name = "email", nullable = false, length = 100) | ||
private String email; | ||
|
||
@Column(name = "gender") | ||
@Enumerated(value = EnumType.ORDINAL) | ||
private UserGender gender; | ||
|
||
@NotNull | ||
@Column(name = "is_authed", nullable = false) | ||
private Boolean isAuthed = false; | ||
|
||
@Column(name = "last_logged_at") | ||
private LocalDateTime lastLoggedAt; | ||
|
||
@Size(max = 255) | ||
@Column(name = "profile_image_url") | ||
private String profileImageUrl; | ||
|
||
@NotNull | ||
@Column(name = "is_deleted", nullable = false) | ||
private Boolean isDeleted = false; | ||
|
||
@Size(max = 255) | ||
@Column(name = "auth_token") | ||
private String authToken; | ||
|
||
@Size(max = 255) | ||
@Column(name = "auth_expired_at") | ||
private String authExpiredAt; | ||
|
||
@Size(max = 255) | ||
@Column(name = "reset_token") | ||
private String resetToken; | ||
|
||
@Size(max = 255) | ||
@Column(name = "reset_expired_at") | ||
private String resetExpiredAt; | ||
|
||
@Builder | ||
public User(String password, String nickname, String name, String phoneNumber, UserType userType, | ||
String email, | ||
UserGender gender, Boolean isAuthed, LocalDateTime lastLoggedAt, String profileImageUrl, | ||
Boolean isDeleted, | ||
String authToken, String authExpiredAt, String resetToken, String resetExpiredAt) { | ||
this.password = password; | ||
this.nickname = nickname; | ||
this.name = name; | ||
this.phoneNumber = phoneNumber; | ||
this.userType = userType; | ||
this.email = email; | ||
this.gender = gender; | ||
this.isAuthed = isAuthed; | ||
this.lastLoggedAt = lastLoggedAt; | ||
this.profileImageUrl = profileImageUrl; | ||
this.isDeleted = isDeleted; | ||
this.authToken = authToken; | ||
this.authExpiredAt = authExpiredAt; | ||
this.resetToken = resetToken; | ||
this.resetExpiredAt = resetExpiredAt; | ||
} | ||
|
||
public boolean isSamePassword(String password) { | ||
return this.password.equals(password); | ||
} | ||
|
||
public void updateLastLoggedTime(LocalDateTime lastLoggedTime) { | ||
lastLoggedAt = lastLoggedTime; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package in.koreatech.koin.domain.user; | ||
|
||
public enum UserGender { | ||
MAN, | ||
WOMAN, | ||
; | ||
Comment on lines
+4
to
+6
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 마지막 enum에는 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 다음 아티클을 읽고 후행쉼표를 사용해봤습니다. 간단하게 요약하면 추후 추가적으로 생기는 Enum 프로퍼티에 대해 PR 제출 시 라인 변경사항이 줄어든다는 장점이 있다고 합니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 다음 항목을 입력하기도 편하고, pr을 했을 때 새로운 항목만 표시된다는 장점이 있군요..! 😮 |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package in.koreatech.koin.domain.user; | ||
|
||
import lombok.Getter; | ||
|
||
/** | ||
* 신원 (0: 학생, 1: 대학원생, 2: 교수, 3: 교직원, 4: 졸업생, 5: 점주) | ||
*/ | ||
@Getter | ||
public enum UserIdentity { | ||
UNDERGRADUATE("학부생"), | ||
GRADUATE("대학원생"), | ||
PROFESSOR("교수"), | ||
STAFF("교직원"), | ||
ALUMNI("졸업생"), | ||
OWNER("점주"), | ||
; | ||
|
||
private final String value; | ||
|
||
UserIdentity(String value) { | ||
this.value = value; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package in.koreatech.koin.domain.user; | ||
|
||
import java.util.concurrent.TimeUnit; | ||
import lombok.Getter; | ||
import org.springframework.data.annotation.Id; | ||
import org.springframework.data.redis.core.RedisHash; | ||
import org.springframework.data.redis.core.TimeToLive; | ||
|
||
@Getter | ||
@RedisHash("refreshToken") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 확인해보니 Redis의 따라서 추후 제작될 리프레시 API에서 redis에 저장된 리프레시 토큰과 사용자가 제시한 리프레시 토큰을 비교해 검증하는 로직에서
모두 검사할 수 있어야 할 것 같아요 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
public class UserToken { | ||
|
||
private static final long REFRESH_TOKEN_EXPIRE_DAY = 14L; | ||
|
||
@Id | ||
private Long id; | ||
|
||
private final String refreshToken; | ||
|
||
@TimeToLive(unit = TimeUnit.DAYS) | ||
private final Long expiration; | ||
|
||
private UserToken(Long id, String refreshToken, Long expiration) { | ||
this.id = id; | ||
this.refreshToken = refreshToken; | ||
this.expiration = expiration; | ||
} | ||
|
||
public static UserToken create(Long userId, String refreshToken) { | ||
return new UserToken(userId, refreshToken, REFRESH_TOKEN_EXPIRE_DAY); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package in.koreatech.koin.domain.user; | ||
|
||
import lombok.Getter; | ||
|
||
@Getter | ||
public enum UserType { | ||
STUDENT("STUDENT", "학생"), | ||
USER("USER", "사용자"), | ||
; | ||
|
||
private final String value; | ||
private final String description; | ||
|
||
UserType(String value, String description) { | ||
this.value = value; | ||
this.description = description; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
시간 연산은 다양한 곳에서 활용될 것 같은데, 추후에라도 시간 계산을 별도 유틸리티 객체로 빼는 건 어떨까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
지금으로서는 어떻게 추상화시켜야할지 감이 잘 안오네용
추후에 동일한 연산이 사용된다면 분리해보면 좋을 것 같아요! 👍