관리 메뉴

bright jazz music

blog36 : JWT를 이용한 인증 - application.yml 커스텀 설정값 만들기 본문

Projects/blog

blog36 : JWT를 이용한 인증 - application.yml 커스텀 설정값 만들기

bright jazz music 2023. 2. 22. 08:14

지난 번 포스팅에서는 jwt를 사용하여 암호화를 하고 그것을 세션으로 이용하는 내용을 다루었다.

따라서 UserController.java와 AuthResolver.java에 동일한 값의 특정 키를 가져와서 jwt를 암호화, 또는 복호화 하는 데 사용했다.

 

AuthResolver.java에서는 복호화하는 데 사용했다. AuthController.java에서는 로그인을 하는 경우에 암호화 하였다. 세션의 경우 일반적으로는 모든 사용자가 같은 키를 사용하게 된다. 사용자별로 세션을 만들어 줄 때마다 키 값이 달라지는 경우 복호화 할 수 없기 때문이다. 키 값은 서버 어딘가에 위치해야만 한다.

 

지난 번 포스팅에서는 AuthController.java와 AuthResolver.java에 문자열 상수로 저장하였다. 문자열로 저장한 이유는 바이트 값으로 저장하여 사용하기가 어렵기 때문이다.

//AuthController.java
private static final String KEY = "Kz06PMZdP03FQVS3m8Jg9gKSEQjV4/NePMOq1F0GNH4=";

 

 

AuthController에서 문자열을 SecretKey로 사용하려면 바이트로 변환해야 했기 때문에 아래와 같은 과정을 거쳤다.

//AuthController.java
SecretKey key = Keys.hmacShaKeyFor(Base64.getDecoder().decode(KEY));

바이트를 스트링으로 변환하는 경우에는 encode(), 스트링을 바이트로 변환하는 경우에는 decode()를 사용한다.

 

상기한대로 현재는 암호화 된 키값을 부여하는 controller와 키 값을 복호화 하는 resolver에 동일한 키값이 설정값으로 작성돼 있다. 설정값이 분리되어 사용되고 있는 것은 바람직하지 못하다. 따라서 여기서는 AppConfig라는 클래스를 생성하여 그 내부에 설정값을 클래스가 프로퍼티로서 보유하도록 한다. 이 때, 그 설정값들을 클래스 내부에 하드코딩하지 않고 application.yml에 설정하여 매핑되도록 설정해 준다.

 

1. 설정값을 저장할 클래스 생성( AppConfig.java)

 

 2. 클래스에 설정값을 상수로 저장하는 경우

//AppConfig.java
package com.endofma.blog.config;

public class AppConfig {
    public static final String KEY = "Kz06PMZdP03FQVS3m8Jg9gKSEQjV4/NePMOq1F0GNH4=";
}

 

//AuthController.java
SecretKey key = Keys.hmacShaKeyFor(Base64.getDecoder().decode(AppConfig.KEY));

위처럼 사용할 수도 있다. 경우 개발환경(local, dev 등)이 변경되는 경우 조건문을 사용해서 설정값 또한 변경해 줘야 하는데 그것이 쉽지 않다.일반적인 자바파일은 앱에 대한 컨텍스트를 유지하지 못하기 때문이다. 따라서 application.yml 또는 application.properties에 값을 저장하고 바인딩 하는 방식을 사용한다.

 

 

3. application.yml 커스텀 및 코드 수정

#application.yml
#jwt 설정

catnails:
  hello: "world"

 

//AppConfig.java
package com.endofma.blog.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

//@Configuration
@Data
@ConfigurationProperties(prefix = "catnails") //application.yml
public class AppConfig {
    public String hello;
}

 

이런 오류가 발생한다.

 

이는 위 AppConfig.java에서 주석처리한 @Configuration 어노테이션을 사용하거나, 또는 메인메소드가 존재하는 클래스에 아래와 같이 @EnableConfigurationProperties() 를 설정해 주면 된다. 이 방법이 선호되는 듯하다.

//BlogApplication.java

package com.endofma.blog;

import com.endofma.blog.config.AppConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;

@EnableConfigurationProperties(AppConfig.class)
@SpringBootApplication
public class BlogApplication {

    public static void main(String[] args) {
        SpringApplication.run(BlogApplication.class, args);
    }

}

 

 

4. AuthResolver에서 값 확인 해보기

그러러면 

//AuthResolver.java

package com.endofma.blog.config;

import com.endofma.blog.config.data.UserSession;
import com.endofma.blog.domain.Session;
import com.endofma.blog.exception.Unauthorized;
import com.endofma.blog.repository.SessionRepository;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import java.util.Optional;



@Slf4j
@RequiredArgsConstructor
public class AuthResolver implements HandlerMethodArgumentResolver {

    //주입
    private final SessionRepository sessionRepository;
    private final String KEY = "Kz06PMZdP03FQVS3m8Jg9gKSEQjV4/NePMOq1F0GNH4=";

