Spring Boot 권한 사용 안하는 JWT+Security

Spring boot에서 보통 권한을 사용해서 JWT를 사용할거다

 

현재 나는 권한이 필요 없는 JWT를 만들려고 한다.

 

Spring boot 3.0.5 버전이다

 


build.gradle

implementation 'org.springframework.boot:spring-boot-starter-security'
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'

application.properties

jwt.header=Authorization
jwt.access-secret={시크릿키}
jwt.refresh-secret={리프레쉬 토큰 시크릿키}
jwt.access-token-validity-in-seconds={토큰 유효시간 (초)}
jwt.refresh-token-validity-in-seconds={리프레시토큰 유효시간(초)}

JwtTokenProvider.class

import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SecurityException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
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.User;
import org.springframework.stereotype.Component;

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

@Component
public class jwtTokenProvider implements InitializingBean {

    private final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class);
    private final String accessSecret;
    private final String refreshSecret;
    private final long accessTokenValidity;
    private final long refreshTokenValidity;
    private Key accessSecretKey;
    private Key refreshSecretKey;

    public JwtTokenProvider(
            @Value("${jwt.access-secret}") String accessSecret,
            @Value("${jwt.refresh-secret}") String refreshSecret,
            @Value("${jwt.access-token-validity-in-seconds}") long accessTokenValidityInSeconds,
            @Value("${jwt.refresh-token-validity-in-seconds}") long refreshTokenValidityInSeconds) {
        this.accessSecret = accessSecret;
        this.refreshSecret = refreshSecret;
        this.accessTokenValidity = accessTokenValidityInSeconds * 1000;
        this.refreshTokenValidity = refreshTokenValidityInSeconds * 1000;
    }
    // application.properties 파일 안에 있는 값을 가져옴

    @Override
    public void afterPropertiesSet() throws Exception {
        byte[] keyBytes = Decoders.BASE64.decode(accessSecret);
        this.accessSecretKey = Keys.hmacShaKeyFor(keyBytes);
        keyBytes = Decoders.BASE64.decode(refreshSecret);
        this.refreshSecretKey = Keys.hmacShaKeyFor(keyBytes);
    }
    // JWT 토큰 생성 및 검증에 사용할 secret, refreshSecretkey를 생성하는 로직 
	
    public String createAccessToken(String username) {
        return buildJwtToken(username, accessSecretKey, accessTokenValidity);
    }

    public String createRefreshToken(String username) {
        return buildJwtToken(username, refreshSecretKey, refreshTokenValidity);
    }

    private String buildJwtToken(String username, Key secretKey, long validityPeriod) {
        long now = (new Date()).getTime();
        Date validity = new Date(now + validityPeriod);

        return Jwts.builder()
                .setSubject(username) // username 넣고
                .signWith(secretKey, SignatureAlgorithm.HS512) // HS512알고리즘으로 생성
                .setExpiration(validity) // 만료 시간 넣기
                .compact();
    }

    public Authentication getAuthentication(String token) {
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(secretKey) // 서명 검증에 사용할 키
                .build()
                .parseClaimsJws(token)
                .getBody();
		// jwt토큰 문자열을 파싱해서 Claims 객체 얻음
        // Claims 객체는 jwt 토큰의 페이로드를 담고 있음
        
        User principal = new User(claims.getSubject(), "N/A", new ArrayList<>());
		//User 클래스는 spring Security에서 사용자 정보를 담는 클래스
        //claims 객체에서 주체를 가져오고 비밀번호, 권한을 사용 안하기에 비워둠
        
        return new UsernamePasswordAuthenticationToken(principal, token, new ArrayList<>());
        // UsernamePasswordAuthenticationToken 클래스는 스프링 시큐리티에서 사용자의 인증 정보를 담음
    }
    
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token);
            return true;
        } catch (ExpiredJwtException e) {
            logger.info("Expired JWT token.");
            throw new ExpiredJwtTokenException(CommonResponseStatus.EXPIRED_JWT);
        } catch (JwtException | IllegalArgumentException e) {
            logger.info("Invalid JWT token : " + e.getMessage());
            throw new UnsuitableJwtException();
        }
    }
    
    public String extractIDs(String token) {
        try {
            return Jwts.parserBuilder()
                    .setSigningKey(refreshSecretKey)
                    .build()
                    .parseClaimsJws(token)
                    .getBody()
                    .getSubject();
        } catch (ExpiredJwtException e) {
            throw new InvalidJwtTokenException(CommonResponseStatus.EXPIRED_JWT);
        } catch (JwtException e) {
            throw new InvalidJwtTokenException(CommonResponseStatus.UNSUITABLE_JWT);
        }
    }
    // refreshToken 검증할때 사용
}

