
이번 포스트는 JwtProvider와 JwtAuthenticationFilter에 중점적으로 내용을 전달할 것입니다!
User, UserDetailService, UserRepository는 다른 포스트에서 소개 하였기 때문에 코드만 작성하겠습니다.
package com.springboot.jwt_securityprac.entity;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.*;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Builder
@Table
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false,unique = true)
private String uid;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String name;
@ElementCollection(fetch = FetchType.EAGER)
@Builder.Default
private List<String> roles = new ArrayList<>();
//계정이 가지고 있는 권한 목록 리턴
@Override
public Collection<? extends GrantedAuthority> getAuthorities(){
return this.roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
//계정의 비밀번호 리턴
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Override
public String getPassword(){
return this.getPassword();
}
//계정의 이름 리턴
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Override
public String getUsername() {
return this.getUid();
}
//계정이 만료됐는지 리턴 - true면 만료 X
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Override
public boolean isAccountNonExpired() {
return true;
}
//계정이 잠겨 있는지 리턴 -> true면 안잠김.
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Override
public boolean isAccountNonLocked() {
return true;
}
//계정의 비밀번호가 만료됐는지 리턴 -> ture면 만료 X
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//계정이 활성화돼 있는지 리턴 -> true면 활성화
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Override
public boolean isEnabled() {
return true;
}
}
현재 ID 값은 인덱스 값이기 때문에 ID 값을 토큰 생성 정보로 사용하기 위해 getByUid()메서드 생성.
package com.springboot.jwt_securityprac.repository;
import com.springboot.jwt_securityprac.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User,Long> {
User getByUid(String uid);
}
package com.springboot.jwt_securityprac.service;
import com.springboot.jwt_securityprac.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
@RequiredArgsConstructor
@Service
public class UserDetailServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
private final Logger logger = LoggerFactory.getLogger(UserDetailServiceImpl.class);
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException{
logger.info("[loadUserByUsername] loadUserByUsername 수횅 . username: {}",username);
return userRepository.getByUid(username);
}
}
Jwt을 생성하고 파싱하는 클래스입니다.
JwtProvider에서 토큰을 생성하고, 토큰으로 인증 정보 조회, 회원 구별 정보 추출 등
여러 메서드를 작성할 수 있습니다.
application.properties 시크릿 키 저장.
springboot.jwt.secret = @awdfsijfncmasopidh213215rw!^
@Component
@RequiredArgsConstructor
public class JwtTokenProvider {
private final UserDetailsService userDetailsService;
private final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class);
@Value("${springboot.jwt.secret}")
private String secretKey = "secretKey";
private final long tokenValidMillisecond = 1000L*60*60; //1시간 토큰 유효
.....
}
@PostConstruct
protected void init(){
logger.info("[init] JwtTokenProvider 내 secretKey 초기화 시작");
System.out.println(secretKey);
//서명을 생성하기 위한 키
secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes(StandardCharsets.UTF_8));
System.out.println(secretKey);
logger.info("[init] JwtTokenProvider 내 secretKey 초기화 완료");
}
@PostConstruct 해당 객체가 빈 객체로 주입된 이후 수행되는 메서드를 가지킵니다.
@Component 어노테이션이 지정돼 있어 어플리케이션이 가동되면 빈으로 자동 주입되는 데 이때 @PostConstruct가 지정돼 있는 init() 메서드가 자동으로 실행됩니다.
secretKey를 Base64 형식으로 인코딩
<인코딩 전/후 텍스트>
-인코딩 전-
@awdfsijfncmasopidh213215rw!^
-인코딩 후(Base64 인코딩 결과)-
QGF3ZGZzaWpmbmNtYXNvcGlkaDIxMzIxNXJ3IV4=
//jwt 토큰 생성
public String createToken(String uid, List<String> roles){
//uid를 이용하여 jwt 생성
Claims claims = Jwts.claims().setSubject(uid);
//클레임에 "roles"라는 이름으로 역할 정보(roles 매개변수)를 추가
claims.put("roles",roles);
Date now = new Date();
String token = Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime()+tokenValidMillisecond))
.signWith(SignatureAlgorithm.HS256,secretKey)
.compact();
logger.info("[createToken] 토큰 생성 완료");
return token;
}
Claims claims = Jwts.claims().setSubject(uid);: 이 부분에서 JWT의 클레임(claim)을 설정합니다. 클레임은 토큰에 포함되는 정보를 나타냅니다. 여기서는 주제 클레임(subject claim)으로 사용자의 고유 식별자인 uid 값을 설정합니다.
claims.put("roles", roles);: 클레임에 "roles"라는 이름으로 역할 정보(roles 매개변수)를 추가합니다. 이것은 사용자 역할과 같은 추가 정보를 JWT에 담을 때 사용됩니다.
Date now = new Date();: 현재 날짜와 시간을 가져옵니다.
String token = Jwts.builder()...: JWT를 빌드합니다. 다음과 같은 정보를 JWT에 설정합니다.
클레임(claims): 앞서 설정한 클레임 객체를 설정합니다.
발행 시간(issuedAt): 현재 시간을 설정합니다.
만료 시간(expiration): 토큰의 유효 기간을 설정합니다. 현재 시간에tokenValidMillisecond (밀리초)를 더한 시간까지 토큰이 유효합니다.
서명(Signature): HMAC-SHA256 알고리즘을 사용하여 시크릿 키(secretKey)로 서명합니다.
이 서명은 토큰이 변경되지 않았음을 검증하는 데 사용됩니다.
compact(): JWT를 문자열로 변환하여 반환합니다. 이 문자열로 토큰이 클라이언트에게 전송되어 인증 및 권한 부여 등에 사용됩니다.
이 메서드는 필터에서 인증이 성공했을 때 SecurityContextHolder에 저장할 Authentication을 생성하는 역할을 합니다.
//jwt 토큰으로 인증 정보 조회
public Authentication getAuthentication(String token){
logger.info("[getAuthentication] 토큰 인증 정보 조회 시작");
UserDetails userDetails = userDetailsService.loadUserByUsername(
this.getUsername(token));
logger.info("[getAuthenticaiton] 토큰 인증 정보 조회 완료, UserDetails userName",userDetails.getUsername());
return new UsernamePasswordAuthenticationToken(userDetails,"",userDetails.getAuthorities());
}
Authentication을 구현하는 편한 방법은 UsernamePasswordAuthenticationToken을 사용하는것이다.

