-
Notifications
You must be signed in to change notification settings - Fork 0
소셜 로그인 템플릿 적용 #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
base: develop
Are you sure you want to change the base?
Changes from all commits
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,92 @@ | ||
| package dorun.project.routineapp.api.controller; | ||
|
|
||
| import dorun.project.routineapp.api.entity.RefreshToken; | ||
| import dorun.project.routineapp.common.APIResponse; | ||
| import dorun.project.routineapp.config.properties.AppProperties; | ||
| import dorun.project.routineapp.oauth.entity.RoleType; | ||
| import dorun.project.routineapp.oauth.token.Token; | ||
| import dorun.project.routineapp.oauth.token.TokenProvider; | ||
| import dorun.project.routineapp.oauth.util.HeaderUtil; | ||
| import dorun.project.routineapp.api.repository.RefreshTokenRepository; | ||
| import io.jsonwebtoken.Claims; | ||
| import jakarta.servlet.http.HttpServletRequest; | ||
| import jakarta.servlet.http.HttpServletResponse; | ||
| import java.util.Date; | ||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
| import java.util.Optional; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.web.bind.annotation.PostMapping; | ||
| import org.springframework.web.bind.annotation.RequestMapping; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| @RestController | ||
| @RequestMapping("/api/v1/auth") | ||
| @RequiredArgsConstructor | ||
| public class AuthController { | ||
| private final AppProperties appProperties; | ||
| private final TokenProvider tokenProvider; | ||
| private final RefreshTokenRepository refreshTokenRepository; | ||
|
|
||
| private static final long UPDATE_TOKEN_STRATEGY = 259200000; // 3일 | ||
| private static final String ACCESS_TOKEN = "access_token"; | ||
| private static final String REFRESH_TOKEN = "refresh_token"; | ||
|
|
||
| @PostMapping("/refresh") | ||
| public APIResponse<Map<String, String>> refreshToken(HttpServletRequest request, HttpServletResponse response) { | ||
| String accessToken = HeaderUtil.getToken(request, ACCESS_TOKEN); | ||
| Token authToken = tokenProvider.convertToken(accessToken); | ||
| if (!authToken.validate()) { | ||
| return APIResponse.invalidRefreshToken(); | ||
| } | ||
|
|
||
| Claims claims = authToken.getExpiredTokenClaims(); | ||
| if (claims == null) { | ||
| return APIResponse.notExpiredTokenYet(); | ||
| } | ||
|
|
||
| String userId = claims.getSubject(); | ||
| RoleType roleType = RoleType.of(claims.get("role", String.class)); | ||
|
|
||
| String refreshToken = HeaderUtil.getToken(request, REFRESH_TOKEN); | ||
| Token authRefreshToken = tokenProvider.convertToken(refreshToken); | ||
|
|
||
| if (authRefreshToken.validate()) { | ||
| return APIResponse.invalidRefreshToken(); | ||
| } | ||
|
|
||
| Optional<RefreshToken> searchedRefreshToken = refreshTokenRepository.findByUserId(userId); | ||
| RefreshToken userRefreshToken; | ||
| if (searchedRefreshToken.isEmpty()) { | ||
| return APIResponse.invalidRefreshToken(); | ||
| } | ||
| userRefreshToken = searchedRefreshToken.get(); | ||
|
|
||
| Date now = new Date(); | ||
| Token newAccessToken = tokenProvider.createToken( | ||
| userId, | ||
| roleType.getCode(), | ||
| new Date(now.getTime() + appProperties.getAuth().getTokenExpiry()) | ||
| ); | ||
|
|
||
| long validTime = authRefreshToken.getTokenClaims().getExpiration().getTime() - now.getTime(); | ||
|
|
||
| if (validTime <= UPDATE_TOKEN_STRATEGY) { | ||
| long refreshTokenExpiry = appProperties.getAuth().getRefreshTokenExpiry(); | ||
|
|
||
| authRefreshToken = tokenProvider.createToken( | ||
| appProperties.getAuth().getTokenSecret(), | ||
| new Date(now.getTime() + refreshTokenExpiry) | ||
| ); | ||
|
|
||
| userRefreshToken.updateRefreshToken(authRefreshToken.getToken()); | ||
| refreshTokenRepository.save(userRefreshToken); | ||
| } | ||
|
|
||
| Map<String, String> tokens = new HashMap<>(); | ||
| tokens.put(ACCESS_TOKEN, newAccessToken.getToken()); | ||
| tokens.put(REFRESH_TOKEN, userRefreshToken.getRefreshToken()); | ||
|
|
||
| return APIResponse.success("tokens", tokens); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package dorun.project.routineapp.api.entity; | ||
|
|
||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.EntityListeners; | ||
| import jakarta.persistence.MappedSuperclass; | ||
| import java.time.LocalDateTime; | ||
| import lombok.Getter; | ||
| import org.springframework.data.annotation.CreatedDate; | ||
| import org.springframework.data.annotation.LastModifiedDate; | ||
| import org.springframework.data.jpa.domain.support.AuditingEntityListener; | ||
|
|
||
| @MappedSuperclass | ||
| @Getter | ||
| @EntityListeners(AuditingEntityListener.class) | ||
| public abstract class BaseEntity { | ||
|
Contributor
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. -추상클래스인 이유가 있을까요??
Contributor
Author
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. 당장은 특별한 이유는 없습니다. 다만 뭔가 명확한 단점이 있다면 일반 클래스로 사용해도 됩니다.
Contributor
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. 오! 그렇군요.
배워갑니다...
Contributor
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. Serializable 을 implements 하고 있는 것도 좋아보입니다!!
Contributor
Author
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. 넵 좋은 코멘트입니다. 수정하겠습니다! |
||
|
|
||
| @Column(name = "CREATED_AT") | ||
|
Contributor
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. ( updatable = false ) 옵션이 있으면 좋을 것 같습니다
Contributor
Author
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. 이 또한 좋은 코멘트입니다. 수정하겠습니다! |
||
| @CreatedDate | ||
| private LocalDateTime createdAt; | ||
|
|
||
| @Column(name = "MODIFIED_AT") | ||
| @LastModifiedDate | ||
| private LocalDateTime modifiedAt; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package dorun.project.routineapp.api.entity; | ||
|
|
||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
| import org.springframework.data.annotation.Id; | ||
| import org.springframework.data.redis.core.RedisHash; | ||
| import org.springframework.data.redis.core.index.Indexed; | ||
|
|
||
| @Getter | ||
| @AllArgsConstructor | ||
| @RedisHash(value = "refreshToken", timeToLive = 60 * 60 * 24 * 3) | ||
| public class RefreshToken { | ||
| @Id | ||
| private String UUID; | ||
| @Indexed | ||
| private String userId; | ||
| private String refreshToken; | ||
|
|
||
| public void updateRefreshToken(String refreshToken) { | ||
| this.refreshToken = refreshToken; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| package dorun.project.routineapp.api.entity; | ||
|
|
||
| import dorun.project.routineapp.oauth.entity.ProviderType; | ||
| import dorun.project.routineapp.oauth.entity.RoleType; | ||
| import com.fasterxml.jackson.annotation.JsonIgnore; | ||
| 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.Table; | ||
| import jakarta.validation.constraints.NotNull; | ||
| import jakarta.validation.constraints.Size; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
| import lombok.Setter; | ||
|
|
||
| @Builder | ||
| @Getter | ||
| @Setter | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| @Entity | ||
| @Table(name = "USER") | ||
| public class User extends BaseEntity { | ||
| @JsonIgnore | ||
| @Id | ||
| @Column(name = "USER_SEQ") | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long userSeq; | ||
|
|
||
| @Column(name = "USER_ID", length = 64, unique = true) | ||
| @NotNull | ||
| @Size(max = 64) | ||
| private String userId; | ||
|
|
||
| @Column(name = "USERNAME", length = 100) | ||
| @NotNull | ||
| @Size(max = 100) | ||
| private String username; | ||
|
|
||
| @JsonIgnore | ||
| @Column(name = "PASSWORD", length = 128) | ||
| @NotNull | ||
| @Size(max = 128) | ||
| private String password; | ||
|
|
||
| @Column(name = "EMAIL", length = 512, unique = true) | ||
| @NotNull | ||
| @Size(max = 512) | ||
| private String email; | ||
|
Contributor
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. provider 마다 같은 email을 사용할 수 도 있지 않을까요?
Contributor
Author
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. 이것은 사용자를 어떻게 관리할 것인지 논의가 조금 필요해보입니다. 하나의 이메일을 가진 사용자를 유일하게 설정하고 여러 provider에 대해 연동을 시켜줄지, 또는 provider 별로 이메일이 같은 사용자를 다른 사용자로 취급할지 등을 생각해봐야 할 것 같아요.
Contributor
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. 저의 경우 여러 소셜로그인을 사용하는데 다른 provider 같은 이메일을 사용하곤하는데
|
||
|
|
||
| @Column(name = "PROVIDER_TYPE", length = 20) | ||
| @Enumerated(EnumType.STRING) | ||
| @NotNull | ||
| private ProviderType providerType; | ||
|
|
||
| @Column(name = "ROLE_TYPE", length = 20) | ||
| @Enumerated(EnumType.STRING) | ||
| @NotNull | ||
| private RoleType roleType; | ||
|
|
||
| public User( | ||
| @NotNull @Size(max = 64) String userId, | ||
| @NotNull @Size(max = 100) String username, | ||
| @NotNull @Size(max = 512) String email, | ||
| @NotNull ProviderType providerType, | ||
| @NotNull RoleType roleType | ||
| ) { | ||
| this.userId = userId; | ||
| this.username = username; | ||
| this.password = "NO_PASS"; | ||
| this.email = email != null ? email : "NO_EMAIL"; | ||
|
Contributor
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. guest 로그인을 고려한 것으로 보이는데 만약 email 값이 null이라면 해당 유저가 guest라는 것을 boolean 변수 또는 enum를 통해서 설정하는것은 어떤가요? |
||
| this.providerType = providerType; | ||
| this.roleType = roleType; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package dorun.project.routineapp.api.repository; | ||
|
|
||
| import dorun.project.routineapp.api.entity.RefreshToken; | ||
| import java.util.Optional; | ||
| import org.springframework.data.repository.CrudRepository; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| @Repository | ||
| public interface RefreshTokenRepository extends CrudRepository<RefreshToken, String> { | ||
| Optional<RefreshToken> findByUserId(String userId); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package dorun.project.routineapp.api.repository; | ||
|
|
||
| import dorun.project.routineapp.api.entity.User; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| @Repository | ||
| public interface UserRepository extends JpaRepository<User, Long> { | ||
| User findByUserId(String userId); | ||
|
Contributor
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. Optional 을 이용해서 null에 대한 처리를 깔끔하게 할 수 있을 것이라고 생각합니다.
Contributor
Author
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. 수정하겠습니다. |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| package dorun.project.routineapp.common; | ||
|
|
||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
| import lombok.Getter; | ||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| @Getter | ||
| @RequiredArgsConstructor | ||
| public class APIResponse<T> { | ||
|
|
||
| private final static int SUCCESS = 200; | ||
| private final static int NOT_FOUND = 400; | ||
| private final static int FAILED = 500; | ||
| private final static String SUCCESS_MESSAGE = "SUCCESS"; | ||
| private final static String NOT_FOUND_MESSAGE = "NOT FOUND"; | ||
| private final static String FAILED_MESSAGE = "서버에서 오류가 발생하였습니다."; | ||
| private final static String INVALID_ACCESS_TOKEN = "Access Token이 유효하지 않습니다."; | ||
| private final static String INVALID_REFRESH_TOKEN = "Refresh Token이 유효하지 않습니다."; | ||
| private final static String NOT_EXPIRED_TOKEN_YET = "Token이 아직 만료되지 않았습니다."; | ||
|
|
||
| private final APIResponseHeader header; | ||
| private final Map<String, T> body; | ||
|
|
||
| public static <T> APIResponse<T> success(String name, T body) { | ||
| Map<String, T> map = new HashMap<>(); | ||
| map.put(name, body); | ||
|
|
||
| return new APIResponse<>(new APIResponseHeader(SUCCESS, SUCCESS_MESSAGE), map); | ||
| } | ||
|
|
||
| public static <T> APIResponse<T> fail() { | ||
| return new APIResponse<>(new APIResponseHeader(FAILED, FAILED_MESSAGE), null); | ||
| } | ||
|
|
||
| public static <T> APIResponse<T> invalidAccessToken() { | ||
| return new APIResponse<>(new APIResponseHeader(FAILED, INVALID_ACCESS_TOKEN), null); | ||
| } | ||
|
|
||
| public static <T> APIResponse<T> invalidRefreshToken() { | ||
| return new APIResponse<>(new APIResponseHeader(FAILED, INVALID_REFRESH_TOKEN), null); | ||
| } | ||
|
|
||
| public static <T> APIResponse<T> notExpiredTokenYet() { | ||
| return new APIResponse<>(new APIResponseHeader(FAILED, NOT_EXPIRED_TOKEN_YET), null); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| package dorun.project.routineapp.common; | ||
|
|
||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
| import lombok.Setter; | ||
|
|
||
| @Getter | ||
| @Setter | ||
| @AllArgsConstructor | ||
| public class APIResponseHeader { | ||
| private int code; | ||
| private String message; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package dorun.project.routineapp.config; | ||
|
|
||
| import dorun.project.routineapp.oauth.token.TokenProvider; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
|
|
||
| @Configuration | ||
| public class JwtConfig { | ||
|
Contributor
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. 이 클래스가 있어야 할 이유를 잘 모르겠습니다..!
Contributor
Author
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. 기본적인 목적은 yml 파일에서 바인딩되는 값이
Contributor
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. 제가 잘 몰라서 그러는데 yml 파일에서 가져오는 환경변수를 TokenProvider 클래스에서 직접 바인딩해서 사용하면 안되는 이유가 있을 까요?
Contributor
Author
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. '안 된다' 라기보다 생성할 때 이외에 사용할 일이 없고, 프로그램이 실행되는 동안 계속 사용되는 객체가 그 값을 들고 있어야 하는 이유도 딱히 없다고 생각했습니다. 부수적으로, yml 파일의 값을 바인딩하는 코드를 config 패키지에 모아두는 것도 목적이었습니다.
Contributor
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.
Contributor
Author
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. 이야기해주신 내용들이 구현을 꼼꼼하게 하지 못해서 그런 부분들입니다. 위에서 이야기한 목적에 부합하도록 수정해야 하는 부분이 맞습니다. |
||
| @Value("${jwt.secret}") | ||
| private String secret; | ||
|
|
||
| @Bean | ||
| public TokenProvider jwtProvider() { | ||
| return new TokenProvider(secret); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package dorun.project.routineapp.config; | ||
|
|
||
| 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 | ||
| public class OpenApiConfig { | ||
| @Bean | ||
| public OpenAPI openAPI() { | ||
| Info info = new Info() | ||
| .title("소셜 로그인 템플릿 프로젝트 API Document") | ||
| .version("v0.0.1") | ||
| .description("소셜 로그인 템플릿 프로젝트의 API 명세서입니다."); | ||
| return new OpenAPI() | ||
| .components(new Components()) | ||
| .info(info); | ||
| } | ||
| } |
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.
해당 변수를 yaml 파일로 빼는게 좋아보입니다.
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.
넵, 동의합니다. 그리고 리프레시 토큰을 레디스에 얼마나 저장할 지도 정해야 합니다. 임의로 3일로 해두었는데, 적당한 기간을 정해봐요.