Configuration 구현

AuthenticationConfig

스프링 시큐리티의 기본적인 웹 보안 활성화하고, 메서드를 재정의하여 인증과 권한을 설정할 수 있는 설정파일

package com.jh.jwt.configuration;

import com.jh.jwt.exception.JwtExceptionFilter;
import com.jh.jwt.utils.TokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class AuthenticationConfig {

    private final TokenProvider tokenProvider;

    // 비밀번호 인코딩에 BcryptPasswordEncoder 사용
    @Bean
    public BCryptPasswordEncoder encodePassword() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
                // JWT를 사용하기 때문에 httpBasic, csrf를 disable
                .httpBasic().disable()
                .csrf().disable()
                .cors().and()
                .authorizeRequests()
                // API 경로 설정
                .antMatchers("/api/v1/users/login").permitAll() // 권한없이 사용할 수 있는 api
                .antMatchers("/api/v1/users/sign").permitAll() // 권한없이 사용할 수 있는 api
                .antMatchers(HttpMethod.POST, "/api/v1/reviews").authenticated() // JWT인증이 필요한 api

                // 세션도 사용하지 않기때문에 STATELESS로 설정
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                .and()
                // 필터를 거치기 이전에 JwtFilter를 먼저 거치도록 설정
                .addFilterBefore(new JwtFilter(tokenProvider), UsernamePasswordAuthenticationFilter.class)
                // Jwt 필터를 거치기 전에 Exception 관리를 위해 JwtException Filter를 등록
                .addFilterBefore(new JwtExceptionFilter(), JwtFilter.class)
                .build();
    }
}
  • @EnableWebSecurity는 스프링 스큐리티의 기본적인 웹 보안을 활성화 합니다.
  • 추가로 antMathcer.hasRole("ADMIN")을 이용하면 ADMIN권한을 가진사람만 api를 사용할 수 있도록 설정할 수 있다.

TokenProvider

토큰을 생성하고 검증하는 역할과 토큰의 정보를 꺼내는 역할을 수행

package com.jh.jwt.utils;

import com.jh.jwt.domain.CustomUserDetails;
import com.jh.jwt.exception.NotLoggedInException;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureException;
import org.springframework.http.HttpHeaders;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.security.Key;
import java.util.*;
import java.util.stream.Collectors;

public class TokenProvider {

    protected final String secret;
    protected final String refreshSecret;

    protected final long accessTokenExpired;
    protected final long refreshTokenExpired;

    private final RedisUtil redisUtil;

    protected Key accessKey;
    protected Key refreshKey;
    public TokenProvider(String secret, Long expiredMs, String refreshSecret, Long refreshTokenExpired, RedisUtil redisUtil){
        this.secret = secret;
        this.accessTokenExpired = expiredMs;
        this.refreshSecret = refreshSecret;
        this.refreshTokenExpired = refreshTokenExpired;

        byte[] accessKeyBytes = Base64.getDecoder().decode(secret);
        this.accessKey = Keys.hmacShaKeyFor(accessKeyBytes);

        byte[] refreshKeyBytes = Base64.getDecoder().decode(refreshSecret);
        this.refreshKey = Keys.hmacShaKeyFor(refreshKeyBytes);

        this.redisUtil = redisUtil;
    }

    // Access 토큰에서 사용자명 반환
    public String getUsername(String token) {
        return Jwts.parserBuilder().setSigningKey(accessKey).build().parseClaimsJws(token).getBody().get("username", String.class);
    }

    // Refresh토큰에서 사용자명 반환
    public String getRefreshUsername(String token) {
        return Jwts.parserBuilder().setSigningKey(refreshKey).build().parseClaimsJws(token).getBody().get("username", String.class);
    }

    // 토큰에서 권한정보 반환
    public Collection<? extends GrantedAuthority> getAuthority(String token) {
        Claims claims = Jwts.parserBuilder().setSigningKey(accessKey).build().parseClaimsJws(token).getBody();
        return Arrays.stream(claims.get("auth").toString().split(","))
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }

