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

feat: refresh token을 도입한다 #818

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
Open
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
21 changes: 20 additions & 1 deletion backend/src/docs/asciidoc/auth.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
:toc: left
:toclevels: 3

== 충전소의 고장을 신고한다 (/oauth/{provider}/login-uri)
== 소셜 로그인 주소를 발급받는다 (/oauth/{provider}/login-uri)

=== Request

Expand All @@ -26,3 +26,22 @@ include::{snippets}/auth-controller-test/login/request-fields.adoc[]

include::{snippets}/auth-controller-test/login/http-response.adoc[]

== 새로운 access token을 발급받는다 (/renew)

=== Request

include::{snippets}/auth-controller-test/renew-access-token/http-request.adoc[]

=== Response

include::{snippets}/auth-controller-test/renew-access-token/http-response.adoc[]

== 로그아웃을 한다 (/logout)

=== Request

include::{snippets}/auth-controller-test/logout/http-request.adoc[]

=== Response

include::{snippets}/auth-controller-test/logout/http-response.adoc[]
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,32 @@

import com.carffeine.carffeine.auth.controller.dto.LoginUriResponse;
import com.carffeine.carffeine.auth.controller.dto.TokenResponse;
import com.carffeine.carffeine.auth.controller.support.AuthMember;
import com.carffeine.carffeine.auth.controller.support.RefreshTokenCookieGenerator;
import com.carffeine.carffeine.auth.domain.OAuthMember;
import com.carffeine.carffeine.auth.service.AuthService;
import com.carffeine.carffeine.auth.service.OAuthRequester;
import com.carffeine.carffeine.auth.service.dto.OAuthLoginRequest;
import com.carffeine.carffeine.auth.service.dto.Tokens;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

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