JWT 토큰에서 회원 구별 정보 추출
public String getUsername(String token){
logger.info("[getUsername] 토큰에서 회원 구별 정보 추출");
String info = Jwts.parser()
.setSigningKey(secretKey) // 시크릿키로 jwt 검증
.parseClaimsJws(token) // 토큰 파싱하고 내용 추출
.getBody() // 토큰 본문 가져오기(payload)
//Subject -> 토큰 제목 - 토큰에서 사용자에 대한 식별값
.getSubject(); //클레임에서 "sub" (subject) 필드를 가져와서 회원의 구별 정보를 추출
logger.info("[getUsername] 토큰 기반 회원 구별 정보 추출 완료, info : {}", info);
return info;
}
이때, Jws란 -> 서버에서 인증을 근거로 인증정보를 서버의 private key로 서명 한것을 토큰화 한 것입니다.
HTTP 헤더 정보에 X-AUTH-TOKEN = JWT토큰 추가
public String resolveToken(HttpServletRequest request){
logger.info("[resolveToken] HTTP 헤더에서 Token 값 추출");
return request.getHeader("X-AUTH-TOKEN");
}
JWT 토큰의 유효성 + 만료일 체크
public boolean validationToken(String token) {
logger.info("[validateToken] 토큰 유효 체크 시작");
try {
Jws<Claims> claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token);
logger.info("[validateToken] 토큰 유효 체크 완료");
return !claims.getBody().getExpiration().before(new Date());
} catch (Exception e) {
logger.info("[validateToken] 토큰 유효 체크 예외 발생");
return false;
}
}
}
package com.springboot.jwt_securityprac.jwt;
import io.jsonwebtoken.*;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Date;
import java.util.List;
@Component
@RequiredArgsConstructor
public class JwtTokenProvider {
private final UserDetailsService userDetailsService;
private final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class);
@Value("${springboot.jwt.secret}")
private String secretKey = "secretKey";
private final long tokenValidMillisecond = 1000L*60*60; //1시간 토큰 유효
//시크릿 키 초기화
@PostConstruct
protected void init(){
logger.info("[init] JwtTokenProvider 내 secretKey 초기화 시작");
System.out.println(secretKey);
//서명을 생성하기 위한 키
secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes(StandardCharsets.UTF_8));
System.out.println(secretKey);
logger.info("[init] JwtTokenProvider 내 secretKey 초기화 완료");
}
//jwt 토큰 생성
public String createToken(String uid, List<String> roles){
//uid를 이용하여 jwt 생성
Claims claims = Jwts.claims().setSubject(uid);
//클레임에 "roles"라는 이름으로 역할 정보(roles 매개변수)를 추가
claims.put("roles",roles);
Date now = new Date();
String token = Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime()+tokenValidMillisecond))
.signWith(SignatureAlgorithm.HS256,secretKey)
.compact();
logger.info("[createToken] 토큰 생성 완료");
return token;
}
//jwt 토큰으로 인증 정보 조회
public Authentication getAuthentication(String token){
logger.info("[getAuthentication] 토큰 인증 정보 조회 시작");
UserDetails userDetails = userDetailsService.loadUserByUsername(
this.getUsername(token));
logger.info("[getAuthenticaiton] 토큰 인증 정보 조회 완료, UserDetails userName",userDetails.getUsername());
return new UsernamePasswordAuthenticationToken(userDetails,"",userDetails.getAuthorities());
}
//JWT 토큰에서 회원 구별 정보 추출
public String getUsername(String token){
logger.info("[getUsername] 토큰에서 회원 구별 정보 추출");
String info = Jwts.parser()
.setSigningKey(secretKey) // 시크릿키로 jwt 검증
.parseClaimsJws(token) // 토큰 파싱하고 내용 추출
.getBody() // 토큰 본문 가져오기(payload)
//Subject -> 토큰 제목 - 토큰에서 사용자에 대한 식별값
.getSubject(); //클레임에서 "sub" (subject) 필드를 가져와서 회원의 구별 정보를 추출
logger.info("[getUsername] 토큰 기반 회원 구별 정보 추출 완료, info : {}", info);
return info;
}
//HTTP 헤더 정보에 X-AUTH-TOKEN = JWT토큰 추가
public String resolveToken(HttpServletRequest request){
logger.info("[resolveToken] HTTP 헤더에서 Token 값 추출");
return request.getHeader("X-AUTH-TOKEN");
}
// JWT 토큰의 유효성 + 만료일 체크
public boolean validationToken(String token) {
logger.info("[validateToken] 토큰 유효 체크 시작");
try {
Jws<Claims> claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token);
logger.info("[validateToken] 토큰 유효 체크 완료");
return !claims.getBody().getExpiration().before(new Date());
} catch (Exception e) {
logger.info("[validateToken] 토큰 유효 체크 예외 발생");
return false;
}
}
}
JwtAuthenticationFilter는 JWT 토큰으로 인증하고, SecurityContextHolder에 추가하는 필터를 설정하는 클래스 입니다.
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
private final JwtTokenProvider jwtTokenProvider;
public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider){
this.jwtTokenProvider = jwtTokenProvider;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = jwtTokenProvider.resolveToken(request);
logger.info("[doFilterInternal] token 값 추출 완료. token : {}", token);
logger.info("[doFilterInternal] token 값 유효성 체크 시작");
if(token!=null&& jwtTokenProvider.validationToken(token)){
Authentication authentication = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
logger.info("[doFilterInternal] token 값 유효성 체크 완료");
}
filterChain.doFilter(request,response);
}
}
doFilterInternal 메서드: 이 메서드는 필터가 실제로 동작하는 부분입니다. 모든 HTTP 요청이 이 메서드를 통과하게 됩니다.
jwtTokenProvider.resolveToken(request) 호출: 이 부분에서 jwtTokenProvider 객체를 사용하여 HTTP 요청에서 JWT 토큰을 추출합니다. resolveToken 메서드는 이전에 설명한대로 HTTP 헤더에서 X-AUTH-TOKEN 헤더 값을 가져와서 JWT 토큰으로 사용합니다.
JWT 토큰의 유효성 검사: 추출한 JWT 토큰이 유효한지 확인하기 위해 jwtTokenProvider.validationToken(token)을 호출합니다. 이 부분은 JWT 토큰의 서명을 확인하고, 토큰의 만료 여부를 확인하는 역할을 합니다. 토큰이 유효하면 다음 단계로 이동하고, 그렇지 않으면 유효성 검사에 실패하게 됩니다.
유효한 토큰일 경우 jwtTokenProvider.getAuthentication(token)을 호출하여 JWT 토큰에서 추출한 정보를 기반으로 인증(Authentication) 객체를 생성합니다. 이 인증 객체는 사용자의 인증 정보와 권한을 포함하게 됩니다.
SecurityContextHolder.getContext().setAuthentication(authentication)을 사용하여 Spring Security의 SecurityContext에 인증 정보를 설정합니다. 이렇게 하면 해당 요청에 대한 사용자 인증이 완료되며, 이후의 Spring Security 처리에서 이 인증 정보를 사용할 수 있습니다.
마지막으로 filterChain.doFilter(request, response)를 호출하여 다음 필터로 요청을 전달합니다. 이로써 현재 필터의 처리가 완료되고 다음 단계로 넘어갑니다.