코드 그라데이션

JWT 예제 구현 (4) JWT 기본 코드 구현 + Security 설정 추가 본문

Spring/Security

JWT 예제 구현 (4) JWT 기본 코드 구현 + Security 설정 추가

완벽한 장면 2023. 12. 11. 20:22

build.gradle에 jwt 관련 코드를 추가

plugins {
    id 'java'
    id 'org.springframework.boot' version '2.4.1' // Spring Boot 버전을 2.4.1로 변경
    id 'io.spring.dependency-management' version '1.0.10.RELEASE' // Spring Dependency Management 버전 변경
}

group = 'inflearn'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-web'

    compileOnly 'org.projectlombok:lombok'

    runtimeOnly 'com.h2database:h2:1.4.200'

    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'

	// 추가!!
    compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'
}

tasks.named('test') {
    useJUnitPlatform()
}

 

 

application.yml 에 jwt 관련 내용 추가

spring:
  h2:
    console:
      enabled: true

  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
    username: sa
    password:

  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
    hibernate:
      ddl-auto: create-drop
    properties:
      hibernate:
        format_sql: true
        show_sql: true
        
# 추가!!!        
jwt:
  header: Authorization
  #HS512 알고리즘을 사용할 것이기 때문에 512bit, 즉 64byte 이상의 secret key를 사용해야 한다.
  #echo 'silvernine-tech-spring-boot-jwt-tutorial-secret-silvernine-tech-spring-boot-jwt-tutorial-secret'|base64
  secret: c2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQtc2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQK
  token-validity-in-seconds: 86400

logging:
  level:
    inflearn.freejwt: DEBUG

 


jwt 디렉토리 생성

TokenProvider.java

package inflearn.freejwt.jwt;

import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
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.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.stream.Collectors;

@Component
public class TokenProvider implements InitializingBean {

    private final Logger logger = LoggerFactory.getLogger(TokenProvider.class);

    private static final String AUTHORITIES_KEY = "auth";

    private final String secret;
    private final long tokenValidityInMilliseconds;

    private Key key;


    public TokenProvider(
            @Value("${jwt.secret}") String secret,
            @Value("${jwt.token-validity-in-seconds}") long tokenValidityInSeconds) {
        this.secret = secret;
        this.tokenValidityInMilliseconds = tokenValidityInSeconds * 1000;
    }

    @Override
    public void afterPropertiesSet() {
        // JWT 서명을 위한 Key 생성
        byte[] keyBytes = Decoders.BASE64.decode(secret);
        this.key = Keys.hmacShaKeyFor(keyBytes);
    }
    /**
     * InitializingBean을 implements 해서 afterPropertiesSet을 오버라이드 한 이유는
     * 빈이 생성 되고 주입을 받은 후에 secret 값을 Base64 Decode 해서 key 변수에 할당하기 위함.
     */


    /**
     *
     * @param authentication
     * @return
     *
     * Authentication 객체의 권한정보를 이용해서 토큰을 생성하는 createToken 메서드 추가
     */
    public String createToken(Authentication authentication) {
        // 사용자 권한을 JWT에 추가
        String authorities = authentication.getAuthorities()
                .stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(","));

        long now = (new Date()).getTime();
        Date validity = new Date(now + this.tokenValidityInMilliseconds); // 토큰 만료 시간

        // JWT 토큰 생성
        return Jwts.builder()
                .setSubject(authentication.getName())
                .claim(AUTHORITIES_KEY, authorities)
                .signWith(key, SignatureAlgorithm.HS512)
                .setExpiration(validity)
                .compact();
    }


    /**
     *
     * @param token
     * @return
     *
     * Token에 담겨있는 정보를 이용해 Authentication 객체를 리턴하는 메서드 생성
     */
    public Authentication getAuthentication(String token) {
        // JWT 토큰 파싱
        // 토큰을 파라미터로 받아서 클레임을 만들고
        Claims claims = Jwts
                .parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();


        // 사용자 권한 파싱 및 Principal 생성
        Collection<? extends GrantedAuthority> authorities = // 클레임에서 권한정보들을 빼내서
                Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toList());

        // 권한정보들을 이용해서 User 객체를 만들어 주고,
        User principal = new User(claims.getSubject(), "", authorities);

        // 유저객체와 토큰, 권한정보를 이용해서 최종적으로
        // Authentication 객체 반환
        return new UsernamePasswordAuthenticationToken(principal, token, authorities);
    }

    /**
     *
     * @param token
     * @return
     * 토큰을 파라미터로 받아서 토큰의 유효성 검사를 수행하는 validateToken 메서드 추가
     */
    public boolean validateToken(String token) {
        try {
            // 토큰을 파싱해보고
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;

            // 나오는 Exception들을 캐치하고
        } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
            logger.info("잘못된 JWT 서명입니다.");
        } catch (ExpiredJwtException e) {
            logger.info("만료된 JWT 토큰입니다.");
        } catch (UnsupportedJwtException e) {
            logger.info("지원되지 않는 JWT 토큰입니다.");
        } catch (IllegalArgumentException e) {
            logger.info("JWT 토큰이 잘못되었습니다.");
        }
        return false;
    }
}

 

 

