새소식

TIL

[TIL] 230607 <Spring> WebSecurityConfig, JwtAuthorizationFilter, JwtAuthenticationFilter, JwtUtil, UserDetailsServiceImpl

  • -


WebSecurityConfig

`WebSecurityConfig` 클래스는 Spring Boot 애플리케이션이 시작될 때 작동됩니다. 
이 클래스는 Spring Security의 보안 설정을 정의하고, 애플리케이션 컨텍스트에 설정을 등록하는 역할을 합니다. 
Spring Boot는 애플리케이션 시작 시 모든 `@Configuration` 클래스와 `@Bean` 메서드를 검색하고 초기화하므로,
`WebSecurityConfig`도 이 과정에서 함께 초기화됩니다.

 

 

< 작동과정 >

1. 애플리케이션 시작

  • Spring Boot 애플리케이션이 시작되면, Spring 프레임워크는 애플리케이션 컨텍스트를 초기화합니다.
  • 이 과정에서 애플리케이션의 모든 구성 요소가 스캔되고, 필요한 빈들이 생성 및 초기화됩니다.



2. Configuration 클래스 로드와 Bean 초기화

  • Spring 프레임워크는 클래스패스를 스캔하여 모든 @Configuration 애너테이션이 붙은 클래스들을 찾습니다.
  • @Configuration 애너테이션이 붙은 WebSecurityConfig 클래스도 이 과정에서 발견됩니다.



3. @EnableWebSecurity 애너테이션

  • @EnableWebSecurity 애너테이션이 WebSecurityConfig 클래스에 붙어있으면, Spring Security 설정이 활성화됩니다.
  • 이 애너테이션은 Spring Security의 설정 클래스를 활성화하고, 기본 보안 설정을 초기화합니다.

 

4. Bean 초기화

  • Spring 프레임워크는 @Configuration 클래스 내의 @Bean 메서드들을 호출하여 필요한 빈들을 생성하고, 애플리케이션 컨텍스트에 등록합니다.
  • WebSecurityConfig 클래스의 다음과 같은 @Bean 메서드들이 호출됩니다 :
    • passwordEncoder()
    • authenticationManager(AuthenticationConfiguration configuration)
    • jwtAuthenticationFilter()
    • jwtAuthorizationFilter()

 

5. HTTP 보안 설정 적용

  • WebSecurityConfig 클래스의 securityFilterChain(HttpSecurity http) 메서드가 호출됩니다.
  • 이 메서드 내에서 HttpSecurity 객체를 사용하여 HTTP 보안 설정이 정의됩니다
    ㄴ 이 설정은 CSRF 보호, 세션 관리, 요청 권한, 로그인 페이지, 필터 추가 등을 포함합니다.

 

6. SecurityFilterChain 생성

  • securityFilterChain(HttpSecurity http) 메서드의 마지막 부분에서 http.build()가 호출됩니다.
  • http.build()는 설정된 HttpSecurity 객체를 기반으로 SecurityFilterChain을 생성합니다.
  • 생성된 SecurityFilterChain은 애플리케이션 컨텍스트에 등록되어, Spring Security가 요청을 처리할 때 사용됩니다.

 

이 전체 과정은 Spring Boot 애플리케이션이 시작될 때 자동으로 이루어지며, 개발자가 특별히 호출하지 않아도 됩니다.
Spring Boot와 Spring Security가 연동되어 애플리케이션의 보안 설정을 초기화하고 적용합니다.

 

package com.sparta.myselectshop.config;


import com.sparta.myselectshop.jwt.JwtUtil;
import com.sparta.myselectshop.security.JwtAuthenticationFilter;
import com.sparta.myselectshop.security.JwtAuthorizationFilter;
import com.sparta.myselectshop.security.UserDetailsServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity // Spring Security 지원을 가능하게 함
@RequiredArgsConstructor
public class WebSecurityConfig {

	private final JwtUtil jwtUtil;
	private final UserDetailsServiceImpl userDetailsService;  //사용자 정보를 로드하는 서비스
	private final AuthenticationConfiguration authenticationConfiguration;  //인증 구성을 위한 클래스.