@RestController
@RequiredArgsConstructor
public class AuthController {

private final AuthService authService;
private final OAuthRequester oAuthRequester;
private final RefreshTokenCookieGenerator refreshTokenCookieGenerator;

@GetMapping("/oauth/{provider}/login-uri")
public ResponseEntity<LoginUriResponse> getRedirectUri(
Expand All @@ -37,7 +44,24 @@ public ResponseEntity<TokenResponse> login(
@PathVariable String provider
) {
OAuthMember oAuthMember = oAuthRequester.login(request, provider);
String token = authService.generateToken(oAuthMember);
return ResponseEntity.ok(new TokenResponse(token));
Tokens tokens = authService.generateTokens(oAuthMember);
return ResponseEntity.ok()
.header(SET_COOKIE, refreshTokenCookieGenerator.createCookie(tokens.refreshToken()).toString())
.body(new TokenResponse(tokens.accessToken()));
}

@PostMapping("/renew")
public ResponseEntity<TokenResponse> renewAccessToken(@CookieValue(value = "refresh-token") String refreshToken) {
String accessToken = authService.renewAccessToken(refreshToken);
return ResponseEntity.ok(new TokenResponse(accessToken));
}

@PostMapping("/logout")
public ResponseEntity<Void> logOut(@AuthMember Long loginMember) {
authService.logout(loginMember);
return ResponseEntity.ok()
.header(SET_COOKIE, refreshTokenCookieGenerator.createLogoutCookie().toString())
.build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public class LoginInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String token = AuthenticationExtractor.extract(request)
.orElseThrow(() -> new AuthException(AuthExceptionType.UNAUTHORIZED));
Long memberId = tokenProvider.extract(token);
Long memberId = Long.parseLong(tokenProvider.extract(token));
authenticationContext.setAuthentication(memberId);
return true;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.carffeine.carffeine.auth.controller.support;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseCookie;
import org.springframework.stereotype.Component;

import java.time.Duration;

@Component
public class RefreshTokenCookieGenerator {

private static final String REFRESH_TOKEN = "refreshToken";
private static final String VALID_COOKIE_PATH = "/";
private static final String LOGOUT_COOKIE_VALUE = "";
private static final int LOGOUT_COOKIE_AGE = 0;

@Value("${jwt.refresh-token-expiration-period}")
private long expireLength;

public ResponseCookie createCookie(String refreshToken) {
return ResponseCookie.from(REFRESH_TOKEN, refreshToken)
.maxAge(Duration.ofMillis(expireLength))
.path(VALID_COOKIE_PATH)
.sameSite("None")
Copy link
Collaborator

Choose a reason for hiding this comment

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

sameSite None 아주 좋네요 👍

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

어우 정말 오랜만이네요 반가워요

.secure(true)
.httpOnly(true)
.build();
}

public ResponseCookie createLogoutCookie() {
return ResponseCookie.from(REFRESH_TOKEN, LOGOUT_COOKIE_VALUE)
.maxAge(LOGOUT_COOKIE_AGE)
.sameSite("None")
.secure(true)
.httpOnly(true)
.build();
}

Copy link
Collaborator

Choose a reason for hiding this comment

The 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,31 @@
package com.carffeine.carffeine.auth.domain;

import com.carffeine.carffeine.common.domain.BaseEntity;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Getter
@Builder
@EqualsAndHashCode(of = "id", callSuper = false)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class RefreshToken extends BaseEntity {

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

private Long memberId;

private String tokenId;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.carffeine.carffeine.auth.domain;

import org.springframework.data.repository.Repository;

import java.util.Optional;

public interface RefreshTokenRepository extends Repository<RefreshToken, Long> {

RefreshToken save(RefreshToken refreshToken);

Optional<RefreshToken> findByTokenId(String refreshToken);

void deleteByMemberId(Long loginMember);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ public interface TokenProvider {

String create(Long id);

Long extract(String token);
String extract(String token);

String createRefreshToken(String id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ public class JwtProvider implements TokenProvider {

@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration-period}")
private int expirationPeriod;
@Value("${jwt.access-token-expiration-period}")
private long accessTokenExpirationPeriod;
@Value("${jwt.refresh-token-expiration-period}")
private long refreshTokenExpirationPeriod;
private Key key;

@PostConstruct
Expand All @@ -45,15 +47,15 @@ private void init() {
@Override
public String create(Long id) {
Claims claims = Jwts.claims();
claims.put("id", id);
return createToken(claims);
claims.put("id", String.valueOf(id));
return createToken(claims, accessTokenExpirationPeriod);
}

private String createToken(Claims claims) {
private String createToken(Claims claims, long expiredPeriod) {
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(issuedAt())
.setExpiration(expiredAt())
.setExpiration(expiredAt(expiredPeriod))
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
Expand All @@ -63,19 +65,19 @@ private Date issuedAt() {
return Date.from(now.atZone(ZoneId.systemDefault()).toInstant());
}

private Date expiredAt() {
private Date expiredAt(long expiredPeriod) {
LocalDateTime now = LocalDateTime.now();
return Date.from(now.plusHours(expirationPeriod).atZone(ZoneId.systemDefault()).toInstant());
return Date.from(now.plusHours(expiredPeriod).atZone(ZoneId.systemDefault()).toInstant());
}

@Override
public Long extract(String token) {
public String extract(String token) {
try {
return Jwts.parser()
.setSigningKey(secret.getBytes())
.parseClaimsJws(token)
.getBody()
.get("id", Long.class);
.get("id", String.class);
} catch (SecurityException e) {
throw new AuthException(SIGNITURE_NOT_FOUND);
} catch (MalformedJwtException e) {
Expand All @@ -88,4 +90,11 @@ public Long extract(String token) {
throw new AuthException(INVALID_TOKEN);
}
}

@Override
public String createRefreshToken(String tokenId) {
Claims claims = Jwts.claims();
claims.put("id", tokenId);
return createToken(claims, refreshTokenExpirationPeriod);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,67 @@

import com.carffeine.carffeine.auth.domain.OAuthMember;
import com.carffeine.carffeine.auth.domain.Provider;
import com.carffeine.carffeine.auth.domain.RefreshToken;
import com.carffeine.carffeine.auth.domain.RefreshTokenRepository;
import com.carffeine.carffeine.auth.domain.TokenProvider;
import com.carffeine.carffeine.auth.service.dto.Tokens;
import com.carffeine.carffeine.member.domain.Member;
import com.carffeine.carffeine.member.domain.MemberRepository;
import com.carffeine.carffeine.member.domain.MemberRole;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.UUID;

@Transactional
@RequiredArgsConstructor
@Service
public class AuthService {

private final TokenProvider tokenProvider;
private final MemberRepository memberRepository;
private final RefreshTokenRepository refreshTokenRepository;
private final OAuthRequester oAuthRequester;

public String loginUri(String redirectUri, String provider) {
return oAuthRequester.loginUri(Provider.from(provider), redirectUri);
}

@Transactional
public String generateToken(OAuthMember oAuthMember) {
public Tokens generateTokens(OAuthMember oAuthMember) {
Member member = getMember(oAuthMember);
String tokenId = UUID.randomUUID().toString();

String accessToken = tokenProvider.create(member.getId());
String refreshToken = tokenProvider.createRefreshToken(tokenId);

refreshTokenRepository.save(RefreshToken.builder()
.tokenId(tokenId)
.memberId(member.getId())
.build());

return new Tokens(accessToken, refreshToken);
}

private Member getMember(OAuthMember oAuthMember) {
Member newMember = Member.builder()
.email(oAuthMember.email())
.name(oAuthMember.nickname())
.imageUrl(oAuthMember.imageUrl())
.memberRole(MemberRole.USER)
.build();
Member member = memberRepository.findByEmail(oAuthMember.email())
return memberRepository.findByEmail(oAuthMember.email())
.orElseGet(() -> memberRepository.save(newMember));
return tokenProvider.create(member.getId());
}

public String renewAccessToken(String refreshToken) {
String tokenId = tokenProvider.extract(refreshToken);
return refreshTokenRepository.findByTokenId(tokenId)
.map(it -> tokenProvider.create(it.getMemberId()))
.orElseThrow();
}

public void logout(Long loginMember) {
refreshTokenRepository.deleteByMemberId(loginMember);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.carffeine.carffeine.auth.service.dto;

public record Tokens(
String accessToken,
String refreshToken
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.servlet.Filter;

@Profile("!test")
Copy link
Collaborator

Choose a reason for hiding this comment

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

이 부분이 잘 이해가 안가는데요 Profile을 이와 같이 설정한 이유가 무엇인가요??

@Configuration
public class WebConfig implements WebMvcConfigurer {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public ResponseEntity<StationsSummaryResponse> getStationsSummary(@RequestParam
return ResponseEntity.ok(new StationsSummaryResponse(stations));
}

@GetMapping("/stations/regions")
@GetMapping("/stations/markers/regions")
public ResponseEntity<List<RegionMarker>> getMarkerByRegions(@RequestParam List<String> regions) {
return ResponseEntity.ok(stationQueryService.findMarkersByRegions(regions));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import com.carffeine.carffeine.station.infrastructure.repository.charger.dto.ChargerStatusResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.time.DayOfWeek;
Expand All @@ -26,7 +25,7 @@ public class StationService {
private final PeriodicCongestionCustomRepository periodicCongestionCustomRepository;
private final AtomicBoolean isRunning = new AtomicBoolean(false);

@Scheduled(cron = "0 0/10 * * * *")
// @Scheduled(cron = "0 0/10 * * * *")
public void calculateCongestion() {
if (isRunning.compareAndSet(false, true)) {
LocalDateTime now = LocalDateTime.now();
Expand All @@ -36,7 +35,7 @@ public void calculateCongestion() {

String stationId = null;
String chargerId = null;
long limit = 1000;
long limit = 10000;
long size = limit;

while (limit == size) {
Expand Down
Loading
Loading