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

[BE] issue229: Refresh Token 적용하기 #236

Merged
merged 21 commits into from Aug 12, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions backend/src/docs/asciidoc/index.adoc
Expand Up @@ -12,6 +12,12 @@
=== Github 로그인
operation::auth/login[snippets='http-request,request-parameters,http-response,response-fields']

=== 리프래시 토큰
operation::auth/refresh[snippets='http-request,http-response']

=== 로그아웃
operation::auth/logout[snippets='http-request,http-response']

[[Member]]
== 회원

Expand Down
4 changes: 2 additions & 2 deletions backend/src/docs/asciidoc/index.html
Expand Up @@ -475,7 +475,7 @@ <h3 id="_github_로그인"><a class="link" href="#_github_로그인">Github 로
<h4 id="_github_로그인_http_request"><a class="link" href="#_github_로그인_http_request">HTTP request</a></h4>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight nowrap"><code class="language-http hljs" data-lang="http">POST /api/login/token?code=authorization-code HTTP/1.1
<pre class="highlightjs highlight nowrap"><code class="language-http hljs" data-lang="http">POST /api/auth/login?code=authorization-code HTTP/1.1
Content-Type: application/json
Accept: application/json
Host: localhost:8080</code></pre>
Expand Down Expand Up @@ -890,4 +890,4 @@ <h4 id="My-Role_response_fields"><a class="link" href="#My-Role_response_fields"
}
</script>
</body>
</html>
</html>
@@ -1,11 +1,15 @@
package com.woowacourse.moamoa.auth.config;

import static org.springframework.http.HttpMethod.DELETE;
import static org.springframework.http.HttpMethod.GET;
import static org.springframework.http.HttpMethod.POST;
import static org.springframework.http.HttpMethod.PUT;

import com.woowacourse.moamoa.auth.controller.matcher.AuthenticationRequestMatcher;
import com.woowacourse.moamoa.auth.controller.matcher.AuthenticationRequestMatcherBuilder;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;

@Configuration
@AllArgsConstructor
Expand All @@ -14,10 +18,10 @@ public class AuthRequestMatchConfig {
@Bean
public AuthenticationRequestMatcher authenticationRequestMatcher() {
return new AuthenticationRequestMatcherBuilder()
.addUpAuthenticationPath(HttpMethod.POST, "/api/studies", "/api/studies/\\d+/reviews", "/api/studies/\\d+/reviews/\\d+")
.addUpAuthenticationPath(HttpMethod.GET, "/api/my/studies", "/api/members/me", "/api/members/me/role")
.addUpAuthenticationPath(HttpMethod.PUT, "/api/studies/\\d+/reviews/\\d+")
.addUpAuthenticationPath(HttpMethod.DELETE, "/api/studies/\\d+/reviews/\\d+")
.addUpAuthenticationPath(POST, "/api/studies", "/api/studies/\\d+/reviews", "/api/studies/\\d+/reviews/\\d+")
.addUpAuthenticationPath(GET, "/api/my/studies", "/api/members/me", "/api/members/me/role")
.addUpAuthenticationPath(PUT, "/api/studies/\\d+/reviews/\\d+")
.addUpAuthenticationPath(DELETE, "/api/studies/\\d+/reviews/\\d+")
.build();
}
}
Expand Up @@ -7,8 +7,8 @@