	@Bean
	public PasswordEncoder passwordEncoder() {  //비밀번호를 암호화하기 위한 PasswordEncoder를 빈으로 정의
		return new BCryptPasswordEncoder();  //여기서는 BCryptPasswordEncoder를 사용
	}

	@Bean
	// 인증 관리자를 빈으로 정의
	public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
		return configuration.getAuthenticationManager();  //authenticationConfiguration을 이용하여 AuthenticationManager를 반환
	}

	@Bean
	// JWT 인증 필터를 빈으로 정의
	public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
		// 이 필터는 JWT를 사용하여 인증을 처리하며, 인증 관리자를 설정
		JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtUtil);
		filter.setAuthenticationManager(authenticationManager(authenticationConfiguration));
		return filter;
	}

	@Bean
	// JWT 인가(권한 부여) 필터를 빈으로 정의
	public JwtAuthorizationFilter jwtAuthorizationFilter() {
		// 이 필터는 JWT를 사용하여 권한 부여를 처리
		return new JwtAuthorizationFilter(jwtUtil, userDetailsService);
	}

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		// CSRF 설정
		http.csrf((csrf) -> csrf.disable());  //CSRF 보호를 비활성화

		// 기본 설정인 Session 방식은 사용하지 않고 JWT 방식을 사용하기 위한 설정
		http.sessionManagement((sessionManagement) ->
			sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
		);

		// 요청 권한 설정: 특정 URL 패턴에 대한 접근 권한을 설정
		http.authorizeHttpRequests((authorizeHttpRequests) ->
			authorizeHttpRequests
				.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
				.requestMatchers("/").permitAll() // 메인 페이지 요청 허가
				.requestMatchers("/api/user/**").permitAll() // '/api/user/'로 시작하는 요청 모두 접근 허가
				.anyRequest().authenticated() // 그 외 모든 요청 인증처리
		);

		// 폼 로그인 설정: 로그인 페이지를 /api/user/login-page로 설정하고 접근을 허용
		http.formLogin((formLogin) ->
			formLogin
				.loginPage("/api/user/login-page").permitAll()
		);

		// 필터 관리: JWT 인증 필터와 권한 부여 필터를 필터 체인에 추가
		http.addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter.class);
		http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

		// SecurityFilterChain 객체를 생성하고 반환
		return http.build();
	}
}

 

필터 체인 동작 순서

  1. JwtAuthorizationFilter: 각 요청마다 JWT를 확인하고, 유효한 경우 사용자 정보를 설정합니다. 이 필터는 모든 요청에 대해 가장 먼저 실행됩니다.
  2. JwtAuthenticationFilter: 로그인 요청을 처리하고 JWT를 생성합니다. 이 필터는 사용자가 로그인할 때 호출됩니다.
  3. UsernamePasswordAuthenticationFilter: 사용자가 로그인 폼을 통해 인증을 시도할 때 가장 먼저 작동하는 필터입니다. 기본적인 사용자 이름과 비밀번호 인증을 처리합니다.

 


1) JwtAuthorizationFilter

`JwtAuthorizationFilter` 클래스는 Spring Security의 `OncePerRequestFilter`를 확장하여 JWT를 사용한 인증 및 권한 부여를 처리하는 필터입니다. 
이 클래스의 역할은 클라이언트로부터 전송된 JWT 토큰을 가져와서 유효성을 검사하고, 토큰에서 추출한 정보를 사용하여 사용자를 인증하고 인증된 사용자에게 적절한 권한을 부여하는 것입니다.

 


1. JWT 토큰 검증 : 클라이언트로부터 전송된 HTTP 요청의 헤더에서 JWT 토큰을 추출하고, 해당 토큰의 유효성을 검사합니다.
   
2. JWT 토큰 파싱 : 유효한 JWT 토큰에서 사용자 정보(Claims)를 추출합니다.

3. 사용자 인증 : 추출한 사용자 정보를 기반으로 Spring Security의 UserDetailsService를 사용하여 사용자를 인증합니다.