JwtFilter.class

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.GenericFilterBean;
import project.response.ErrorCode;
import project.response.ErrorResponse;

import java.io.IOException;
import java.util.Set;

@RequiredArgsConstructor
public class JwtFilter extends GenericFilterBean {

    private final JwtTokenProvider jwtTokenProvider;
    private final ObjectMapper objectMapper = new ObjectMapper();
    private final Set<String> securedPaths = Set.of({JWT 검사할 경로 URL});

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;

        String path = httpServletRequest.getRequestURI(); // 어떤 경로로 접속했는지 가져옴

        if (authenticationPaths.stream().anyMatch(path::startsWith)) { // 경로가 securedPaths로 시작하는지 검사
            try {
                String jwt = resolveToken(httpServletRequest.getHeader(AUTHORIZATION_HEADER)); // 헤더 값을 가져옴
                jwtTokenProvider.validateToken(jwt); // JWT 검사
                Authentication authentication = jwtTokenProvider.getAuthentication(jwt); //
                SecurityContextHolder.getContext().setAuthentication(authentication);
                chain.doFilter(request, response);
            } catch (InvalidJwtException e) { // 올바르지 않은 경우
                sendResponse(httpServletResponse, JsonResponseStatus.INVALID_JWT);
            } catch (ExpiredJwtTokenException e) { // 시간이 만료된 경우
                sendResponse(httpServletResponse, JsonResponseStatus.ACCESS_TOKEN_EXPIRED);
            } catch (MissingJwtException e) { // JWT가 없는 경우
                sendResponse(httpServletResponse, JsonResponseStatus.MISSING_JWT);
            }
        } else {
            chain.doFilter(request, response);
        }
    }
	
    // 에러 전송
    private void sendResponse(HttpServletResponse response, JsonResponseStatus status) throws IOException {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");
        response.getWriter().write(convertObjectToJson(new JsonResponse<>(status)));
    }

    private String convertObjectToJson(Object object) throws IOException {
        if (object == null)
            return null;
        return objectMapper.writeValueAsString(object);
    }

    private String getBearerToken(String token) {
        if (StringUtils.hasText(token) && token.startsWith("Bearer "))
            return token.substring(7);
        throw new MissingJwtException(JsonResponseStatus.MISSING_JWT);
    }
}

JwtAuthenticationEntryPoint.class

package project.neighborhoodcommunity.jwt;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

import java.io.IOException;

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException, ServletException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
    }
}
// 유효하지 않은 jwt를 가지고 접근할때 401에러

CustomUserDetailService.class

package project.neighborhoodcommunity.jwt;

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;

@Component
public class CustomUserDetailsService implements AuthenticationProvider {
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        return new UsernamePasswordAuthenticationToken(authentication.getName(), authentication.getCredentials().toString(), null);
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

JwtSecurityConfig.class

package project.neighborhoodcommunity.jwt;

import lombok.RequiredArgsConstructor;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@AllArgsConstructor
public class JwtSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    private JwtTokenProvider jwtTokenProvider;

    @Override
    public void configure(HttpSecurity builder) throws Exception {
        builder.addFilterBefore(new JwtFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
    }
    // HttpSecurity 객체를 통해 보안 설정을 구성하는 메서드
    // Http 요청이 들어올 때, Username~~ 실행전에 JwtFilter를 실행하도록 설정
}

SecurityConfig.class

package project.neighborhoodcommunity.config;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.filter.CorsFilter;
import project.neighborhoodcommunity.jwt.JwtAuthenticationEntryPoint;
import project.neighborhoodcommunity.jwt.JwtSecurityConfig;
import project.neighborhoodcommunity.jwt.JwtTokenProvider;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final CorsFilter corsFilter;
    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    private final JwtTokenProvider jwtTokenProvider;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
                //CORS 필터 먼저 거치고 Username~~ 필터 순으로
                .exceptionHandling()
                .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                // 인증 실패시 jwtAuthen~~를 통해 예외처리 설정
                .and()

                .headers()
                .frameOptions()
                .sameOrigin()
                .and()

                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // 세션 사용하지 않음
                
//                .requiresChannel()
//                .anyRequest().requiresSecure()
//                .and()

                .apply(new JwtSecurityConfig(jwtTokenProvider));
                // JWT를 이용한 인증 설정
                
        return http.build();
    }
}

 

댓글

Designed by JB FACTORY