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
11 changes: 6 additions & 5 deletions src/main/java/com/oronaminc/join/global/config/CacheType.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
@AllArgsConstructor
public enum CacheType {
ROOM_BY_ID("roomById", 300, 1000),
ROOM_BY_SECRET_CODE("roomBySecretCode", 300, 1000)
;

ROOM_BY_SECRET_CODE("roomBySecretCode", 300, 1000),
REFRESH_LATEST("refreshLatest", 60 * 60 * 24 * 14, 100_000), // 14일
REFRESH_BLACKLIST("refreshBlacklist", 60 * 60 * 24 * 14, 100_000);
public final String cacheName;
public final long expireAfterWrite;
public final long maximumSize;
public final int expireAfterWrite;
public final int maximumSize;

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,21 @@
import com.oronaminc.join.member.dto.GuestLoginRequest;
import com.oronaminc.join.member.dto.KakaoLoginRequest;
import com.oronaminc.join.member.token.AuthTokenResponse;
import com.oronaminc.join.member.token.JwtTokenProvider;
import com.oronaminc.join.member.token.JwtUtils;
import com.oronaminc.join.member.token.LoginResponse;
import com.oronaminc.join.member.token.RefreshTokenStore;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import jakarta.validation.Valid;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
Expand All @@ -31,6 +33,8 @@
public class AuthController {

private final AuthService authService;
private final JwtTokenProvider jwtTokenProvider;
private final RefreshTokenStore refreshTokenStore;

@Operation(
summary = "카카오 로그인"
Expand Down Expand Up @@ -84,7 +88,30 @@ public Map<String, AuthTokenResponse> guestLogin(
@PostMapping("/logout")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void logout(HttpServletRequest request, HttpServletResponse response) {
HttpSession session = request.getSession();

String refresh = null;
if(request.getCookies() != null){
for (Cookie cookie : request.getCookies()) {
if ("refreshToken".equals(cookie.getName())) refresh = cookie.getValue();
}
}

if (refresh != null) {
try {
var body = jwtTokenProvider.parseClaims(refresh);
refreshTokenStore.isBlacklisted(refresh);
refreshTokenStore.saveLatest(body.memberId(), "");
}catch (Exception ignored){ }
}

// 쿠키 제거
ResponseCookie expired = ResponseCookie.from("refreshToken", "")
.httpOnly(true).secure(true).sameSite("None")
.path("/").maxAge(0).build();

SecurityContextHolder.clearContext();

/*HttpSession session = request.getSession();
if (session != null) {
session.invalidate();
}
Expand All @@ -95,6 +122,6 @@ public void logout(HttpServletRequest request, HttpServletResponse response) {
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setMaxAge(0);
response.addCookie(cookie);
response.addCookie(cookie);*/
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import com.oronaminc.join.member.token.JwtMemberInfo;
import com.oronaminc.join.member.token.JwtTokenProvider;
import com.oronaminc.join.member.token.LoginResponse;
import com.oronaminc.join.member.token.RefreshTokenStore;
import com.oronaminc.join.member.token.TokenPair;
import com.oronaminc.join.member.util.MemberMapper;
import java.util.Map;
Expand All @@ -37,6 +38,7 @@ public class AuthService extends DefaultOAuth2UserService {
private final MemberRepository memberRepository;
private final MemberReader memberReader;
private final JwtTokenProvider jwtTokenProvider;
private final RefreshTokenStore refreshTokenStore;

private final RestTemplate restTemplate = new RestTemplate();

Expand Down Expand Up @@ -79,6 +81,7 @@ public LoginResponse loadGuest(GuestLoginRequest guestLoginRequest) {

TokenPair tokenPair = jwtTokenProvider.generateTokenPair(
new JwtMemberInfo(guest.getId(), guest.getNickname(), guest.getMemberType()));
refreshTokenStore.saveLatest(guest.getId(), tokenPair.refreshToken());

AuthTokenResponse authTokenResponse = new AuthTokenResponse(tokenPair.accessToken(),
tokenPair.accessTokenExpiresIn(), guest.getId(),
Expand All @@ -99,6 +102,8 @@ public LoginResponse kakaoLogin(String code) {
TokenPair tokenPair = jwtTokenProvider.generateTokenPair(
new JwtMemberInfo(member.getId(), member.getNickname(), member.getMemberType()));

refreshTokenStore.saveLatest(member.getId(), tokenPair.refreshToken());

AuthTokenResponse authTokenResponse = new AuthTokenResponse(tokenPair.accessToken(),
tokenPair.accessTokenExpiresIn(), member.getId(),
member.getNickname(), member.getMemberType());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.oronaminc.join.member.security;

import com.oronaminc.join.member.token.JwtTokenProvider;
import com.oronaminc.join.member.token.TokenBody;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.http.HttpHeaders;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtTokenProvider jwtTokenProvider;


@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {

String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (StringUtils.hasText(header) && header.startsWith("Bearer ")) {
String token = header.substring(7);
try {
TokenBody body = jwtTokenProvider.parseClaims(token);
var auth = new UsernamePasswordAuthenticationToken(
body.memberId(),
null,
List.of(new SimpleGrantedAuthority(body.role().name()))
);
SecurityContextHolder.getContext().setAuthentication(auth);
}catch (Exception e){

}
}

filterChain.doFilter(request,response);
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.oronaminc.join.member.security;

import com.oronaminc.join.member.token.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
Expand All @@ -23,7 +25,7 @@ public class SecurityConfig {
private final AuthService authService;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
public SecurityFilterChain filterChain(HttpSecurity http, JwtTokenProvider jwt) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
Expand Down Expand Up @@ -55,6 +57,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.formLogin(AbstractHttpConfigurer::disable)
.oauth2Login(oauth2 -> oauth2.userInfoEndpoint(userInfo -> userInfo
.userService(authService)))
.addFilterBefore(new JwtAuthenticationFilter(jwt),
UsernamePasswordAuthenticationFilter.class)
.logout(withDefaults())
.build();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.oronaminc.join.member.security;

import com.oronaminc.join.member.token.AuthTokenResponse;
import com.oronaminc.join.member.token.JwtMemberInfo;
import com.oronaminc.join.member.token.JwtTokenProvider;
import com.oronaminc.join.member.token.JwtUtils;
import com.oronaminc.join.member.token.RefreshTokenStore;
import com.oronaminc.join.member.token.TokenBody;
import com.oronaminc.join.member.token.TokenPair;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
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/auth/token")
@RequiredArgsConstructor
public class TokenController {

private final JwtTokenProvider jwtTokenProvider;
private final RefreshTokenStore refreshTokenStore;

@PostMapping("/refresh")
public AuthTokenResponse refresh(HttpServletRequest request, HttpServletResponse response) {
String refresh = extraRefreshCookie(request);
if (refresh == null) throw new IllegalArgumentException("refresh cookie is null");

if (refreshTokenStore.isBlacklisted(refresh)) throw new IllegalArgumentException("refresh cookie is blacklisted");

TokenBody body = jwtTokenProvider.parseClaims(refresh);
if (!refreshTokenStore.isLatest(body.memberId(), refresh)) throw new IllegalArgumentException("refresh token is invalid");

TokenPair tokenPair = jwtTokenProvider.generateTokenPair(new JwtMemberInfo(body.memberId(),
body.nickname(), body.role()));
refreshTokenStore.isBlacklisted(refresh);
refreshTokenStore.saveLatest(body.memberId(), tokenPair.refreshToken());
JwtUtils.addRefreshTokenCookie(response, tokenPair.refreshToken(), tokenPair.refreshTokenExpiresIn());

return new AuthTokenResponse(tokenPair.accessToken(), tokenPair.accessTokenExpiresIn(), body.memberId(), body.nickname(), body.role());
}

private String extraRefreshCookie(HttpServletRequest request) {
if (request.getCookies() == null) return null;
for (Cookie cookie : request.getCookies()) {
if ("refreshToken".equals(cookie.getName())) return cookie.getValue();
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public static void addRefreshTokenCookie(HttpServletResponse response, String re
.httpOnly(true)
.secure(true)
.path("/")
.sameSite("Strict")
.sameSite("None")
.maxAge(JwtUtils.toSeconds(expiresIn))
.build();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.oronaminc.join.member.token;

import java.util.Objects;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class RefreshTokenStore {
private final CacheManager cacheManager;

private Cache latest() {
return cacheManager.getCache("refreshLatest");
}
private Cache blacklist() {
return cacheManager.getCache("refreshBlacklist");
}

public void saveLatest(Long memberId, String refreshToken) {
latest().put(key(memberId), refreshToken);
}

public boolean isLatest(Long memberId, String refreshToken) {
String stored = latest().get(key(memberId), String.class);
return Objects.equals(stored, refreshToken);
}

public boolean isBlacklisted(String refreshToken) {
Boolean v = blacklist().get(refreshToken, Boolean.class);
return v != null && v;
}

private String key(Long memberId) {
return "refresh:" + memberId;
}
}
Loading