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

액세스 토큰 만료시 Refresh 하는 기능 구현 #43

Merged
merged 24 commits into from
Jan 14, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6cd3748
chore(config): enable OpenFeign
jacobhboy Jan 8, 2024
65ae96f
chore(config): OpenFeign 설정 변경
jacobhboy Jan 8, 2024
00eb08a
chore(build): jwt 의존성 추가
jacobhboy Jan 10, 2024
6018394
chore(yml): jwt 설정 추가
jacobhboy Jan 10, 2024
660068e
feat(auth): 구글 auth feign 추가
jacobhboy Jan 10, 2024
5aa78ae
feat(auth): 구글 auth로 로그인 구현
jacobhboy Jan 10, 2024
dfbdb8f
feat(auth): 토큰 발급
jacobhboy Jan 10, 2024
20105d5
feat(user): 유저 추가 혹은 업데이트 구현
jacobhboy Jan 10, 2024
7887bd0
feat(auth): jwt credential 추가
jacobhboy Jan 10, 2024
121913e
feat(user): email 추가
jacobhboy Jan 10, 2024
a9fc11d
feat(auth): exception들 추가
jacobhboy Jan 10, 2024
0b4c6e1
feat(user): Role 제거
jacobhboy Jan 10, 2024
1dcb6b4
feat(auth): dto 추가
jacobhboy Jan 10, 2024
d880e66
chore(merge): 머지 하였음
jacobhboy Jan 10, 2024
aefb008
feat(user): Role 추가
jacobhboy Jan 10, 2024
e768859
feat(profile): getCurrentUser 추가
jacobhboy Jan 10, 2024
d2b50d2
feat(comment): getCurrentUser 추가
jacobhboy Jan 10, 2024
57c1208
feat(like): getCurrentUser 추가
jacobhboy Jan 10, 2024
fb3b48c
feat(auth): 현재 사용자를 저장하는 authRepository 구현
jacobhboy Jan 10, 2024
0b423f5
feat(auth): 인터셉터 구현
jacobhboy Jan 10, 2024
7270f7c
feat(user): 아이디로 유저 조회하는 기능 추가
jacobhboy Jan 10, 2024
a854d44
feat(auth): extract가 token을 받도록 변경
jacobhboy Jan 10, 2024
d972cd5
feat(auth): access token을 refresh하는 기능 구현
jacobhboy Jan 10, 2024
f9dead8
fix(auth): merge conflict 해결
jacobhboy Jan 14, 2024
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
19 changes: 19 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,20 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'

//openFeign
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:4.1.0'

//querydsl
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

//jwt
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
Expand All @@ -42,6 +50,17 @@ dependencies {
testRuntimeOnly 'com.h2database:h2'
}

//openFeign
ext {
set('springCloudVersion', "2023.0.0")
}

dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}