4. 권한 부여 : 인증된 사용자에 대한 Authentication 객체를 생성하고, 해당 사용자에게 적절한 권한을 부여합니다.

5. Spring Security에 인증 설정 : 생성된 인증 객체를 Spring Security의 SecurityContext에 설정하여 현재 사용자의 인증 상태를 유지합니다.

package com.sparta.myselectshop.security;

import com.sparta.myselectshop.jwt.JwtUtil;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Slf4j(topic = "JWT 검증 및 인가")
public class JwtAuthorizationFilter extends OncePerRequestFilter {

	private final JwtUtil jwtUtil;
	private final UserDetailsServiceImpl userDetailsService;

	public JwtAuthorizationFilter(JwtUtil jwtUtil, UserDetailsServiceImpl userDetailsService) {
		this.jwtUtil = jwtUtil;
		this.userDetailsService = userDetailsService;
	}

	@Override
	protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) throws ServletException, IOException {
		// HTTP 요청에서 JWT 토큰 추출
		String tokenValue = jwtUtil.getJwtFromHeader(req);

		if (StringUtils.hasText(tokenValue)) {
			// JWT 토큰 유효성 검증
			if (!jwtUtil.validateToken(tokenValue)) {
				// 유효하지 않은 토큰이면 에러 로깅 후 종료
				log.error("Token Error");
				return;
			}
			// JWT 토큰으로부터 사용자 정보(Claims) 추출
			Claims info = jwtUtil.getUserInfoFromToken(tokenValue);

			try {
				// 사용자 인증 처리
				setAuthentication(info.getSubject());
			} catch (Exception e) {
				// 인증 처리 중 예외 발생 시 에러 로깅 후 종료
				log.error(e.getMessage());
				return;
			}
		}

		// 다음 필터(또는 서블릿)로 요청 전달
		filterChain.doFilter(req, res);
	}

	// 사용자 인증 처리
	public void setAuthentication(String username) {
		// 빈 SecurityContext 생성
		SecurityContext context = SecurityContextHolder.createEmptyContext();
		// 사용자의 인증 객체 생성
		Authentication authentication = createAuthentication(username);
		// SecurityContext에 인증 객체 설정
		context.setAuthentication(authentication);

		// SecurityContext를 SecurityContextHolder에 설정
		SecurityContextHolder.setContext(context);
	}

	// 사용자의 인증 객체 생성
	private Authentication createAuthentication(String username) {
		// 사용자 정보 조회
		UserDetails userDetails = userDetailsService.loadUserByUsername(username);
		// UserDetails를 사용하여 인증 토큰 생성
		return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
	}
}

 

(1) JwtUtil

JwtUtil 클래스는 JWT 토큰을 생성, 검증하고 사용자 정보를 추출하는 역할을 합니다.

@PostConstruct 어노테이션이 붙은 init() 메서드에서는 Base64로 인코딩된 Secret Key를 디코딩하여 키 객체를 초기화합니다.

 

JwtAuthorizationFilter의 doFilterInternal()에서
JwtUtil의 getJwtFromHeader(), validateToken(), getUserInfoFromToken()사용!
1. getJwtFromHeader : HTTP 요청의 헤더에서 JWT 토큰을 추출
2. validateToken : 주어진 JWT 토큰이 유효한지 검증
3. getUserInfoFromToken : JWT 토큰에서 사용자 정보를 추출


package com.sparta.myselectshop.jwt;

import com.sparta.myselectshop.entity.UserRoleEnum;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.security.Key;
import java.util.Base64;
import java.util.Date;

@Slf4j(topic = "JwtUtil")
@Component
public class JwtUtil {
	// Header KEY 값
	public static final String AUTHORIZATION_HEADER = "Authorization";
	// 사용자 권한 값의 KEY
	public static final String AUTHORIZATION_KEY = "auth";
	// Token 식별자
	public static final String BEARER_PREFIX = "Bearer ";
	// 토큰 만료시간
	private final long TOKEN_TIME = 60 * 60 * 1000L; // 60분

	@Value("${jwt.secret.key}") // Base64 Encode 한 SecretKey
	private String secretKey;
	private Key key;
	private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