    // Access 토큰 생성
    public String createAccessJwt(CustomUserDetails principal) {
        Claims claims = Jwts.claims();
        claims.put("username", principal.getUsername());
        claims.put("auth", principal.getAuthorities());

        return  Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + accessTokenExpired))
                .signWith(accessKey, SignatureAlgorithm.HS512)
                .compact();
    }

    // Refresh 토큰 생성
    public String createRefreshJwt(CustomUserDetails principal) {
        Claims claims = Jwts.claims();
        claims.put("username", principal.getUsername());

        return  Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + refreshTokenExpired))
                .signWith(refreshKey, SignatureAlgorithm.HS512)
                .compact();
    }

    // 헤더 Authorization에서 토큰정보 추출
    public String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader(HttpHeaders.AUTHORIZATION);
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
    // Access토큰 토큰 유효성 검사
    public boolean validateToken(String token) {
        try {
            Claims claims = Jwts.parserBuilder().setSigningKey(accessKey).build().parseClaimsJws(token).getBody();
            if (redisUtil.hasKeyBlackList(token)) {
                throw new SignatureException("로그인을 해주세요.");
            }
            return true;
        } catch(MalformedJwtException | SignatureException e) {
            throw new MalformedJwtException("잘못된 JWT 서명입니다.");
        } catch(ExpiredJwtException e) {
            throw new ExpiredJwtException(e.getHeader(), e.getClaims(), "만료된 JWT 토큰입니다.");
        } catch(UnsupportedJwtException e) {
            throw new UnsupportedJwtException("지원하지 않는 JWT 토큰입니다.");
        } catch(IllegalArgumentException e) {
            throw new IllegalArgumentException("JWT 토큰이 잘못되었습니다.");
        }
    }

    // Refresh 토큰 유효성 검사
    public boolean validateRefreshToken(String token) {
        try {
            Claims claims = Jwts.parserBuilder().setSigningKey(refreshKey).build().parseClaimsJws(token).getBody();
            return true;
        } catch(MalformedJwtException | SignatureException  |  ExpiredJwtException |  UnsupportedJwtException | IllegalArgumentException e) {
            System.out.println("e = " + e);
            throw new NotLoggedInException("로그인을 해주세요.");
        }
    }

}

JwtFilter

실직적인 인증절차를 진행하는 설정파일

package com.jh.jwt.configuration;

import com.jh.jwt.utils.TokenProvider;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

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

// 토큰 인증 계층
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {
    protected final Logger logger = LoggerFactory.getLogger(JwtFilter.class);
    private final TokenProvider tokenProvider;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            // authorization이 null이면 SecurityFilterChain을 거치도록한다.
            final String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
            if(authorization == null){
                filterChain.doFilter(request, response);
                return;
            }
            String token = tokenProvider.resolveToken(request);
            if (tokenProvider.validateToken(token)){

                // Username, Authorities Token에서 꺼내기
                String username = tokenProvider.getUsername(token);
                Collection<? extends GrantedAuthority>  authority = tokenProvider.getAuthority(token);
                logger.info("username: {}", username);
                logger.info("authority: {}", authority);

                // 권한 부여
                UsernamePasswordAuthenticationToken authenticationToken =
                        new UsernamePasswordAuthenticationToken(username, null, authority);

                // 해당 authenticationToken을 스프링 시큐리티 컨텍스트에 저장한다.
                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                filterChain.doFilter(request, response);
        }
    }
}
  • OncePerRequestFilter는 Request한번에 한번만 실행되는 Filter입니다.
  • doFilterInternal에서 토큰의 유효성을 검증하고, 토큰에 들어있는 username과 authority를 가져와 authentication을 생성하고 스프링 시큐리티 컨텍스트에 저장하는 역할을 합니다.

JWTConfig

TokenProvider에 의존성을 주입하고 빈을 생성하는 역할을 하는 설정파일

package com.jh.jwt.configuration;

import com.jh.jwt.utils.RedisUtil;
import com.jh.jwt.utils.TokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

// TokenProvider에 의존성을 주입하고 빈을 생성하는 설정 파일
@Configuration
@RequiredArgsConstructor
public class JwtConfig {

    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.access_token_expired}")
    private Long accessTokenExpired;

    @Value("${jwt.refresh_secret}")
    private String refreshSecret;

    @Value("${jwt.refresh_token_expired}")
    private Long refreshTokenExpired;

    private final RedisUtil redisUtil;

    @Bean(name = "tokenProvider")
    public TokenProvider tokenProvider() {
        return new TokenProvider(secret, accessTokenExpired, refreshSecret, refreshTokenExpired, redisUtil);
    }
}

참고

멋사 SpringSecurity 인증인가 - 04 Token유효한지 만료되었는지 검사하기
https://velog.io/@suhongkim98/Spring-Security-JWT로 인증인가구현하기

+ Recent posts