//checkStyle
compileJava.options.encoding = 'UTF-8'
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/sickgyun/server/auth/annotation/AdminOnly.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.sickgyun.server.auth.annotation;

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

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@LoginRequired
public @interface AdminOnly {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.sickgyun.server.auth.annotation;

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

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {
}
7 changes: 7 additions & 0 deletions src/main/java/com/sickgyun/server/auth/domain/Token.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.sickgyun.server.auth.domain;

public record Token(
String accessToken,
String refreshToken
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.sickgyun.server.auth.exception;

import org.springframework.http.HttpStatus;

import com.sickgyun.server.common.exception.SickgyunException;

public class NotBssmEmailException extends SickgyunException {
public NotBssmEmailException(String email) {
super(HttpStatus.BAD_REQUEST, email + "은 Bssm 이메일이 아닙니다.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.sickgyun.server.auth.exception;

import org.springframework.http.HttpStatus;

import com.sickgyun.server.common.exception.SickgyunException;

public class TokenExpiredException extends SickgyunException {
public TokenExpiredException() {
super(HttpStatus.FORBIDDEN, "토큰이 만료되었습니다.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.sickgyun.server.auth.exception;

import org.springframework.http.HttpStatus;

import com.sickgyun.server.common.exception.SickgyunException;

public class TokenInvalidException extends SickgyunException {
public TokenInvalidException() {
super(HttpStatus.UNAUTHORIZED, "토큰이 유효하지 않습니다.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.sickgyun.server.auth.exception;

import org.springframework.http.HttpStatus;

import com.sickgyun.server.common.exception.SickgyunException;

public class TokenMissingException extends SickgyunException {
public TokenMissingException() {
super(HttpStatus.UNAUTHORIZED, "토큰이 없습니다.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.sickgyun.server.auth.exception;

import org.springframework.http.HttpStatus;

import com.sickgyun.server.common.exception.SickgyunException;

public class UserIsNotAdminException extends SickgyunException {
public UserIsNotAdminException() {
super(HttpStatus.UNAUTHORIZED, "사용자가 어드민이 아닙니다.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.sickgyun.server.auth.exception;

import static org.springframework.http.HttpStatus.*;

import com.sickgyun.server.common.exception.SickgyunException;

public class UserNotLoginException extends SickgyunException {
public UserNotLoginException() {
super(FORBIDDEN, "유저가 로그인하지 않았습니다.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.sickgyun.server.auth.infra.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import com.sickgyun.server.auth.infra.feign.dto.GoogleUserInfoResponse;

@FeignClient(name = "GoogleOauthFeign", url = "https://www.googleapis.com/oauth2/v1/userinfo")
public interface GoogleOauthFeign {
@GetMapping
GoogleUserInfoResponse getInfo(@RequestParam(name = "access_token") String accessToken);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.sickgyun.server.auth.infra.feign.dto;

import com.sickgyun.server.user.domain.User;

public record GoogleUserInfoResponse(
String email,
String name
) {
public User toUser() {
return new User(name, email);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.sickgyun.server.auth.interceptor;

import static org.springframework.http.HttpHeaders.*;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import com.sickgyun.server.auth.annotation.AdminOnly;
import com.sickgyun.server.auth.annotation.LoginRequired;
import com.sickgyun.server.auth.exception.UserIsNotAdminException;
import com.sickgyun.server.auth.repository.AuthRepository;
import com.sickgyun.server.auth.util.BearerTokenExtractor;
import com.sickgyun.server.auth.util.JwtParser;
import com.sickgyun.server.user.domain.User;
import com.sickgyun.server.user.domain.repository.UserRepository;
import com.sickgyun.server.user.domain.role.Role;
import com.sickgyun.server.user.exception.UserNotFoundException;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;

@Configuration
@RequiredArgsConstructor
public class AuthInterceptor implements HandlerInterceptor {
private final JwtParser jwtParser;
private final AuthRepository authRepository;
private final UserRepository userRepository;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (handler instanceof HandlerMethod hm) {
if (hm.hasMethodAnnotation(LoginRequired.class)) {
String jwt = BearerTokenExtractor.extract(request.getHeader(AUTHORIZATION));
Long userId = jwtParser.getIdFromJwt(jwt);

User user = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException(userId));

authRepository.updateCurrentUser(user);
}
if (hm.hasMethodAnnotation(AdminOnly.class)) {
User currentUser = authRepository.getCurrentUser();
shouldUserAdmin(currentUser);
}
}
return true;
}

private static void shouldUserAdmin(User currentUser) {
if (currentUser.getRole() != Role.ADMIN) {
throw new UserIsNotAdminException();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.sickgyun.server.auth.presentation;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.sickgyun.server.auth.presentation.dto.AccessTokenRequest;
import com.sickgyun.server.auth.presentation.dto.RefreshTokenRequest;
import com.sickgyun.server.auth.presentation.dto.TokenResponse;
import com.sickgyun.server.auth.service.CommandAuthService;

import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {
private final CommandAuthService authService;

@PostMapping("/login")
public TokenResponse login(@RequestBody AccessTokenRequest accessToken) {
return TokenResponse.from(
authService.login(accessToken.accessToken())
);
}

@PostMapping("/refresh")
public TokenResponse refreshAccessToken(@RequestBody RefreshTokenRequest refreshToken) {
return TokenResponse.from(
authService.refresh(refreshToken.refreshToken())
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.sickgyun.server.auth.presentation.dto;

public record AccessTokenRequest(
String accessToken
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.sickgyun.server.auth.presentation.dto;

public record RefreshTokenRequest(
String refreshToken
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.sickgyun.server.auth.presentation.dto;

import com.sickgyun.server.auth.domain.Token;

public record TokenResponse(
String accessToken,
String refreshToken
) {
public static TokenResponse from(Token token) {
return new TokenResponse(
token.accessToken(),
token.refreshToken()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.sickgyun.server.auth.repository;

import org.springframework.stereotype.Repository;

import com.sickgyun.server.auth.exception.UserNotLoginException;
import com.sickgyun.server.user.domain.User;

@Repository
public class AuthRepository {
private User currentUser;

public User getCurrentUser() {
if (currentUser == null) {
throw new UserNotLoginException();
}
return currentUser;
}

public void updateCurrentUser(User currentUser) {
this.currentUser = currentUser;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.sickgyun.server.auth.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.sickgyun.server.auth.domain.Token;
import com.sickgyun.server.auth.service.implementation.AuthReader;
import com.sickgyun.server.auth.service.implementation.AuthValidator;
import com.sickgyun.server.auth.service.implementation.TokenProvider;
import com.sickgyun.server.auth.util.BearerTokenExtractor;
import com.sickgyun.server.user.domain.User;
import com.sickgyun.server.user.service.implementation.UserCreator;
import com.sickgyun.server.user.service.implementation.UserReader;
import com.sickgyun.server.user.service.implementation.UserUpdater;
import com.sickgyun.server.user.service.implementation.UserValidator;

import lombok.RequiredArgsConstructor;

@Service
@Transactional
@RequiredArgsConstructor
public class CommandAuthService {
private final TokenProvider tokenProvider;
private final AuthReader authReader;
private final AuthValidator authValidator;
private final UserValidator userValidator;
private final UserUpdater userUpdater;
private final UserCreator userCreator;
private final UserReader userReader;

public Token login(String accessToken) {
User user = authReader.getGoogleUser(accessToken);
authValidator.shouldBeBssmEmail(user);

User updatedUser;
if (userValidator.checkUserExist(user)) {
updatedUser = userUpdater.updateUser(user);
} else {
updatedUser = userCreator.create(user);
}

return tokenProvider.createNewTokens(updatedUser);
}

public Token refresh(String bearer) {
String refreshToken = BearerTokenExtractor.extract(bearer);
authValidator.shouldRefreshTokenValid(refreshToken);

Long userId = authReader.getIdFromJwt(refreshToken);
String accessToken = tokenProvider.createAccessToken(userReader.readUser(userId));

Comment on lines +46 to +51
Copy link
Contributor

Choose a reason for hiding this comment

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

깔쌈하네요

return new Token(accessToken, refreshToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.sickgyun.server.auth.service.implementation;

import org.springframework.stereotype.Service;

import com.sickgyun.server.auth.infra.feign.GoogleOauthFeign;
import com.sickgyun.server.auth.util.JwtParser;
import com.sickgyun.server.user.domain.User;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class AuthReader {
private final GoogleOauthFeign googleFeign;
private final JwtParser jwtParser;

public User getGoogleUser(String accessToken) {
return googleFeign.getInfo(accessToken).toUser();
}

public Long getIdFromJwt(String token) {
return jwtParser.getIdFromJwt(token);
}
}
Loading