	// 객체 초기화
	@PostConstruct
	public void init() {
		// SecretKey를 Base64 디코딩하여 키 초기화
		byte[] bytes = Base64.getDecoder().decode(secretKey);
		key = Keys.hmacShaKeyFor(bytes);
	}

	
	// JwtAuthenticationFilter의 successfulAuthentication()에서 사용됨 (로그인 성공 시)
	// 토큰 생성
	public String createToken(String username, UserRoleEnum role) {
		Date date = new Date();

		return BEARER_PREFIX +
			Jwts.builder()
				.setSubject(username) // 사용자 식별자값(ID)
				.claim(AUTHORIZATION_KEY, role) // 사용자 권한
				.setExpiration(new Date(date.getTime() + TOKEN_TIME)) // 만료 시간
				.setIssuedAt(date) // 발급일
				.signWith(key, signatureAlgorithm) // 암호화 알고리즘
				.compact(); // 토큰 생성
	}


	// JwtAuthorizationFilter의 doFilterInternal()에서 사용됨
	// HTTP 요청 헤더에서 JWT 추출 메서드
	public String getJwtFromHeader(HttpServletRequest request) {
		String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
		// JWT 토큰이 존재하고 "Bearer " 접두사로 시작할 경우 토큰 반환
		if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
			return bearerToken.substring(7);  // "Bearer " 접두사 제거 후 토큰 반환
		}
		return null;  // 토큰 없을 경우 null 반환
	}


	// JwtAuthorizationFilter의 doFilterInternal()에서 사용됨
	// 토큰 검증
	public boolean validateToken(String token) {
		try {
			// 주어진 토큰의 유효성을 검증하고 유효할 경우 true 반환
			Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
			return true;
		} catch (SecurityException | MalformedJwtException | SignatureException e) {
			log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
		} catch (ExpiredJwtException e) {
			log.error("Expired JWT token, 만료된 JWT token 입니다.");
		} catch (UnsupportedJwtException e) {
			log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
		} catch (IllegalArgumentException e) {
			log.error("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
		}
		return false;  // 유효하지 않은 경우 false 반환
	}


	// JwtAuthorizationFilter의 doFilterInternal()에서 사용됨
	// 토큰에서 사용자 정보 추출 메서드
	public Claims getUserInfoFromToken(String token) {
		// 주어진 토큰에서 클레임 정보(사용자 정보)를 파싱하여 반환
		return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
	}
}

 

 

 

(2) UserDetailsServiceImpl

 

  • UserDetails
    - 검증된 UserDetails는 UsernamePasswordAuthenticationToken 타입의 Authentication를 만들 때 사용되며 해당 인증객체는 SecurityContextHolder에 세팅됨
    - Custom하여 사용 가능
           
  • UserDetailsImpl
이 클래스는 Spring Security의 `UserDetails` 인터페이스를 구현하여 사용자 정보를 제공하는 역할을 합니다. 
사용자의 이름, 비밀번호, 권한 등의 정보를 제공합니다. 
또한 사용자 계정의 상태를 나타내는 여러 메서드를 구현하여 사용자 계정의 유효성을 확인합니다.
package com.sparta.myselectshop.security;

import com.sparta.myselectshop.entity.User;
import com.sparta.myselectshop.entity.UserRoleEnum;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;

public class UserDetailsImpl implements UserDetails {

	private final User user;

	public UserDetailsImpl(User user) {
		this.user = user;
	}

	public User getUser() {
		return user;
	}

	@Override
	public String getPassword() {
		return user.getPassword();
	}

	@Override
	public String getUsername() {
		return user.getUsername();
	}

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		// 사용자의 역할을 가져옴
		UserRoleEnum role = user.getRole();
		// 사용자의 역할에서 권한(Authority)을 가져옴
		String authority = role.getAuthority();

		// Spring Security의 SimpleGrantedAuthority를 사용하여 권한 객체를 생성
		SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
		// 권한 객체를 담을 Collection을 생성
		Collection<GrantedAuthority> authorities = new ArrayList<>();
		// 생성한 권한 객체를 Collection에 추가
		authorities.add(simpleGrantedAuthority);

