Skip to content

Commit

Permalink
feat: 중복로그인 방지 기능 추가 (#82)
Browse files Browse the repository at this point in the history
- Redis Template 추가
- JwtConfig에 만료시간 추가
- OAuth2AuthenticationSuccess 클래스에서 로그인 성공시 발급되는 JWT Redis에 저장
- JWT Redis에 저장시 만료시간을 JWT의 만료시간과 동일하게 설정
- JwtAuthorizationFilter 클래스에서 요청시 보낸 JWT와 Redis의 JWT 비교해서 중복로그인 확인하고 중복로그인 시 예외처리
  • Loading branch information
ah9mon authored Aug 9, 2023
1 parent 48d3ede commit 767ebdb
Show file tree
Hide file tree
Showing 6 changed files with 49 additions and 10 deletions.
3 changes: 3 additions & 0 deletions src/main/java/com/anywayclear/config/JwtConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,7 @@ public class JwtConfig {
private String header;
private String success;
private String fail;
private String second;
private String minute;
private String hour;
}
2 changes: 1 addition & 1 deletion src/main/java/com/anywayclear/config/RedisConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public RedisTemplate<String, Alarm> redisAlarmTemplate() {
}

@Bean
public RedisTemplate<String, String> redisTokenTemplate() {
public RedisTemplate<String, String> redisAuthenticatedUserTemplate() {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
// RedisTemplate 사용 시 Spring <-> Redis간 데이터 직렬화, 역직렬화에 사용하는 방식이 Jdk 직렬화 방식이기 때문
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/com/anywayclear/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
Expand All @@ -35,6 +36,7 @@ public class SecurityConfig {
private final JwtConfig jwtConfig;
private final MemberRepository memberRepository;
private final AuthenticationConfiguration authenticationConfiguration;
private final RedisTemplate<String, String> redisAuthenticatedUserTemplate;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
Expand Down Expand Up @@ -93,7 +95,7 @@ public AuthenticationManager authenticationManagerBean() throws Exception {

@Bean
public JwtAuthorizationFilter jwtAuthorizationFilter() throws Exception {
return new JwtAuthorizationFilter(authenticationManagerBean(), memberRepository, jwtConfig);
return new JwtAuthorizationFilter(authenticationManagerBean(), memberRepository, jwtConfig, redisAuthenticatedUserTemplate);
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
Expand All @@ -32,11 +33,13 @@ public class JwtAuthorizationFilter extends BasicAuthenticationFilter {

private final JwtConfig jwtConfig;
private final MemberRepository memberRepository;
private final RedisTemplate<String, String> redisAuthenticatedUserTemplate;

public JwtAuthorizationFilter(AuthenticationManager authenticationManager, MemberRepository memberRepository, JwtConfig jwtConfig) {
public JwtAuthorizationFilter(AuthenticationManager authenticationManager, MemberRepository memberRepository, JwtConfig jwtConfig, RedisTemplate<String, String> redisAuthenticatedUserTemplate) {
super(authenticationManager);
this.jwtConfig = jwtConfig;
this.memberRepository = memberRepository;
this.redisAuthenticatedUserTemplate = redisAuthenticatedUserTemplate;
}

@Override
Expand All @@ -63,10 +66,12 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
String accessToken = request.getHeader(jwtConfig.getHeader()).replace(jwtConfig.getPrefix()+ " ","");
String userId = JWT.require(Algorithm.HMAC512(jwtConfig.getKey())).build().verify(accessToken).getClaim("userId").asString();
if (userId != null) {
Optional<Member> memberOptional = memberRepository.findByUserId(userId);
if (memberOptional.isPresent() && !memberOptional.get().isDeleted()) {
processValidJwt(memberOptional.get());
Member member = memberRepository.findByUserId(userId).orElseThrow(() -> new CustomException(ExceptionCode.INVALID_MEMBER));
if (!member.isDeleted()) {
if (checkDuplicatedLogin(userId, accessToken, response)) return; // 중복로그인 시 예외처리
processValidJwt(member);
chain.doFilter(request, response);

} else { // 탈퇴한 회원일 때
sendJsonResponse(response, ExceptionCode.INVALID_DELETED_MEMBER);
}
Expand All @@ -82,6 +87,15 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
}
}

private boolean checkDuplicatedLogin(String userId, String token, HttpServletResponse response) throws IOException {
String tokenInRedis = redisAuthenticatedUserTemplate.opsForValue().get(userId);
if (tokenInRedis != null && !tokenInRedis.equals(token)) { // redis에 저장된 토큰과 다른 값이면
sendJsonResponse(response, ExceptionCode.INVALID_DUPLICATED_AUTHENTICATION); // 중복로그인 토큰 만료 처리
return true;
}
return false;
}

private void sendJsonResponse(HttpServletResponse response, ExceptionCode exceptionCode) throws IOException {
response.setContentType("application/json"); // JSON 형식의 데이터라고 설정
response.setCharacterEncoding("UTF-8"); // 인코딩 설정
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,27 @@
import com.auth0.jwt.algorithms.Algorithm;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;

import javax.persistence.criteria.CriteriaBuilder;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
import java.util.concurrent.TimeUnit;

@Log4j2
@Component
@RequiredArgsConstructor
public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

private final JwtConfig jwtConfig;
private final RedisTemplate<String, String> redisAuthenticatedUserTemplate;

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
Expand All @@ -36,16 +40,23 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo
OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
String userId = (String) oAuth2User.getAttributes().get("userId");
String role = (String) oAuth2User.getAttributes().get("role");
int expirationTime = setExpirationTime();
System.out.println("expirationTime = " + expirationTime);


String accessToken = createToken(userId, role);
String accessToken = createToken(userId, role, expirationTime);
setTokenInRedis(userId, accessToken, expirationTime);
System.out.println("accessToken = " + accessToken);

String redirectUrl = createRedirectUrl(accessToken, userId);

getRedirectStrategy().sendRedirect(request,response,redirectUrl);
}

private void setTokenInRedis(String userId, String token, int expirationTime) {
redisAuthenticatedUserTemplate.opsForValue().set(userId, token);
redisAuthenticatedUserTemplate.expire(userId, expirationTime, TimeUnit.MILLISECONDS);
}

private String createRedirectUrl(String accessToken, String userId) {
String redirectUrl = jwtConfig.getSuccess();
redirectUrl = UriComponentsBuilder.fromUriString(redirectUrl)
Expand All @@ -55,10 +66,18 @@ private String createRedirectUrl(String accessToken, String userId) {
return redirectUrl;
}

private String createToken(String userId, String role) {
private int setExpirationTime() {
int second = Integer.parseInt(jwtConfig.getSecond());
int minute = Integer.parseInt(jwtConfig.getMinute());
int hour = Integer.parseInt(jwtConfig.getHour());

return 1000 * second * minute * hour;
}

private String createToken(String userId, String role, int expirationTime) {
String accessToken = JWT.create()
.withSubject("mokumoku")
.withExpiresAt(new Date(System.currentTimeMillis() + (1000 * 60 * 60 * 24)))
.withExpiresAt(new Date(System.currentTimeMillis() + (expirationTime)))
.withClaim("userId", userId)
.withClaim("role", role)
.sign(Algorithm.HMAC512(jwtConfig.getKey()));
Expand Down
1 change: 1 addition & 0 deletions src/main/java/com/anywayclear/exception/ExceptionCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public enum ExceptionCode {
INVALID_TOKEN(UNAUTHORIZED, "잘못된 토큰입니다", 401),
INVALID_EXPIRED_TOKEN(UNAUTHORIZED, "만료된 토큰입니다", 401),
INVALID_DELETED_MEMBER(UNAUTHORIZED, "탈퇴한 회원입니다", 401),
INVALID_DUPLICATED_AUTHENTICATION(UNAUTHORIZED, "중복 로그인 입니다", 401),

// 403 Forbidden : 자원에 대한 권한 없음
INVALID_AUTH(FORBIDDEN, "권한이 없습니다", 403),
Expand Down

0 comments on commit 767ebdb

Please sign in to comment.