    private final AppConfig appConfig;

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        //컨트롤러에서 사용할 어노테이션이나 DTO가 사용자가 사용하려는 것이 맞는지, 지원하는지 체크한다.
        return parameter.getParameterType().equals(UserSession.class); //UserSession 클래스를 사용하는 것이 맞는지 확인. 맞으면 컨트롤러에 값 할당
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        log.info(">>> {}", appConfig.toString()); //추가

        //실제로 해당 DTO에 값을 세팅해준다.

        String jws = webRequest.getHeader("Authorization"); //헤더의 값으로 확인해준다.
        if (jws == null || jws.equals("")){
            throw new Unauthorized();
        }

        byte[] decodedKey = Base64.decodeBase64(KEY); //스트링 값이었던 것을 바이트 값으로 변환한다. setSigningKey(String)이 폐기되어서.

        try {
            Jws<Claims> claims = Jwts.parserBuilder()
//                    .setSigningKey(KEY) //사인값 추가 deprecated
                    .setSigningKey(decodedKey) //파라미터로 바이트 값이 들어간다.
                    .build()
                    .parseClaimsJws(jws);

            String userId = claims.getBody().getSubject();
            return new UserSession(Long.parseLong(userId));

            //OK, we can trust this JWT
        } catch (JwtException e) {
            throw new Unauthorized();
            //don't trust the JWT!
        }


//        Cookie[] cookies = servletRequest.getCookies(); //배열로 받는다.

//        if (cookies.length == 0) {
//            log.error("쿠키가 없음");
//            throw new Unauthorized();
//        }

//        String accessToken = cookies[0].getValue();

        //db를 조회할 필요 없기 때문에 주석처리
//        Session session = sessionRepository.findByAccessToken(accessToken)
//                .orElseThrow(Unauthorized::new);

//        return new UserSession(1L);
//        return new UserSession(session.getUser().getId()); //컨트롤러로 넘겨준다.


    }
}

 

//WebMvcConfig.java

package com.endofma.blog.config;

import com.endofma.blog.repository.SessionRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {

    //인터셉터 추가
//    @Override
//    public void addInterceptors(InterceptorRegistry registry) {
//        registry.addInterceptor(new AuthInterceptor()) //생성한 인터셉터 등록
////                .addPathPatterns() //인증 적용할 패턴
//                .excludePathPatterns("/error", "/favicon.ico"); //인증 제외할 패턴
//    }

    private final SessionRepository sessionRepository; //blog32: 추가

    private final AppConfig appConfig;//테스트로 추가

    @Override //인터셉터를 추가해줬듯이 아규먼트 리졸버를 추가해준다.
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new AuthResolver(sessionRepository, appConfig));
    }

}

 

값이 들어온 것을 확인할 수 있다. 이렇면 String hello = appConfig.hello; 처럼 값을 사용할 수 있게 된다.

 

 

5. 스트링 말고 다른 자료형 넣어보기

 

5.1 ArrayList

#application.yaml
#jwt 설정
catnails:
  hello:
    - "a"
    - "b"
    - "c"
    - "d"
    - "e"

ArrayList 

//AppConfig.java
package com.endofma.blog.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.util.List;

//@Configuration
@Data
@ConfigurationProperties(prefix = "catnails") //application.yml
public class AppConfig {
    public List<String> hello;
}

appConfig.getHello().get(4) 이렇게 사용할 수 있다.

 

 

5.2 HashMap

#application.yaml
#jwt 설정
catnails:
  hello:
    name: "catnails"
    home: "seoul"
    hobby: "programming"
//AppConfig.java

//@Configuration
@Data
@ConfigurationProperties(prefix = "catnails") //application.yml
public class AppConfig {
    public Map<String, String> hello;
}

appConfig.hello.get("hobby"); 이런 식으로 꺼내어 사용할 수 있다.

 

 

5.3 Class

 

#application.yaml
#jwt 설정
catnails:
  hello:
    name: "catnails"
    home: "seoul"
    hobby: "programming"
    age: 36

 

이너클래스 사용

//AppConfig.java
package com.endofma.blog.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.util.List;
import java.util.Map;

//AppConfig.java

//@Configuration
@Data
@ConfigurationProperties(prefix = "catnails") //application.yml
public class AppConfig {

    //Inner Class
    public Hello hello;

    @Data
    public static class Hello{
        public String name;
        public String home;
        public String hobby;
        public Long age;
    }
}

 

Long age = appCongif.hello.age; 이렇게도 사용할 수 있다.

 

 

6. application.yml에서 발생하는 경고 제거하기

 

이건 우리 커스텀이기 때문에 인텔리제이는 이게 무엇을 참조하는지 알지 못한다.

 

spring boot configuration processor를 사용하여 해결한다. 이를 사용하면 사용자 정의 메타데이터를 생성할 수 있다.

 

 

Configuration Metadata

Configuration metadata files are located inside jars under META-INF/spring-configuration-metadata.json. They use a JSON format with items categorized under either “groups” or “properties” and additional values hints categorized under "hints", as sh

docs.spring.io

dependencies {
    annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
}

 

build.gradle에 저 내용을 넣고, 빌드를 해준다, 서버 재시작도 방법이다.

 

 

 

spring-configuration-metadata.json이 생성됨.

 

Comments