		// 생성한 권한 목록을 반환
		return authorities;
	}

	@Override
	public boolean isAccountNonExpired() {
		return true;
	}

	@Override
	public boolean isAccountNonLocked() {
		return true;
	}

	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}

	@Override
	public boolean isEnabled() {
		return true;
	}
}

 

 

  • UserDetailsService
    - UserDetailsService는 username/password 인증방식을 사용할 때 사용자를 조회하고 검증한 후 UserDetails를 반환
    - Custom하여 Bean으로 등록 후 사용 가능
           

  • UserDetailsServiceImpl
이 클래스는 Spring Security의 `UserDetailsService`를 구현하여 사용자 정보를 로드하는 역할을 합니다.
loadUserByUsername 메서드에서는 주어진 사용자 이름에 해당하는 사용자 정보를 데이터베이스에서 찾아서
UserDetailsImpl 객체로 래핑하여 반환합니다.
사용자를 찾을 수 없는 경우 UsernameNotFoundException을 발생시킵니다.
package com.sparta.myselectshop.security;

import com.sparta.myselectshop.entity.User;
import com.sparta.myselectshop.repository.UserRepository;
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 UserDetailsServiceImpl implements UserDetailsService {

	private final UserRepository userRepository;

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		// 주어진 사용자 이름으로 사용자를 데이터베이스에서 찾음
		User user = userRepository.findByUsername(username)
			.orElseThrow(() -> new UsernameNotFoundException("Not Found " + username));

		return new UserDetailsImpl(user);
	}
}

 

 


2) JwtAuthenticationFilter

이 클래스는 사용자의 로그인 인증을 처리하고 JWT 토큰을 생성하는 필터 클래스입니다.
UsernamePasswordAuthenticationFilter를 확장하여 사용자의 로그인 시도를 처리합니다.

 

  • 로그인 요청 처리: /api/user/login URL로 들어오는 로그인 요청을 처리합니다.
  • 인증 시도: 클라이언트로부터 받은 사용자명과 비밀번호를 사용하여 인증을 시도합니다.
  • 인증 성공 시 JWT 생성: 인증이 성공하면 JWT 토큰을 생성하고 이를 응답 헤더에 추가합니다.
  • 인증 실패 시 처리: 인증이 실패하면 401 상태 코드를 반환합니다.

 

package com.sparta.myselectshop.security;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.sparta.myselectshop.dto.LoginRequestDto;
import com.sparta.myselectshop.entity.UserRoleEnum;
import com.sparta.myselectshop.jwt.JwtUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import java.io.IOException;

@Slf4j(topic = "로그인 및 JWT 생성")
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
	private final JwtUtil jwtUtil;

	public JwtAuthenticationFilter(JwtUtil jwtUtil) {
		this.jwtUtil = jwtUtil;
		// 로그인 처리 URL 설정
		setFilterProcessesUrl("/api/user/login");
	}


	// 로그인 시도 처리
	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
		try {
			// 요청에서 로그인 정보를 읽어와 DTO에 매핑
			LoginRequestDto requestDto = new ObjectMapper().readValue(request.getInputStream(), LoginRequestDto.class);

			// 인증 매니저를 통해 사용자 인증 시도
			return getAuthenticationManager().authenticate(
				new UsernamePasswordAuthenticationToken(
					requestDto.getUsername(),  // 사용자명
					requestDto.getPassword(),  // 비밀번호
					null  // 권한 목록은 null로 전달
				)
			);
		} catch (IOException e) {
			// 예외 발생 시 로그 출력 및 RuntimeException 던지기
			log.error(e.getMessage());
			throw new RuntimeException(e.getMessage());
		}
	}


	// 로그인 성공 시 처리
	@Override
	protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) {
		// 사용자명과 역할 정보를 가져옴
		String username = ((UserDetailsImpl) authResult.getPrincipal()).getUsername();
		UserRoleEnum role = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getRole();

		// JWT 토큰 생성
		String token = jwtUtil.createToken(username, role);
		// 응답 헤더에 JWT 토큰 추가
		response.addHeader(JwtUtil.AUTHORIZATION_HEADER, token);
	}

	// 로그인 실패 시 처리
	@Override
	protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
		response.setStatus(401);
	}

}

 

 

