Skip to content

Commit

Permalink
feat: jwtAuthenticationFilter 구현 및 관련 Service 구현 (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
choidongkuen committed Jan 4, 2024
1 parent eb2f318 commit 4e27f1a
Show file tree
Hide file tree
Showing 4 changed files with 259 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package net.teumteum.core.security.filter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.teumteum.core.property.JwtProperty;
import net.teumteum.core.security.UserAuthentication;
import net.teumteum.core.security.service.AuthService;
import net.teumteum.core.security.service.JwtService;
import net.teumteum.core.security.service.RedisService;
import net.teumteum.user.domain.User;
import net.teumteum.user.domain.UserRepository;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final RedisService redisService;
private final AuthService authService;

private final JwtProperty jwtProperty;
private final UserRepository userRepository;

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException
{
/* Cors Preflight Request */
if(request.getMethod().equals("OPTIONS")) {
return;
}
/**
* 전달 받은 Access token 부터 Authentication 인증 객체 Security Context에 저장
*/
try {
String token = this.resolveTokenFromRequest(request);
// access token 이 있고 유효하다면
if (StringUtils.hasText(token) && this.jwtService.validateToken(token)) {
User user = this.authService.findUserByToken(token).get();
UserAuthentication authentication = new UserAuthentication(user);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
// access token 이 만료된 경우 or access token 정보로 식별 안되는 경우
// 예외가 발생하기만 해도 ExceptionTranslationFilter 호출
} catch (InsufficientAuthenticationException e) {
log.info("JwtAuthentication UnauthorizedUserException!");
}
filterChain.doFilter(request, response);
}

private String resolveTokenFromRequest(HttpServletRequest request) {
String token = request.getHeader(jwtProperty.getAccess().getHeader());
if (!ObjectUtils.isEmpty(token) && token.toLowerCase().startsWith(jwtProperty.getBearer().toLowerCase())) {
return token.substring(jwtProperty.getBearer().length()).trim();
}
return null;
}
}
111 changes: 111 additions & 0 deletions src/main/java/net/teumteum/core/security/service/JwtService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package net.teumteum.core.security.service;

import io.jsonwebtoken.*;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.teumteum.core.property.JwtProperty;
import net.teumteum.core.security.dto.TokenResponse;
import net.teumteum.user.domain.User;
import org.springframework.security.oauth2.jwt.JwtException;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.time.LocalDateTime;
import java.util.Date;
import java.util.Optional;
import java.util.UUID;

/* JWT 관련 모든 작업을 위한 Service */
@Slf4j
@Service
@RequiredArgsConstructor
public class JwtService {
private final JwtProperty jwtProperty;
private final RedisService redisService;

// HttpServletRequest 부터 Access Token 추출
public Optional<String> extractAccessToken(HttpServletRequest request) {
return Optional.ofNullable(request.getHeader(this.jwtProperty.getAccess().getHeader()))
.filter(StringUtils::hasText)
.filter(accessToken -> accessToken.startsWith(jwtProperty.getBearer()))
.map(accessToken -> accessToken.replace(jwtProperty.getBearer(), ""));
}

// HttpServletRequest 부터 Refresh Token 추출
public String extractRefreshToken(HttpServletRequest request) {
return request.getHeader(this.jwtProperty.getRefresh().getHeader());
}

// access token 생성
public String createAccessToken(String payload) {
return this.createToken(payload, this.jwtProperty.getAccess().getExpiration());
}


// refresh token 생성
public String createRefreshToken() {
return this.createToken(UUID.randomUUID().toString(), this.jwtProperty.getRefresh().getExpiration());

}


// access token 으로부터 회원 아이디 추출
public String getUserIdFromToken(String token) {
try {
return Jwts.parser()
.setSigningKey(this.jwtProperty.getSecret())
.parseClaimsJws(token)
.getBody()
.getSubject();
} catch (Exception exception) {
throw new JwtException("Access Token is not valid");
}
}

// kakao oauth 로그인 & 일반 로그인 시 jwt 응답 생성 + redis refresh 저장
public TokenResponse createServiceToken(User users) {
String accessToken = this.createAccessToken(String.valueOf(users.getId()));
String refreshToken = this.createRefreshToken();

/* 서비스 토큰 생성 */
TokenResponse userServiceTokenResponseDto = TokenResponse.builder()
.accessToken(this.jwtProperty.getBearer() + " " + accessToken)
.refreshToken(refreshToken)
.build();

/* redis refresh token 저장 */
this.redisService.setDataExpire(String.valueOf(users.getId()),
userServiceTokenResponseDto.getRefreshToken(), this.jwtProperty.getRefresh().getExpiration());

return userServiceTokenResponseDto;
}

// token 유효성 검증
public boolean validateToken(String token) {
try {
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(this.jwtProperty.getSecret()).parseClaimsJws(token);
return !claimsJws.getBody().getExpiration().before(new Date());
} catch (ExpiredJwtException exception) {
log.warn("만료된 jwt 입니다.");
} catch (UnsupportedJwtException exception) {
log.warn("지원되지 않는 jwt 입니다.");
} catch (IllegalArgumentException exception) {
log.warn("jwt 에 오류가 존재합니다.");
}
return false;
}

// 실제 token 생성 로직
private String createToken(String payload, Long tokenExpiration) {
Claims claims = Jwts.claims().setSubject(payload);
Date tokenExpiresIn = new Date(new Date().getTime() + tokenExpiration);

return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date())
.setExpiration(tokenExpiresIn)
.signWith(SignatureAlgorithm.HS512, this.jwtProperty.getSecret())
.compact();
}
}
43 changes: 43 additions & 0 deletions src/main/java/net/teumteum/core/security/service/RedisService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package net.teumteum.core.security.service;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import java.time.Duration;

