로그인 로직을 구현했으니 이제 토큰을 발급해주고 JWTFilter에서 토큰을 검증하는 로직을 작성해보자.

JWT 토큰은 Header, Payload, Secret 세가지 부분으로 이루어져 있다.
1) Header
2) Payload
3) Secret
이 프로젝트는 양방향 대칭키 방식을 사용해서 암호화를 진행하겠다.
application.properties
spring.jwt.secret=[암호키]
properties 파일에 암호키를 생성해 놓고 토큰을 발급해줄 JWTUtil 클래스를 만들겠다.
JWTUtil.java
package com.example.classicHub.jwt;
import java.util.Date;
import javax.crypto.SecretKey;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
@Component
public class JWTUtil {
private SecretKey secretKey;
/** 생성자 **/
public JWTUtil(@Value("${spring.jwt.secret}")String secret) {
byte[] byteSecretKey = Decoders.BASE64.decode(secret);
secretKey = Keys.hmacShaKeyFor(byteSecretKey);
}
// 토큰을 받고 검증하는 3가지 메서드
// 토큰을 받고 setSigningKey로 내 프로그램의 secretKey와 동일한지 확인
// parseClaimsJws로 내부 Claims 확인하고 get으로 데이터를 가져옴
// 토큰을 전달 받아서 email 가져오기
public String getEmail(String token) {
return Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token).getBody().get("email", String.class);
}
// 토큰을 전달 받아서 role 가져오기
public String getRole(String token) {
return Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token).getBody().get("role", String.class);
}
// 토큰을 전달 받아서 만료 여부 확인하기
public Boolean isExpired(String token) {
return Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token).getBody().getExpiration().before(new Date());
}
// 토큰 발급
public String createJwt(String email, String role, Long expiredMs) {
// claims 저장
Claims claims = Jwts.claims();
claims.put("email", email);
claims.put("role", role);
return Jwts.builder()
.setClaims(claims) // 토큰에 데이터 저장
.setIssuedAt(new Date(System.currentTimeMillis())) // 발행 시간 저장
.setExpiration(new Date(System.currentTimeMillis() + expiredMs)) // 발행 시간 + 유효 기간 = 만료 시간을 저장
.signWith(secretKey, SignatureAlgorithm.HS256) // 암호키와 HS256 암호화 알고리즘으로 암호키까지 저장
.compact();
}
}
properties에 저장한 암호키를 @Value("${spring.jwt.secret}") 로 가져와서 SercretKey에 저장한다.
세가지 검증 메서드를 작성하고 토큰을 발급하는 메서드를 작성했다.
이제 LoginFilter에서 로그인 성공시점에 jwt를 Headers 부분에 저장한다.
LoginFilter.java
private final JWTUtil jwtutil;
/** 생성자 **/
public LoginFilter(AuthenticationManager authenticationManager, JWTUtil jwtutil) {
this.authenticationManager = authenticationManager;
this.jwtutil = jwtutil;
}
// 로그인 성공시 실행하는 메소드 (여기서 JWT 발급)
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
System.out.println("com.example.classicHub.jwt.LoginFilter : 로그인 성공");
CustomUserDetails customUserDetails = (CustomUserDetails) authResult.getPrincipal();
// 이메일 가져오기
String email = customUserDetails.getEmail();
// 역할 가져오기
Collection<? extends GrantedAuthority> authorities = authResult.getAuthorities();
Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
GrantedAuthority auth = iterator.next();
String role = auth.getAuthority();
// 토큰 만들기
String token = jwtutil.createJwt(email, role, 60*60*10L);
// HTTP 인증 방식은 RFC 7235 정의에 따라 아래 인증 헤더 형태를 가져야 함.
// Authorization : Bearer {인증토큰}
response.addHeader("Authorization", "Bearer " + token);
}
// 로그인 실패시 실행하는 메소드
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
System.out.println("com.example.classicHub.jwt.LoginFilter : 로그인 실패");
// 로그인 실패시 401 응답
response.setStatus(401);
}
Authentication에서 getPrincipal 메서드로 유저를 확인할 수 있다.
CustomUserDetails 타입으로 변환해주고
이메일과 role을 가져왔다. JWTUtil을 생성자 주입 방식으로 받아왔다.
이 때 토큰을 생성할 때 HTTP 인증 방식은 RFC 7235 정의에 따라 아래 인증 헤더 형태를 가져야 한다.
Authorization : Bearer {인증토큰}
토큰을 생성한 뒤 reponse 응답의 header 부분에 jwt 토큰을 발급해 주면 된다.

이제 로그인 성공 시 Header 부분에 jwt 토큰을 발급 받은 것을 확인할 수 있다.
앞으로 어떤 요청을 보낼 때 Header 부분에 이 토큰과 함께 보내주면 된다.