(1) JwtUtil

JwtUtil 클래스는 JWT 토큰을 생성, 검증하고 사용자 정보를 추출하는 역할을 합니다.

@PostConstruct 어노테이션이 붙은 init() 메서드에서는 Base64로 인코딩된 Secret Key를 디코딩하여 키 객체를 초기화합니다.

JwtAuthenticationFilter의 successfulAuthentication()에서 JwtUtil의 createToken()사용
ㄴ 토큰 생성(createToken) : 사용자 이름과 권한 정보를 받아 JWT 토큰을 생성합니다.

package com.sparta.myselectshop.jwt;

import com.sparta.myselectshop.entity.UserRoleEnum;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.security.Key;
import java.util.Base64;
import java.util.Date;

@Slf4j(topic = "JwtUtil")
@Component
public class JwtUtil {
	// Header KEY 값
	public static final String AUTHORIZATION_HEADER = "Authorization";
	// 사용자 권한 값의 KEY
	public static final String AUTHORIZATION_KEY = "auth";
	// Token 식별자
	public static final String BEARER_PREFIX = "Bearer ";
	// 토큰 만료시간
	private final long TOKEN_TIME = 60 * 60 * 1000L; // 60분

	@Value("${jwt.secret.key}") // Base64 Encode 한 SecretKey
	private String secretKey;
	private Key key;
	private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

	// 객체 초기화
	@PostConstruct
	public void init() {
		// SecretKey를 Base64 디코딩하여 키 초기화
		byte[] bytes = Base64.getDecoder().decode(secretKey);
		key = Keys.hmacShaKeyFor(bytes);
	}

	
	// JwtAuthenticationFilter의 successfulAuthentication()에서 사용됨 (로그인 성공 시)
	// 토큰 생성
	public String createToken(String username, UserRoleEnum role) {
		Date date = new Date();

		return BEARER_PREFIX +
			Jwts.builder()
				.setSubject(username) // 사용자 식별자값(ID)
				.claim(AUTHORIZATION_KEY, role) // 사용자 권한
				.setExpiration(new Date(date.getTime() + TOKEN_TIME)) // 만료 시간
				.setIssuedAt(date) // 발급일
				.signWith(key, signatureAlgorithm) // 암호화 알고리즘
				.compact(); // 토큰 생성
	}


	// JwtAuthorizationFilter의 doFilterInternal()에서 사용됨
	// HTTP 요청 헤더에서 JWT 추출 메서드
	public String getJwtFromHeader(HttpServletRequest request) {
		String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
		// JWT 토큰이 존재하고 "Bearer " 접두사로 시작할 경우 토큰 반환
		if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
			return bearerToken.substring(7);  // "Bearer " 접두사 제거 후 토큰 반환
		}
		return null;  // 토큰 없을 경우 null 반환
	}


	// JwtAuthorizationFilter의 doFilterInternal()에서 사용됨
	// 토큰 검증
	public boolean validateToken(String token) {
		try {
			// 주어진 토큰의 유효성을 검증하고 유효할 경우 true 반환
			Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
			return true;
		} catch (SecurityException | MalformedJwtException | SignatureException e) {
			log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
		} catch (ExpiredJwtException e) {
			log.error("Expired JWT token, 만료된 JWT token 입니다.");
		} catch (UnsupportedJwtException e) {
			log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
		} catch (IllegalArgumentException e) {
			log.error("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
		}
		return false;  // 유효하지 않은 경우 false 반환
	}


	// JwtAuthorizationFilter의 doFilterInternal()에서 사용됨
	// 토큰에서 사용자 정보 추출 메서드
	public Claims getUserInfoFromToken(String token) {
		// 주어진 토큰에서 클레임 정보(사용자 정보)를 파싱하여 반환
		return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
	}
}

 

Contents

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

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