Written on
·
25
·
Edited
0
안녕하십니까. 스프링 시큐리티 강의를 듣고 있는 대학생입니다. 현재 강의를 들으면서 팀 프로젝트에 소셜 로그인을 적용하던 중 궁금증이 생겨 글을 작성하게 되었습니다. 현재 사용자가 로그인 후 토큰을 발급받고 MySQL에 리프레시 토큰을 저장한 뒤에, 사용자가 API에 접근할 때마다 매번 사용자 정보를 조회하는 것이 좋은 방법인지 궁금합니다.
현재 매번 조회하는 이유는 토큰의 사용자 정보가 서버 데이터베이스에 올바르게 저장된 사용자인지 검증하기 위해서 이 방식을 채택했습니다
package backend_lingua.linguas.infrastructure.security.filter;
import backend_lingua.linguas.infrastructure.security.token.enumerated.TokenType;
import io.jsonwebtoken.ExpiredJwtException;
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 org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = getBearerToken(request);
// 토큰이 있는 경우에만 검증
if (StringUtils.hasText(token)) {
try {
if (jwtTokenProvider.validateAccessToken(token)) {
Authentication authentication = jwtTokenProvider.getAuthentication(token, TokenType.ACCESS_TOKEN);
SecurityContextHolder.getContext().setAuthentication(authentication);
log.debug("Security Context에 '{}' 인증 정보를 저장했습니다.", authentication.getName());
}
} catch (ExpiredJwtException e) {
log.error("토큰 인증 실패: {}", e.getMessage());
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication failed");
return;
}
}
filterChain.doFilter(request, response);
}
private String getBearerToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
public TokenInfo generateToken(Authentication authentication) {
String accessToken = createAccessToken(authentication);
String refreshToken = createRefreshToken(authentication);
Date accessTokenExpiryDate = createExpiryDate(accessTokenExpiration);
return TokenInfo.from(accessToken, refreshToken, accessTokenExpiryDate.getTime(), refreshTokenExpiration);
}
public String createAccessToken(Authentication authentication) {
return createToken(authentication, accessTokenExpiration, accessTokenSecret);
}
public String createRefreshToken(Authentication authentication) {
String token = createToken(authentication, refreshTokenExpiration, refreshTokenSecret);
Date expiryDate = createExpiryDate(refreshTokenExpiration);
// DB에 리프레시 토큰 저장
tokenService.createRefreshToken(token, expiryDate, authentication);
return token;
}
private String createToken(Authentication authentication, long expirationTime, String secretKey) {
String authorities = authentication.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
return Jwts.builder()
.setSubject(authentication.getName()) // email
.claim("auth", authorities)
.setIssuedAt(new Date())
.setExpiration(createExpiryDate(expirationTime))
.signWith(createKey(secretKey))
.compact();
}
public String getUsernameFromToken(String token, TokenType tokenType) {
String secretKey = (tokenType == TokenType.ACCESS_TOKEN) ? accessTokenSecret : refreshTokenSecret;
return Jwts.parserBuilder()
.setSigningKey(createKey(secretKey))
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public Authentication getAuthentication(String token, TokenType tokenType) {
String username = getUsernameFromToken(token, tokenType);
UserPrincipal userPrincipal = (UserPrincipal) userDetailsService.loadUserByUsername(username);
return new UsernamePasswordAuthenticationToken(
userPrincipal,
token,
userPrincipal.getAuthorities()
);
}
public boolean validateAccessToken(String token) {
return validateToken(token, accessTokenSecret);
}
public boolean validateRefreshToken(String token) {
return validateToken(token, refreshTokenSecret);
}
private boolean validateToken(String token, String secretKey) {
try {
Jwts.parserBuilder()
.setSigningKey(createKey(secretKey))
.build()
.parseClaimsJws(token);
return true;
} catch (ExpiredJwtException e) {
log.error("만료된 JWT 토큰입니다.");
throw e; // Filter에서 401 처리하도록
} catch (SecurityException | MalformedJwtException e) {
log.error("잘못된 JWT 서명입니다.");
} catch (UnsupportedJwtException e) {
log.error("지원되지 않는 JWT 토큰입니다.");
} catch (IllegalArgumentException e) {
log.error("JWT 토큰이 잘못되었습니다.");
}
return false;
}
private Date createExpiryDate(long expirationTime) {
return new Date(System.currentTimeMillis() + expirationTime);
}
private SecretKey createKey(String secret) {
byte[] keyBytes = Decoders.BASE64.decode(secret);
return Keys.hmacShaKeyFor(keyBytes);
}
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final MemberRepository memberRepository;
@Override
@Transactional(readOnly = true)
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
Member member = memberRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다: " + email));
return UserPrincipal.create(member);
}
}
public class TokenServiceImpl implements TokenService {
private final RefreshTokenRepository refreshTokenRepository;
private final MemberRepository memberRepository;
@Override
@Transactional
public RefreshToken createRefreshToken(String token, Date expiryDate, Authentication authentication) {
String email = authentication.getName();
Member member = memberRepository.findByEmail(email)
.orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다: " + email));
// 새 토큰 생성
RefreshToken refreshToken = RefreshToken.builder()
.token(token)
.member(member)
.expiryDate(convertToLocalDateTime(expiryDate))
.build();
return refreshTokenRepository.save(refreshToken);
}
@Override
@Transactional
public void deleteRefreshToken(String email) {
Member member = memberRepository.findByEmail(email)
.orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다: " + email));
deleteRefreshTokenByUser(member);
}
@Override
@Transactional
public void deleteRefreshTokenByUser(Member member) {
refreshTokenRepository.findByMemberId(member.getId())
.ifPresent(refreshTokenRepository::delete);
}
private LocalDateTime convertToLocalDateTime(Date date) {
return Instant.ofEpochMilli(date.getTime())
.atZone(ZoneId.systemDefault())
.toLocalDateTime();
}
}
Answer 1
0
안녕하세요, 인프런 AI 인턴입니다. 황중석님.
사용자가 API에 접근할 때마다 데이터베이스에서 사용자 정보를 조회하는 것은 검증 측면에서의 장점이 있을 수 있지만, 성능 측면에서는 비효율적일 수 있습니다. 매번 데이터베이스에서 사용자를 확인하는 대신, 토큰 자체에서 정보를 추출하여 검증하는 것이 더 일반적입니다. JWT 토큰은 자체적으로 사용자 정보와 인증 정보를 포함할 수 있으며 서버 측에서 검증할 수 있습니다.
하지만, 만약 서버 데이터베이스의 사용자 정보와 JWT 토큰의 정보를 비교하여 더 높은 보안을 원하신다면, 캐시를 사용하여 데이터베이스 조회 빈도를 줄이는 것을 고려해 볼 수 있습니다. 예를 들어, Redis와 같은 메모리 기반의 캐시를 사용하여 주기적으로 갱신되는 사용자 정보를 저장하고 검증할 수 있습니다.
또한, 보안을 강화하고 싶으시다면, OAuth2 서버의 권장 사항에 따라 사용자 인증 정보를 비교하는 방법을 적용하거나, 필요에 따라 사용자 정보를 갱신하는 전략을 고려할 수 있습니다. 이를 통해 데이터베이스 조회 빈도와 보안을 균형 잡을 수 있습니다.
스프링 시큐리티와 OAuth2에 관해 더 많은 정보가 필요하시면, 다음의 Spring Authorization Server Reference Guide의 Getting Started 섹션을 참조하실 수 있습니다:
Spring Authorization Server Reference Guide
추가 질문이 있으시면 언제든지 문의해 주세요. 감사합니다.