public class AuthenticationExtractor {

private static String BEARER_TYPE = "Bearer";
private static String ACCESS_TOKEN_TYPE = AuthenticationExtractor.class.getSimpleName() + ".ACCESS_TOKEN_TYPE";
private static final String BEARER_TYPE = "Bearer";
private static final String ACCESS_TOKEN_TYPE = AuthenticationExtractor.class.getSimpleName() + ".ACCESS_TOKEN_TYPE";
Comment on lines +10 to +11
Copy link
Collaborator

Choose a reason for hiding this comment

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

굿!


public static String extract(HttpServletRequest request) {
Enumeration<String> headers = request.getHeaders(AUTHORIZATION);
Expand Down
@@ -1,9 +1,15 @@
package com.woowacourse.moamoa.auth.controller;

import com.woowacourse.moamoa.auth.config.AuthenticationPrincipal;
import com.woowacourse.moamoa.auth.service.AuthService;
import com.woowacourse.moamoa.auth.service.response.TokenResponse;
import com.woowacourse.moamoa.auth.service.response.AccessTokenResponse;
import com.woowacourse.moamoa.auth.service.response.TokensResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
Expand All @@ -12,10 +18,48 @@
@RequiredArgsConstructor
public class AuthController {

private static final String REFRESH_TOKEN = "refreshToken";
private static final int REFRESH_TOKEN_EXPIRATION = 7 * 24 * 60 * 60;

private final AuthService authService;

@PostMapping("/api/login/token")
public ResponseEntity<TokenResponse> login(@RequestParam final String code) {
return ResponseEntity.ok().body(authService.createToken(code));
@PostMapping("/api/auth/login")
public ResponseEntity<AccessTokenResponse> login(@RequestParam final String code) {
final TokensResponse tokenResponse = authService.createToken(code);

final AccessTokenResponse response = new AccessTokenResponse(tokenResponse.getAccessToken(), authService.getExpireTime());
final ResponseCookie cookie = putTokenInCookie(tokenResponse);

return ResponseEntity.ok().header("Set-Cookie", cookie.toString()).body(response);
}

@GetMapping("/api/auth/refresh")
public ResponseEntity<AccessTokenResponse> refreshToken(@AuthenticationPrincipal Long githubId, @CookieValue String refreshToken) {
return ResponseEntity.ok().body(authService.refreshToken(githubId, refreshToken));
}

@DeleteMapping("/api/auth/logout")
public ResponseEntity<Void> logout(@AuthenticationPrincipal Long githubId) {
authService.logout(githubId);

return ResponseEntity.noContent().header("Set-Cookie", removeCookie().toString()).build();
}

private ResponseCookie putTokenInCookie(final TokensResponse tokenResponse) {
return ResponseCookie.from(REFRESH_TOKEN, tokenResponse.getRefreshToken())
.maxAge(REFRESH_TOKEN_EXPIRATION)
.path("/")
.secure(true)
.httpOnly(true)
.build();
}

private ResponseCookie removeCookie() {
return ResponseCookie.from(REFRESH_TOKEN, null)
.maxAge(0)
.path("/")
.secure(true)
.httpOnly(true)
.build();
}
}
Expand Up @@ -28,8 +28,8 @@ public boolean supportsParameter(final MethodParameter parameter) {
public Object resolveArgument(final MethodParameter parameter, final ModelAndViewContainer mavContainer,
final NativeWebRequest webRequest, final WebDataBinderFactory binderFactory) {
final HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);

final String token = AuthenticationExtractor.extract(request);

if (token == null) {
throw new UnauthorizedException("인증 타입이 올바르지 않습니다.");
}
Expand Down
@@ -0,0 +1,36 @@
package com.woowacourse.moamoa.auth.domain;

import static javax.persistence.GenerationType.IDENTITY;
import static lombok.AccessLevel.PROTECTED;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor(access = PROTECTED)
@AllArgsConstructor
public class Token {

@Id
@GeneratedValue(strategy = IDENTITY)
private Long id;

@Column(nullable = false)
private Long githubId;

private String refreshToken;

public Token(final Long githubId, final String refreshToken) {
this(null, githubId, refreshToken);
}

public void updateRefreshToken(final String refreshToken) {
this.refreshToken = refreshToken;
}
}
@@ -0,0 +1,10 @@
package com.woowacourse.moamoa.auth.domain.repository;

import com.woowacourse.moamoa.auth.domain.Token;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface TokenRepository extends JpaRepository<Token, Long> {

Optional<Token> findByGithubId(Long githubId);
}
@@ -0,0 +1,9 @@
package com.woowacourse.moamoa.auth.exception;

import com.woowacourse.moamoa.common.exception.UnauthorizedException;

public class RefreshTokenExpirationException extends UnauthorizedException {
public RefreshTokenExpirationException() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

한 칸 띄워도 좋을 것 같아요 ~

super("만료된 리프래시 토큰입니다.");
}
}
@@ -0,0 +1,10 @@
package com.woowacourse.moamoa.auth.exception;

import com.woowacourse.moamoa.common.exception.UnauthorizedException;

public class TokenExpirationException extends UnauthorizedException {

public TokenExpirationException() {
super("만료된 토큰입니다.");
}
}
@@ -0,0 +1,10 @@
package com.woowacourse.moamoa.auth.exception;

import com.woowacourse.moamoa.common.exception.UnauthorizedException;

public class TokenNotFoundException extends UnauthorizedException {

public TokenNotFoundException() {
super("토큰이 존재하지 않습니다.");
}
}
@@ -1,5 +1,7 @@
package com.woowacourse.moamoa.auth.infrastructure;

import com.woowacourse.moamoa.auth.exception.RefreshTokenExpirationException;
import com.woowacourse.moamoa.auth.service.response.TokensResponse;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException;
Expand All @@ -15,6 +17,8 @@
@Component
public class JwtTokenProvider implements TokenProvider {

private static final long REFRESH_TOKEN_EXPIRATION = 7 * 24 * 60 * 60 * 1000; // 7일

private final SecretKey key;
private final long validityInMilliseconds;

Expand All @@ -27,16 +31,23 @@ public JwtTokenProvider(
}

@Override
public String createToken(final Long payload) {
public TokensResponse createToken(final Long payload) {
final Date now = new Date();
final Date validity = new Date(now.getTime() + validityInMilliseconds);

return Jwts.builder()
String accessToken = Jwts.builder()
.setSubject(payload.toString())
.setIssuedAt(now)
.setExpiration(validity)
.setExpiration(new Date(now.getTime() + validityInMilliseconds))
.signWith(key, SignatureAlgorithm.HS256)
.compact();

String refreshToken = Jwts.builder()
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + REFRESH_TOKEN_EXPIRATION))
.signWith(key, SignatureAlgorithm.HS256)
.compact();

return new TokensResponse(accessToken, refreshToken);
}

@Override
Expand All @@ -57,11 +68,47 @@ public boolean validateToken(final String token) {
.build()
.parseClaimsJws(token);

return !claims.getBody()
.getExpiration()
.before(new Date());
Date tokenExpirationDate = claims.getBody().getExpiration();
validateTokenExpiration(tokenExpirationDate);

return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}

@Override
public String recreationAccessToken(final Long githubId, final String refreshToken) {
Jws<Claims> claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(refreshToken);

Date tokenExpirationDate = claims.getBody().getExpiration();
validateTokenExpiration(tokenExpirationDate);

return createAccessToken(githubId);
}

private void validateTokenExpiration(Date tokenExpirationDate) {
if (tokenExpirationDate.before(new Date())) {
throw new RefreshTokenExpirationException();
Copy link
Collaborator

Choose a reason for hiding this comment

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

기존 코드도 401 에러를 던질 것 같은데, 예외 메시지를 다르게 주기 위함인가요 ??

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

넵..!! 로그아웃 시켜야 할 때 코드(4001) 도 함께 전달해달라는 front 측의 요구사항을 반영하기 위해서 였습니다!!

}
}

private String createAccessToken(final Long githubId) {
final Date now = new Date();

return Jwts.builder()
.setSubject(Long.toString(githubId))
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + validityInMilliseconds))
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}

@Override
public long getValidityInMilliseconds() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

@Override 선언이 있어야 할 것 같아요 ~

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

앗..!! 수정하도록 하겠습니다!

return validityInMilliseconds;
}
}
@@ -1,10 +1,16 @@
package com.woowacourse.moamoa.auth.infrastructure;

import com.woowacourse.moamoa.auth.service.response.TokensResponse;

public interface TokenProvider {

String createToken(final Long payload);
TokensResponse createToken(final Long payload);

String getPayload(final String token);

boolean validateToken(final String token);

String recreationAccessToken(final Long githubId, final String refreshToken);

long getValidityInMilliseconds();
}