Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ out/

### Rest Docs
/src/main/resources/static/docs/openapi3.yaml

### application-local.yml
/src/main/resources/application-local.yml
20 changes: 19 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ dependencies {
// Excel Export
implementation 'org.apache.poi:poi-ooxml:5.2.3'
implementation 'org.apache.poi:poi:5.2.3'

// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-gson:0.11.5'
}

bootJar {
Expand Down Expand Up @@ -99,7 +104,20 @@ generateSwaggerUI {
}

openapi3 {
server = "http://localhost:8080"
servers = [
{
url = "https://api.dev.debate-timer.com"
description = "Dev Server"
},
{
url = "https://api.prod.debate-timer.com"
description = "Prod Server"
},
{
url = "http://localhost:8080"
description = "Local Server"
}
]
title = "토론 타이머 API"
description = "토론 타이머 API"
version = "0.0.1"
Expand Down
39 changes: 39 additions & 0 deletions src/main/java/com/debatetimer/client/OAuthClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.debatetimer.client;

import com.debatetimer.dto.member.MemberCreateRequest;
import com.debatetimer.dto.member.MemberInfo;
import com.debatetimer.dto.member.OAuthToken;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;

@Component
@EnableConfigurationProperties(OAuthProperties.class)
public class OAuthClient {

private final RestClient restClient;
private final OAuthProperties oauthProperties;

public OAuthClient(OAuthProperties oauthProperties) {
this.restClient = RestClient.create();
this.oauthProperties = oauthProperties;
}

public OAuthToken requestToken(MemberCreateRequest request) {
return restClient.post()
.uri("https://oauth2.googleapis.com/token")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(oauthProperties.createTokenRequestBody(request))
.retrieve()
.body(OAuthToken.class);
}

public MemberInfo requestMemberInfo(OAuthToken response) {
return restClient.get()
.uri("https://www.googleapis.com/oauth2/v3/userinfo")
.headers(headers -> headers.setBearerAuth(response.access_token()))
.retrieve()
.body(MemberInfo.class);
}
}
41 changes: 41 additions & 0 deletions src/main/java/com/debatetimer/client/OAuthProperties.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.debatetimer.client;

import com.debatetimer.dto.member.MemberCreateRequest;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

@Getter
@ConfigurationProperties(prefix = "oauth")
public class OAuthProperties {

private final String clientId;
private final String clientSecret;
private final String grantType;

public OAuthProperties(
String clientId,
String clientSecret,
String grantType) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.grantType = grantType;
}

public MultiValueMap<String, String> createTokenRequestBody(MemberCreateRequest request) {
String code = request.code();
String decodedVerificationCode = URLDecoder.decode(code, StandardCharsets.UTF_8);

MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("grant_type", grantType);
map.add("client_id", clientId);
map.add("redirect_uri", request.redirectUrl());
map.add("code", decodedVerificationCode);
map.add("client_secret", clientSecret);

return map;
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package com.debatetimer.config;

import com.debatetimer.controller.auth.AuthMember;
import com.debatetimer.controller.tool.jwt.AuthManager;
import com.debatetimer.exception.custom.DTClientErrorException;
import com.debatetimer.exception.custom.DTException;
import com.debatetimer.exception.errorcode.ClientErrorCode;
import com.debatetimer.repository.member.MemberRepository;
import com.debatetimer.service.auth.AuthService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
Expand All @@ -17,7 +18,8 @@
@RequiredArgsConstructor
public class AuthMemberArgumentResolver implements HandlerMethodArgumentResolver {

private final MemberRepository memberRepository;
private final AuthManager authManager;
private final AuthService authService;

@Override
public boolean supportsParameter(MethodParameter parameter) {
Expand All @@ -31,14 +33,11 @@ public Object resolveArgument(
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory
) {
try {
long memberId = Long.parseLong(webRequest.getParameter("memberId"));
return memberRepository.getById(memberId);
} catch (DTException | NumberFormatException exception) {
log.warn(exception.getMessage());
String accessToken = webRequest.getHeader(HttpHeaders.AUTHORIZATION);
if (accessToken == null) {
throw new DTClientErrorException(ClientErrorCode.UNAUTHORIZED_MEMBER);
}
String email = authManager.resolveAccessToken(accessToken);
return authService.getMember(email);
}
}


12 changes: 12 additions & 0 deletions src/main/java/com/debatetimer/config/AuthenticationConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.debatetimer.config;

import com.debatetimer.client.OAuthProperties;
import com.debatetimer.controller.tool.jwt.JwtTokenProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties({OAuthProperties.class, JwtTokenProperties.class})
public class AuthenticationConfig {

}
4 changes: 3 additions & 1 deletion src/main/java/com/debatetimer/config/CorsConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
Expand All @@ -28,7 +29,8 @@ public void addCorsMappings(CorsRegistry registry) {
HttpMethod.OPTIONS.name()
)
.allowCredentials(true)
.allowedHeaders("*");
.allowedHeaders("*")
.exposedHeaders(HttpHeaders.AUTHORIZATION);
}
}

8 changes: 5 additions & 3 deletions src/main/java/com/debatetimer/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.debatetimer.config;

import com.debatetimer.repository.member.MemberRepository;
import com.debatetimer.controller.tool.jwt.AuthManager;
import com.debatetimer.service.auth.AuthService;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
Expand All @@ -11,10 +12,11 @@
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {

private final MemberRepository memberRepository;
private final AuthManager authManager;
private final AuthService authService;

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new AuthMemberArgumentResolver(memberRepository));
argumentResolvers.add(new AuthMemberArgumentResolver(authManager, authService));
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
package com.debatetimer.controller.member;

import com.debatetimer.controller.auth.AuthMember;
import com.debatetimer.controller.tool.cookie.CookieManager;
import com.debatetimer.controller.tool.jwt.AuthManager;
import com.debatetimer.domain.member.Member;
import com.debatetimer.dto.member.JwtTokenResponse;
import com.debatetimer.dto.member.MemberCreateRequest;
import com.debatetimer.dto.member.MemberCreateResponse;
import com.debatetimer.dto.member.MemberInfo;
import com.debatetimer.dto.member.TableResponses;
import com.debatetimer.service.auth.AuthService;
import com.debatetimer.service.member.MemberService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
Expand All @@ -19,6 +28,9 @@
public class MemberController {

private final MemberService memberService;
private final AuthService authService;
private final CookieManager cookieManager;
private final AuthManager authManager;

@GetMapping("/api/table")
public TableResponses getTables(@AuthMember Member member) {
Expand All @@ -27,7 +39,35 @@ public TableResponses getTables(@AuthMember Member member) {

@PostMapping("/api/member")
@ResponseStatus(HttpStatus.CREATED)
public MemberCreateResponse createMember(@RequestBody MemberCreateRequest request) {
return memberService.createMember(request);
public MemberCreateResponse createMember(@RequestBody MemberCreateRequest request, HttpServletResponse response) {
MemberInfo memberInfo = authService.getMemberInfo(request);
MemberCreateResponse memberCreateResponse = memberService.createMember(memberInfo);
JwtTokenResponse jwtTokenResponse = authManager.issueToken(memberInfo);
ResponseCookie refreshTokenCookie = cookieManager.createRefreshTokenCookie(jwtTokenResponse.refreshToken());

response.addHeader(HttpHeaders.AUTHORIZATION, jwtTokenResponse.accessToken());
response.addHeader(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString());
return memberCreateResponse;
}

@PostMapping("/api/member/reissue")
public void reissueAccessToken(HttpServletRequest request, HttpServletResponse response) {
String refreshToken = cookieManager.extractRefreshToken(request.getCookies());
JwtTokenResponse jwtTokenResponse = authManager.reissueToken(refreshToken);
ResponseCookie refreshTokenCookie = cookieManager.createRefreshTokenCookie(jwtTokenResponse.refreshToken());

response.addHeader(HttpHeaders.AUTHORIZATION, jwtTokenResponse.accessToken());
response.addHeader(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString());
}

@PostMapping("/api/member/logout")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void logout(@AuthMember Member member, HttpServletRequest request, HttpServletResponse response) {
String refreshToken = cookieManager.extractRefreshToken(request.getCookies());
String email = authManager.resolveRefreshToken(refreshToken);
authService.logout(member, email);
ResponseCookie deletedRefreshTokenCookie = cookieManager.deleteRefreshTokenCookie();

response.addHeader(HttpHeaders.SET_COOKIE, deletedRefreshTokenCookie.toString());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.debatetimer.controller.tool.cookie;

import com.debatetimer.exception.custom.DTClientErrorException;
import com.debatetimer.exception.errorcode.ClientErrorCode;
import jakarta.servlet.http.Cookie;
import java.util.Arrays;
import org.springframework.stereotype.Component;

@Component
public class CookieExtractor {

public String extractCookie(String cookieName, Cookie[] cookies) {
return Arrays.stream(cookies)
.filter(cookie -> cookie.getName().equals(cookieName))
.findAny()
.map(Cookie::getValue)
.orElseThrow(() -> new DTClientErrorException(ClientErrorCode.EMPTY_COOKIE));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.debatetimer.controller.tool.cookie;

import com.debatetimer.controller.tool.jwt.JwtTokenProperties;
import jakarta.servlet.http.Cookie;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseCookie;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class CookieManager {

private static final String REFRESH_TOKEN_COOKIE_NAME = "refreshToken";

private final CookieProvider cookieProvider;
private final CookieExtractor cookieExtractor;
private final JwtTokenProperties jwtTokenProperties;

public ResponseCookie createRefreshTokenCookie(String token) {
return cookieProvider.createCookie(REFRESH_TOKEN_COOKIE_NAME, token,
jwtTokenProperties.getRefreshTokenExpirationMillis());
}

public String extractRefreshToken(Cookie[] cookies) {
return cookieExtractor.extractCookie(REFRESH_TOKEN_COOKIE_NAME, cookies);
}

public ResponseCookie deleteRefreshTokenCookie() {
return cookieProvider.deleteCookie(REFRESH_TOKEN_COOKIE_NAME);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.debatetimer.controller.tool.cookie;

import java.time.Duration;
import org.springframework.http.ResponseCookie;
import org.springframework.stereotype.Component;

@Component
public class CookieProvider {

private static final String PATH = "/";

public ResponseCookie createCookie(String cookieName, String token, long expirationMillis) {
return ResponseCookie.from(cookieName, token)
.maxAge(Duration.ofMillis(expirationMillis))
.path(PATH)
.sameSite("None")
.secure(true)
.build();
}

public ResponseCookie deleteCookie(String cookieName) {
return ResponseCookie.from(cookieName, "")
.maxAge(0)
.path(PATH)
.sameSite("None")
.secure(true)
.build();
}
}
Loading