JwtFilter.java

package inflearn.freejwt.jwt;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

public class JwtFilter extends GenericFilterBean {

    private static final Logger logger = LoggerFactory.getLogger(JwtFilter.class);

    // Authorization 헤더의 이름
    public static final String AUTHORIZATION_HEADER = "Authorization";

    private TokenProvider tokenProvider;

    public JwtFilter(TokenProvider tokenProvider) {
        this.tokenProvider = tokenProvider;
    }

    /**
     *
     * @param servletRequest  The request to process
     * @param servletResponse The response associated with the request
     * @param filterChain    Provides access to the next filter in the chain for this
     *                 filter to pass the request and response to for further
     *                 processing
     *
     * @throws IOException
     * @throws ServletException
     *
     * 실제 필터링 로직은 이 메서드 내부에 작성
     * 이것은 토큰의 인증정보를 SecurityContext에 저장하는 역할을 수행
     */
    @Override
    public void doFilter(ServletRequest servletRequest,
                         ServletResponse servletResponse,
                         FilterChain filterChain) throws IOException, ServletException {

        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        String jwt = resolveToken(httpServletRequest);
        String requestURI = httpServletRequest.getRequestURI();

        // 추출한 JWT 토큰이 유효한지 검증
        if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
            // JWT 토큰을 사용하여 사용자 인증 정보 생성
            Authentication authentication = tokenProvider.getAuthentication(jwt);
            // 정상 토큰이면 Spring Security의 SecurityContextHolder에 인증 정보 저장
            SecurityContextHolder.getContext().setAuthentication(authentication);

            // 로깅
            logger.debug("Security Context에 '{}' 인증 정보를 저장했습니다, uri: {}", authentication.getName(), requestURI);
        } else {
            // 유효한 JWT 토큰이 없을 경우 로깅
            logger.debug("유효한 JWT 토큰이 없습니다, uri: {}", requestURI);
        }
        // 다음 필터 또는 요청 처리로 전달
        filterChain.doFilter(servletRequest, servletResponse);
    }


    /**
     *
     * @param request
     * @return
     * Request Header에서 토큰 정보를 꺼내오기 위한 resolveToken 메서드
     */
    // Authorization 헤더에서 JWT 토큰 추출
    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }

}

 

 

JwtSecurityConfig

/**
 * TokenProvider, JwtFilter를 SecurityConfig에 적용할 때 사용할 클래스
 */
public class JwtSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    private TokenProvider tokenProvider;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        JwtFilter customFilter = new JwtFilter(tokenProvider);
        http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

 

 

 

JwtAuthenticationEntryPoint

package inflearn.freejwt.jwt;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

// 유효한 자격증명을 제공하지 않고 접근하려고 할 때 401 Unauthorized 에러를 리턴할 클래스
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException, ServletException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED); // 401에러
    }
}

 

 

JwtAccessDeniedHandler

package inflearn.freejwt.jwt;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

// 필요한 권한이 존재하지 않는 경우에 403 에러 리턴하기 위함
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.sendError(HttpServletResponse.SC_FORBIDDEN);
    }
}

 

 

CorsConfig

package inflearn.freejwt.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;


@Configuration
public class CorsConfig {

    // CORS 필터 빈을 정의하는 메서드
    @Bean
    public CorsFilter corsFilter() {
        // URL 기반의 CORS 구성을 관리하는 객체 생성
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        
        // CORS 구성 설정 객체 생성
        CorsConfiguration config = new CorsConfiguration();
        
        // 요청에서 자격 증명(자격 증명 쿠키, 인증 등)을 허용
        config.setAllowCredentials(true);
        
        // 모든 출처(Origin)를 허용
        config.addAllowedOrigin("*");
        
        // 모든 HTTP 헤더를 허용
        config.addAllowedHeader("*");
        
        // 모든 HTTP 메서드(GET, POST, PUT, DELETE 등)를 허용
        config.addAllowedMethod("*");

        // "/api/**" 패턴에 해당하는 엔드포인트에 대해 CORS 구성을 등록
        source.registerCorsConfiguration("/api/**", config);
        
        // CORS 필터를 생성하고 반환
        return new CorsFilter(source);
    }
}
728x90
Comments