/* Redis 관련 작업을 위한 서비스 */
@Service
@RequiredArgsConstructor
public class RedisService {
private final StringRedisTemplate stringRedisTemplate;

/* key 에 해당하는 데이터 얻어오는 메소드 */
public String getData(String key) {
ValueOperations<String, String> valueOperations = getStringStringValueOperations();
return valueOperations.get(key);
}

/* key - value 데이터 설정하는 메소드 */
public void setData(String key, String value) {
ValueOperations<String, String> valueOperations = getStringStringValueOperations();
valueOperations.set(key, value);
}

/* key 에 해당하는 데이터 삭제하는 메소드 */
public void deleteData(String key) {
this.stringRedisTemplate.delete(key);
}

/* key 에 해당하는 데이터 만료기간 설정 메소드 */
public void setDataExpire(String key, String value, Long duration) {
ValueOperations<String, String> valueOperations = getStringStringValueOperations();
Duration expireDuration = Duration.ofSeconds(duration);
valueOperations.set(key, value, expireDuration);
}

private ValueOperations<String, String> getStringStringValueOperations() {
return this.stringRedisTemplate.opsForValue();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package net.teumteum.core.security.service;

import net.teumteum.core.security.UserAuthentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

/* Security 관련 작업을 위한 서비스 */
@Component
public class SecurityService {
public void clearSecurityContext() {
SecurityContextHolder.clearContext();
}

/* 해당 요청에서 로그인한 회원 id 반환 */
public Long getCurrentUserId() {
UserAuthentication userAuthentication = getUserAuthentication();
return userAuthentication.getId();
}

/* 해당 요청에서 로그인한 회원 OAuth id 반환 */
public String getCurrentUserOAuthId() {
UserAuthentication userAuthentication = getUserAuthentication();
return userAuthentication.getOauthId();
}

public void setUserId(Long userId) {
UserAuthentication userAuthentication = getUserAuthentication();
userAuthentication.setUserId(userId);
}

private static UserAuthentication getUserAuthentication() {
return (UserAuthentication) SecurityContextHolder.getContext().getAuthentication();
}
}

0 comments on commit 4e27f1a

Please sign in to comment.