Skip to content

Commit

Permalink
Feat/#87 duplicate login (#88)
Browse files Browse the repository at this point in the history
* chore: redis 의존성 추가

* feat: 중복 로그인 예외 처리
  • Loading branch information
birdieHyun authored Jan 18, 2024
1 parent 9b78ff1 commit 26cba54
Show file tree
Hide file tree
Showing 10 changed files with 119 additions and 4 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ dependencies {

// jwt 추가
implementation 'io.jsonwebtoken:jjwt:0.9.1'

// redis 추가
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}

ext {
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/yonseigolf/server/config/LoginInterceptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import yonseigolf.server.user.dto.response.LoggedInUser;
import yonseigolf.server.user.exception.DuplicatedLoginException;
import yonseigolf.server.user.service.JwtService;
import yonseigolf.server.user.service.PreventDuplicateLoginService;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

Expand All @@ -16,6 +19,7 @@
public class LoginInterceptor implements HandlerInterceptor {

private final JwtService jwtUtil;
private final PreventDuplicateLoginService preventDuplicateLoginService;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
Expand All @@ -28,8 +32,26 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons
}
String token = request.getHeader("Authorization").split(" ")[1];
LoggedInUser loggedInUser = jwtUtil.extractedUserFromToken(token, LoggedInUser.class);

boolean duplicated = preventDuplicateLoginService.checkDuplicatedLogin(loggedInUser.getId(), token);

if (!duplicated) {

invalidateRefreshToken(response);
throw new DuplicatedLoginException("중복 로그인이 발생했습니다.");
}

request.setAttribute("userId", loggedInUser.getId());

return true;
}

private void invalidateRefreshToken(HttpServletResponse response) {
Cookie cookie = new Cookie("refreshToken", null); // 쿠키 이름을 Refresh Token 쿠키 이름과 동일하게 설정
cookie.setHttpOnly(true);
cookie.setSecure(true); // 프로덕션 환경에서는 true로 설정
cookie.setPath("/"); // Refresh Token 쿠키와 동일한 경로 설정
cookie.setMaxAge(0); // 쿠키의 만료 시간을 0으로 설정하여 즉시 만료
response.addCookie(cookie);
}
}
15 changes: 15 additions & 0 deletions src/main/java/yonseigolf/server/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package yonseigolf.server.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;

@Configuration
public class RedisConfig {

@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) {
return new StringRedisTemplate(connectionFactory);
}
}
1 change: 1 addition & 0 deletions src/main/java/yonseigolf/server/config/WebConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/boards/**")
.addPathPatterns("/users/logout")
.addPathPatterns("/users/loggedIn")
.addPathPatterns("/replies/**")
.excludePathPatterns("/oauth/kakao");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import yonseigolf.server.user.exception.RefreshTokenExpiredException;
import yonseigolf.server.user.service.JwtService;
import yonseigolf.server.user.service.OauthLoginService;
import yonseigolf.server.user.service.PreventDuplicateLoginService;
import yonseigolf.server.user.service.UserService;
import yonseigolf.server.util.CustomResponse;

Expand All @@ -34,14 +35,16 @@ public class UserController {
private final OauthLoginService oauthLoginService;
private final KakaoOauthInfo kakaoOauthInfo;
private final JwtService jwtUtil;
private final PreventDuplicateLoginService preventDuplicateLoginService;

@Autowired
public UserController(UserService userService, OauthLoginService oauthLoginService, KakaoOauthInfo kakaoOauthInfo, JwtService jwtUtil) {
public UserController(UserService userService, OauthLoginService oauthLoginService, KakaoOauthInfo kakaoOauthInfo, JwtService jwtUtil, PreventDuplicateLoginService preventDuplicateLoginService) {

this.userService = userService;
this.oauthLoginService = oauthLoginService;
this.kakaoOauthInfo = kakaoOauthInfo;
this.jwtUtil = jwtUtil;
this.preventDuplicateLoginService = preventDuplicateLoginService;
}

@PostMapping("/oauth/kakao")
Expand Down Expand Up @@ -76,6 +79,9 @@ public ResponseEntity<CustomResponse<JwtTokenResponse>> signIn(@RequestAttribute
// signIn 할 경우 로그인 진행
makeRefreshToken(response, loggedInUser);

// redis에 중복 로그인 방지를 위한 access token 저장
preventDuplicateLoginService.registerLogin(loggedInUser.getId(), tokenReponse);

return ResponseEntity
.ok()
.body(CustomResponse.successResponse("로그인 성공",
Expand All @@ -102,6 +108,14 @@ private void createRefreshToken(HttpServletResponse response, String refreshToke
response.addCookie(cookie); // 응답에 쿠키 추가
}

@PostMapping("/users/loggedIn")
public ResponseEntity<CustomResponse<Void>> loggedIn() {

return ResponseEntity
.ok()
.body(CustomResponse.successResponse("로그인 상태입니다."));
}

@PostMapping("/users/signIn/refresh")
public ResponseEntity<CustomResponse<JwtTokenResponse>> refreshAccessToken(HttpServletRequest request) {

Expand All @@ -115,6 +129,7 @@ public ResponseEntity<CustomResponse<JwtTokenResponse>> refreshAccessToken(HttpS

// access token 재발급
String accessToken = userService.generateAccessToken(jwtTokenUser.getId(), jwtUtil, new Date(new Date().getTime() + 3600000));
preventDuplicateLoginService.registerLogin(jwtTokenUser.getId(), accessToken);

return ResponseEntity
.ok()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public class UserExceptionController {
/*
40101 access token 이 없거나 만료됨, refresh token을 통한 access 토큰 재발급
40102 refresh token이 없거나 만료됨, 로그인 페이지로 이동
409 중복 로그인
*/

