새소식

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
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(); } }

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 반환 } }

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; } }

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; } }

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); }

실제로 사용하지는 않지만 서버 실행 시 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; } }

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); } }

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); } }

 

Spring - Spring Security 6 + JWT 로그인, 로그아웃(2편 - global 파일)

개요Spring Security 6 + JWT 로그인, 로그아웃 과정의 두번째 글인 global 패키지에 대한 글이다.파일 구조/src/main/java/project├── global│ ├── config│ │ └── security│ │ └── SecurityConfig.java│

dev-kimchi.tistory.com

 

Spring - Spring Security 6 + JWT 로그인, 로그아웃(4편 - user 파일)

개요Spring Security 6 + JWT 로그인, 로그아웃 과정의 마지막 글인 user 패키지에 대한 글이다.파일 구조/src/main/java/project├── global│ ├── config│ │ └── security│ │ └── SecurityConfig.java│

dev-kimchi.tistory.com

 

반응형

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.