Java/Spring
Spring - Spring Security 6 + JWT 로그인, 로그아웃(3편 - jwt 파일)
- -
개요
Spring Security 6 + JWT 로그인, 로그아웃 과정의 세번째 글인 jwt 패키지에 대한 글이다.
파일 구조
/src/main/java/project
├── global
│ ├── config
│ │ └── security
│ │ └── SecurityConfig.java
│ └── entity
│ └── AuditingFields.java
├── jwt
│ ├── entity
│ │ └── Token.java
│ ├── filter
│ │ └── TokenAuthenticationFilter.java
│ ├── infrastructure
│ │ ├── CustomUserDetails.java
│ │ └── JwtAuthenticationToken.java
│ ├── repository
│ │ └── TokenRepository.java
│ ├── service
│ │ ├── CustomUserDetailsService.java
│ │ └── TokenService.java
│ └── util
│ └── JwtTokenizer.java
└── user
├── controller
│ └── UserController.java
├── dto
│ ├── UserJoinDto.java
│ ├── UserLoginDto.java
│ └── UserLoginResponseDto.java
├── entity
│ ├── Authority.java
│ └── User.java
├── repository
│ └── UserRepository.java
└── service
└── UserService.java
Token.java
import jakarta.persistence.*;
import lombok.*;
@Entity
@Table(name = "tokens")
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter
public class Token extends AuditingFields {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@Setter
@Column(nullable = false, length = 300)
private String accessToken;
@Column(nullable = false, length = 300)
private String refreshToken;
@Column(nullable = false)
private String grantType;
// 업데이트 빌더
public Token update(String accessToken, String refreshToken, String grantType) {
return Token.builder()
.id(this.id)
.user(this.user)
.accessToken(accessToken)
.refreshToken(refreshToken)
.grantType(grantType)
.build();
}
}
TokenAuthenticationFilter.java
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
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.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* JWT 토큰 인증 필터 클래스
*/
@Slf4j
@RequiredArgsConstructor
public class TokenAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenizer jwtTokenizer;
/**
* 필터 메서드
* 각 요청마다 JWT 토큰을 검증하고 인증을 설정
*
* @param request 요청 객체
* @param response 응답 객체
* @param filterChain 필터 체인
* @throws ServletException 서블릿 예외
* @throws IOException 입출력 예외
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = getToken(request); // 요청에서 토큰을 추출
if (StringUtils.hasText(token)) {
try {
// 토큰을 사용하여 인증 설정
getAuthentication(token);
} catch (ExpiredJwtException e) { // 토큰 만료 시
request.setAttribute("exception", "EXPIRED_TOKEN");
log.error("Expired Token : {}", token, e);
throw new BadCredentialsException("Expired token exception", e);
} catch (UnsupportedJwtException e) { // 지원하지 않는 토큰 사용 시
request.setAttribute("exception", "UNSUPPORTED_TOKEN");
log.error("Unsupported Token: {}", token, e);
throw new BadCredentialsException("Unsupported token exception", e);
} catch (MalformedJwtException e) { // 유효하지 않은 토큰 사용 시
request.setAttribute("exception", "INVALID_TOKEN");
log.error("Invalid Token: {}", token, e);
throw new BadCredentialsException("Invalid token exception", e);
} catch (IllegalArgumentException e) { // 올바르지 않은 파라미터 전달 시
request.setAttribute("exception", "NOT_FOUND_TOKEN");
log.error("Token not found: {}", token, e);
throw new BadCredentialsException("Token not found exception", e);
} catch (Exception e) { // 알 수 없는 예외 발생 시
request.setAttribute("exception", "NOT_FOUND_TOKEN");
log.error("JWT Filter - Internal Error: {}", token, e);
throw new BadCredentialsException("JWT filter internal exception", e);
}
}
filterChain.doFilter(request, response); // 다음 필터로 요청을 전달
}
/**
* 토큰을 사용하여 인증 설정
*
* @param token JWT 토큰
*/
private void getAuthentication(String token) {
Claims claims = jwtTokenizer.parseAccessToken(token); // 토큰에서 클레임을 파싱
String email = claims.getSubject(); // 이메일을 가져옴
Long userId = claims.get("userId", Long.class); // 사용자 ID를 가져옴
String nickname = claims.get("nickname", String.class); // 이름을 가져옴
Authority authority = Authority.valueOf(claims.get("authority", String.class)); // 사용자 권한을 가져옴
Collection<? extends GrantedAuthority> authorities = Collections.singletonList(authority);
CustomUserDetails userDetails = new CustomUserDetails(email, "", nickname, (List<GrantedAuthority>) authorities);
Authentication authentication = new JwtAuthenticationToken(authorities, userDetails, null); // 인증 객체 생성
SecurityContextHolder.getContext().setAuthentication(authentication); // SecurityContextHolder에 인증 객체 설정
}
/**
* 요청에서 토큰을 추출
*
* @param request 요청 객체
* @return JWT 토큰
*/
private String getToken(HttpServletRequest request) {
Cookie[] cookies = request.getCookies(); // 쿠키에서 토큰을 찾음
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("accessToken".equals(cookie.getName())) {
return cookie.getValue(); // accessToken 쿠키에서 토큰 반환
}
}
}
return null; // 토큰을 찾지 못한 경우 null 반환
}
}
CustomUserDetails.java
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
/**
* 사용자 인증 및 권한 관리를 위한 사용자 세부 정보 클래스
*/
public class CustomUserDetails implements UserDetails {
private final String email; // 사용자 이름
private final String password; // 비밀번호
private final String nickname; // 이름
private final List<GrantedAuthority> authorities; // 권한 목록
/**
* 생성자
*
* @param email 이메일
* @param password 비밀번호
* @param nickname 닉네임
* @param authorities 권한 목록
*/
public CustomUserDetails(String email, String password, String nickname, List<GrantedAuthority> authorities){
this.email = email;
this.password = password;
this.nickname = nickname;
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return email;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
JwtAuthenticationToken.java
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
/**
* JWT 인증 토큰 클래스
*/
public class JwtAuthenticationToken extends AbstractAuthenticationToken {
private String token; // JWT 토큰
private Object principal; // 주체 (사용자)
private Object credentials; // 인증 정보 (자격 증명)
/**
* 인증된 토큰을 생성하는 생성자
*
* @param authorities 권한
* @param principal 주체 (사용자)
* @param credentials 인증 정보 (자격 증명)
*/
public JwtAuthenticationToken(Collection<? extends GrantedAuthority> authorities, Object principal, Object credentials) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(true); // 토큰을 인증된 상태로 설정
}
/**
* 인증되지 않은 토큰을 생성하는 생성자
*
* @param token JWT 토큰
*/
public JwtAuthenticationToken(String token) {
super(null);
this.token = token;
this.setAuthenticated(false); // 토큰을 인증되지 않은 상태로 설정
}
/**
* 자격 증명을 반환
*
* @return 자격 증명
*/
@Override
public Object getCredentials() {
return this.credentials;
}
/**
* 주체 (사용자)를 반환
*
* @return 주체 (사용자)
*/
@Override
public Object getPrincipal() {
return this.principal;
}
}
TokenRepository.java
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface TokenRepository extends JpaRepository<Token, Long> {
Token findByUserId(Long userId);
Optional<Token> findByAccessToken(String accessToken);
Optional<Token> findByRefreshToken(String refreshToken);
}
CustomUserDetailsService.java
실제로 사용하지는 않지만 서버 실행 시 spring security 관리자계정 자동으로 생성되지 않게 설정하기 위함.
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return null;
}
}
TokenService.java
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
@Service
@RequiredArgsConstructor
public class TokenService {
private final TokenRepository tokenRepository;
/**
* 토큰 사용자의 토큰이 저장되어 있을 경우 update, 없을 경우 create
* @param token Token 데이터
*/
@Transactional
public void saveOrUpdate(Token token) {
Token oldToken = tokenRepository.findByUserId(token.getUser().getId());
if (oldToken != null) {
oldToken.update(token.getAccessToken(), token.getRefreshToken(), token.getGrantType());
tokenRepository.save(oldToken);
} else {
tokenRepository.save(token);
}
}
/**
* access token으로 Token 데이터를 가져와 삭제하는 메소드
*
* @param token
*/
@Transactional
public void deleteByAccessToken(String token) {
tokenRepository.findByAccessToken(token).ifPresent(tokenRepository::delete);
}
/**
* access token으로 Token 데이터를 가져오는 메소드
*
* @param token access token
* @return Token 데이터
*/
@Transactional(readOnly = true)
public Optional<Token> findByAccessToken(String token) {
return tokenRepository.findByAccessToken(token);
}
/**
* refresh token으로 Token 데이터를 가져오는 메소드
*
* @param token refresh token
* @return Token 데이터
*/
@Transactional(readOnly = true)
public Optional<Token> findByRefreshToken(String token) {
return tokenRepository.findByRefreshToken(token);
}
/**
* refresh token으로 Token 데이터를 가져와 access token 값을 업데이트하는 메소드
*
* @param refreshToken
* @param accessToken
*/
@Transactional
public void updateByRefreshToken(String refreshToken, String accessToken) {
Token token = tokenRepository.findByRefreshToken(refreshToken)
.orElseThrow(() -> new RestApiException(TokenExceptionCode.JWT_UNKNOWN_ERROR));
token.setAccessToken(accessToken);
tokenRepository.save(token);
}
}
JwtTokenizer.java
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Date;
/**
* JWT 생성 / 검증 유틸리티 클래스
*/
@Component
public class JwtTokenizer {
private final byte[] accessSecret;
private final byte[] refreshSecret;
public static Long accessTokenExpire;
public static Long refreshTokenExpire;
public JwtTokenizer(@Value("${jwt.accessSecret}") String accessSecret, @Value("${jwt.refreshSecret}") String refreshSecret, @Value("${jwt.accessTokenExpire}") Long accessTokenExpire, @Value("${jwt.refreshTokenExpire}") Long refreshTokenExpire) {
this.accessSecret = accessSecret.getBytes(StandardCharsets.UTF_8);
this.refreshSecret = refreshSecret.getBytes(StandardCharsets.UTF_8);
this.accessTokenExpire = accessTokenExpire;
this.refreshTokenExpire = refreshTokenExpire;
}
/**
* AccessToken 생성
*
* @param id
* @param email
* @param nickname
* @param authority
* @return AccessToken
*/
public String createAccessToken(Long id, String email, String nickname, Authority authority) {
return createToken(id, email, nickname, authority, accessTokenExpire, accessSecret);
}
/**
* RefreshToken 생성
*
* @param id
* @param email
* @param nickname
* @param authority
* @return RefreshToken
*/
public String createRefreshToken(Long id, String email, String nickname, Authority authority) {
return createToken(id, email, nickname, authority, refreshTokenExpire, refreshSecret);
}
/**
* Jwts 빌더를 사용하여 token 생성
*
* @param id
* @param email
* @param nickname
* @param authority
* @param expire
* @param secretKey
* @return
*/
private String createToken(Long id, String email, String nickname, Authority authority, Long expire, byte[] secretKey) {
// 기본으로 가지고 있는 claim : subject
Claims claims = Jwts.claims().setSubject(email);
claims.put("authority", authority);
claims.put("userId", id);
claims.put("nickname", nickname);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date())
.setExpiration(new Date(new Date().getTime() + expire))
.signWith(getSigningKey(secretKey))
.compact();
}
/**
* 토큰에서 유저 아이디 얻기
*
* @param token 토큰
* @return userId
*/
public Long getUserIdFromToken(String token) {
String[] tokenArr = token.split(" ");
token = tokenArr[1];
Claims claims = parseToken(token, accessSecret);
return Long.valueOf((Integer) claims.get("userId"));
}
public Claims parseAccessToken(String accessToken) {
return parseToken(accessToken, accessSecret);
}
public Claims parseRefreshToken(String refreshToken) {
return parseToken(refreshToken, refreshSecret);
}
public Claims parseToken(String token, byte[] secretKey) {
Claims claims = null;
try {
claims = Jwts.parserBuilder()
.setSigningKey(getSigningKey(secretKey))
.build()
.parseClaimsJws(token)
.getBody();
} catch (SignatureException e) { // 토큰 유효성 체크 실패 시
throw new RestApiException(TokenExceptionCode.JWT_INVALID_ERROR);
}
return claims;
}
/**
* @param secretKey - byte형식
* @return Key 형식 시크릿 키
*/
public static Key getSigningKey(byte[] secretKey) {
return Keys.hmacShaKeyFor(secretKey);
}
}
이전 글
다음 글
반응형
Contents
소중한 공감 감사합니다