@ExceptionHandler(IllegalArgumentException.class)
Expand Down Expand Up @@ -80,4 +81,15 @@ public ResponseEntity<CustomErrorResponse> refreshTokenExpired(RefreshTokenExpir
));
}

@ExceptionHandler(DuplicatedLoginException.class)
public ResponseEntity<CustomErrorResponse> duplicatedLogin(DuplicatedLoginException ex) {

return ResponseEntity
.status(HttpStatus.CONFLICT)
.body(new CustomErrorResponse(
"fail",
409,
ex.getMessage()
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package yonseigolf.server.user.exception;

public class DuplicatedLoginException extends RuntimeException {

public DuplicatedLoginException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package yonseigolf.server.user.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class PreventDuplicateLoginService {

private final RedisTemplate redisTemplate;

@Autowired
public PreventDuplicateLoginService(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}

public void registerLogin(long userId, String token) {

redisTemplate.opsForValue().getAndDelete(userId);
redisTemplate.opsForValue().set(userId, token, 30, TimeUnit.MINUTES);
}

// 같으면 true, 다르면 false
public boolean checkDuplicatedLogin(long userId, String token) {

return redisTemplate.opsForValue().get(userId).equals(token);
}
}
6 changes: 6 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ spring:
username: ${DATABASE_USERNAME}
password: ${DATABASE_PASSWORD}
driver-class-name: com.mysql.cj.jdbc.Driver

redis:
host: ${REDIS_HOST}
port: ${REDIS_PORT}
password: ${REDIS_PASSWORD}

jpa:
properties:
hibernate:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import org.springframework.restdocs.payload.JsonFieldType;
import yonseigolf.server.docs.utils.RestDocsSupport;
import yonseigolf.server.user.dto.request.KakaoCode;
import yonseigolf.server.user.dto.request.SignUpUserRequest;
import yonseigolf.server.user.dto.request.UserClassRequest;
import yonseigolf.server.user.dto.response.*;
import yonseigolf.server.user.dto.token.KakaoOauthInfo;
Expand All @@ -22,13 +21,14 @@
import yonseigolf.server.user.entity.UserRole;
import yonseigolf.server.user.service.JwtService;
import yonseigolf.server.user.service.OauthLoginService;
import yonseigolf.server.user.service.PreventDuplicateLoginService;
import yonseigolf.server.user.service.UserService;

import javax.servlet.http.Cookie;
import java.util.Date;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
Expand All @@ -54,6 +54,9 @@ class UserControllerTest extends RestDocsSupport {
private KakaoOauthInfo kakaoOauthInfo;
@Mock
private JwtService jwtUtil;
@Mock
private PreventDuplicateLoginService preventDuplicateLoginService;


@Test
@DisplayName("카카오톡 로그인을 할 수 있다.")
Expand Down Expand Up @@ -349,6 +352,6 @@ void healthCheck() throws Exception {

@Override
protected Object initController() {
return new UserController(userService, oauthLoginService, kakaoOauthInfo, jwtUtil);
return new UserController(userService, oauthLoginService, kakaoOauthInfo, jwtUtil, preventDuplicateLoginService);
}
}

0 comments on commit 26cba54

Please sign in to comment.