1) 경로 요청이 들어온다.
2) Security Authentication Filter가 검증을 한다.
3) JWT Filter가 토큰을 검증한다.
4) SecurityContextHolderSession 일시적 요청에 대한 Session을 생성한다.
5) 생성된 세션은 요청이 끝나면 소멸된다.
JWTFilter를 만들고 SecurityConfig에 등록해주자.
JWTFilter.java
public class JWTFilter extends OncePerRequestFilter{
private final JWTUtil jwtutil;
/** 생성자 **/
public JWTFilter(JWTUtil jwtutil) {
this.jwtutil = jwtutil;
}
}
JWTFilter는 요청에 대해 한 번만 동작하는 OncePerRequestFilter를 상속 받는다.
부모 메소드 doFilterInternal를 오버라이딩 해서 구현해주자.
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 1) 토큰 검증
// request에서 Authorization 헤더를 찾음
String authorization = request.getHeader("Authorization");
// Authorization 헤더 검증
if(authorization == null || !authorization.startsWith("Bearer ")) { // 토큰이 없거나, 접두사 시작이 Bearer이 아님
System.out.println("com.example.classicHub.jwt.JWTFilter : 토큰 검증 실패");
filterChain.doFilter(request, response);
return;
}
// 2) 토큰 소멸 시간 검증
String token = authorization.split(" ")[1]; // 접두사 부분 제거하고 순수 토큰만 가져옴
if(jwtutil.isExpired(token)) {
System.out.println("com.example.classicHub.jwt.JWTFilter : 토큰 만료됨");
filterChain.doFilter(request, response);
return;
}
// 토큰 검증 성공
System.out.println("com.example.classicHub.jwt.JWTFilter : 토큰 검증 성공");
String email = jwtutil.getEmail(token);
String role = jwtutil.getRole(token);
// User 엔티티를 생성하여 값 할당
User user = new User();
user.setEmail(email);
user.setPassword("temppassword");
user.setRole(role);
// UserDetails에 회원 정보 담기
CustomUserDetails customUserDetails = new CustomUserDetails(user);
// 스프링 시큐리티 인증 토큰 생성
Authentication authToken = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());
// 세션에 사용자 등록
SecurityContextHolder.getContext().setAuthentication(authToken);
// 다음 필터로 request, response 넘기기
filterChain.doFilter(request, response);
}
1) 토큰 검증
request 요청에서 아까 토큰을 저장했던 Authorization 헤더를 찾는다.
여기서 토큰이 없거나, 접두사가 Bearer로 시작하지 않는다면 요청과 응답을 다음 필터로 넘기고 return 한다.
2) 토큰 만료 검증
토큰은 Bearer {토큰} 이니까 먼저 'Bearer '부분을 제거하고 순수 토큰을 얻는다.
아까 JWTUtil에서 작성한 isExpired 메서드로 만료됐는지 확인한다.
모든 검증이 끝나면 회원 정보를 담은 인증 토큰을 생성하고 임시 세션에 사용자를 등록한다.
마지막으로 SecurityConfig filterChain 부분에 JWTFilter를 등록해주자
SecurityConfig.java
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
// csrf disable
http.csrf((auth) -> auth.disable());
// Form 로그인 방식 disable
http.formLogin((auth) -> auth.disable());
// http basic 인증 방식 disable
http.httpBasic((auth) -> auth.disable());
// 경로별 인가 작업
http.authorizeHttpRequests((auth) -> auth
.requestMatchers("/login", "/","/join").permitAll() // 이 경로에 대해서는 모든 권한을 허가한다.
.requestMatchers("/admin").hasRole("ADMIN") // 이 경로에 대해서는 해당 권한이 있는 사용자만 접근을 허가한다.
.anyRequest().authenticated() // 그 외 경로에는 로그인된 사용자만 권한을 허가한다.
);
// JWTFilter 등록
http.addFilterBefore(new JWTFilter(jwtutil), LoginFilter.class);
// 커스텀 필터 추가
http.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration),jwtutil), UsernamePasswordAuthenticationFilter.class);
// 세션 설정
http.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // JWT를 통한 인증/인가를 위해서 세션을 STATELESS 상태로 설정.
);
return http.build();
}


이제 permitAll 되지 않은 경로에 jwt토큰을 같이 보내주면 접근에 성공한다.
마지막으로 현재 세션에 저장된 email, role을 확인하고 싶다면 SecurityContextHolder에 접근 하면 된다.
@GetMapping("/")
public String mainP() {
// 현재 세션 사용자 이메일
String email = SecurityContextHolder.getContext().getAuthentication().getName();
// 현재 세션 사용자 role
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
Iterator<? extends GrantedAuthority> iter = authorities.iterator();
GrantedAuthority auth = iter.next();
String role = auth.getAuthority();
return "main page" + email + " " + role;
}
