spring security oauth2 jwt 설정하는 법에 대해 알아보자.
jwt란 JSON Web Token의 약자로 일반 oauth2 토큰을 기반으로 하는 것과 비슷하다. 인터넷에 잘 나와 있으니 참고하길 바란다.

일단 spring boot기반으로 작성할 예정이다.

<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-jwt</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

아주 기본적인것만 추가 하였다. security jwt와 oauth2를 추가하면 된다.
간단한 샘플코드이므로 Resource 서버와 Authorization 서버는 한곳에 넣었다. 분리하고 싶다면 간단하게 EnableResourceServer와 EnableAuthorizationServer 어노테션으로 분리 해주면 될 것이다.

@Configuration
public class OAuth2ServerConfig {

  @Configuration
  @EnableResourceServer
  protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    @Value("${resource.id:spring-boot-application}")
    private String resourceId;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
      resources.resourceId(resourceId);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {

      http.authorizeRequests()
        .antMatchers("/simple/**").hasRole("USER");
    }
  }


  @Configuration
  @RequiredArgsConstructor
  @EnableAuthorizationServer
  public static class JwtOAuth2AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

    private final AuthenticationManager authenticationManager;

    @Value("${resource.id:spring-boot-application}")
    private String resourceId;

    @Value("${access_token.validity_period:3600}")
    private int accessTokenValiditySeconds = 3600;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints)
      throws Exception {
      endpoints.accessTokenConverter(jwtAccessTokenConverter())
        .authenticationManager(this.authenticationManager);
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
      clients.inMemory()
        .withClient("bar")
        .authorizedGrantTypes("password")
        .authorities("ROLE_USER")
        .scopes("read", "write")
        .resourceIds(resourceId)
        .accessTokenValiditySeconds(accessTokenValiditySeconds)
        .secret("foo");
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
      JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
      KeyPair keyPair = new KeyStoreKeyFactory(
        new ClassPathResource("server.jks"), "qweqwe".toCharArray())
        .getKeyPair("hello", "zaqwsx".toCharArray());
      converter.setKeyPair(keyPair);
      return converter;
    }
  }
}

oauth2를 알면 나머지는 다 알겠지만 jwtAccessTokenConverter만 새롭게 느껴질 것이다. jwt토큰 방식의 좋은점은 db가 필요 없다는것이다. 서명을 통한 인증만 하면 된다.
기본적으로 JwtAccessTokenConverter 만 등록해도 상관은 없다.

@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
  return new JwtAccessTokenConverter();
}

하지만 이 예제에서는 KeyPair도 등록하는 방법을 해보자. keytool을 통해 server.jks파일을 생성하자.

keytool -genkeypair -alias hello -keyalg RSA -dname "CN=Web Server,OU=Unit,O=Organization,L=City,S=State,C=US" -keypass zaqwsx -keystore server.jks -storepass qweqwe

여기서 중요한것은 alias의 hello와 keypass의 zaqwsx, storepass의 qweqwe 이것들이다. 필자도 인터넷에서 찾아서 해서 뭘뜻하는지는 자세히 모른다.ㅎㅎㅎ 자신의 맞게 패스워드들을 만들어서 생성하면 된다.
엔터를 치면 server.jks가 생성된다. 그 생성된 파일을 classpath에 두자.
그리고 테스트를 위해 두개의 controller를 만들자.

@RestController
public class AnonymousController {

  @GetMapping("/anonymous")
  public String simple(){
    return "Anonymous";
  }
}

@RestController
public class SimpleController {

  @GetMapping("/simple")
  public String simple(){
    return "hi spring boot";
  }
}

anonymous는 인증을 하지 않아도 되는 경우이고 simple인증을 해야 되는 경우이다.

curl http://localhost:8080/anonymous
Anonymous

curl http://localhost:8080/simple
{"error":"unauthorized","error_description":"Full authentication is required to access this resource"}

anonymous인 경우에는 정상적으로 값이 출력 되지만 simple인 경우에는 인증이 되지 않았다고 출력 되었다.
그럼 이제 인증을 해보자. 참고로 테스트를 위해 yml파일에 다음과 같이 넣었다.

security:
  user:
    name: admin
    password: test
curl -u bar:foo http://localhost:8080/oauth/token -d "grant_type=password&username=admin&password=test"

이와 같이 password타입으로 아이디와 패스워드를 넘긴후 인증 처리를 하였다. 물론 지금 예제는 password 타입밖에 설정이 되어 있지 않다. 결과를 보면 다음과 같다.

{"access_token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsic3ByaW5nLWJvb3QtYXBwbGljYXRpb24iXSwidXNlcl9uYW1lIjoiYWRtaW4iLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiZXhwIjoxNDY3NjE1ODA2LCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiMzU0NDkyYzctMjg5OC00NjE1LWE0YTEtMWQ1N2I0ODUzYWE4IiwiY2xpZW50X2lkIjoiYmFyIn0.dySxGmpgHZsTegkqi39AGaogEK0Gzvg9-gNqxZDG3MaHWpjbU5-5gbYfPm4olANOeu-y5hyOeetuoWtAkMsMKcTqYEqJweKaPfbVo_5m6W6MaMv2VqUUNsOXZFlSR6-RYsixE8dC92pu5YXQ--edog8pJ43skPvk7XD63967dVNJLkp0nDGWwO-Pmv_6t6QzG0eQA_la4o1xl88Cv8gkweH7SNYjnm3UKMiTCS9fxO_wHFnEuQ4PthgMrAOk0myWVmnYOIxR-_FzxkhN_MCGdhvy_PqDhbVRTrEM6MXWd-SEWX3coqZrrgllr1MYQWupKjz8-M8DFKFHmyb-hYb4vA","token_type":"bearer","expires_in":3599,"scope":"read write","jti":"354492c7-2898-4615-a4a1-1d57b4853aa8"}

access_token을 발급 받았다. https://jwt.io/ 사이트에 가면 json으로 확인 할 수 있다.
이제 다시 access_token과 함께 simple에 접속해보자.

curl -H "authorization: bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsic3ByaW5nLWJvb3QtYXBwbGljYXRpb24iXSwidXNlcl9uYW1lIjoiYWRtaW4iLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiZXhwIjoxNDY3NjE1ODA2LCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiMzU0NDkyYzctMjg5OC00NjE1LWE0YTEtMWQ1N2I0ODUzYWE4IiwiY2xpZW50X2lkIjoiYmFyIn0.dySxGmpgHZsTegkqi39AGaogEK0Gzvg9-gNqxZDG3MaHWpjbU5-5gbYfPm4olANOeu-y5hyOeetuoWtAkMsMKcTqYEqJweKaPfbVo_5m6W6MaMv2VqUUNsOXZFlSR6-RYsixE8dC92pu5YXQ--edog8pJ43skPvk7XD63967dVNJLkp0nDGWwO-Pmv_6t6QzG0eQA_la4o1xl88Cv8gkweH7SNYjnm3UKMiTCS9fxO_wHFnEuQ4PthgMrAOk0myWVmnYOIxR-_FzxkhN_MCGdhvy_PqDhbVRTrEM6MXWd-SEWX3coqZrrgllr1MYQWupKjz8-M8DFKFHmyb-hYb4vA" http://localhost:8080/simple

그럼 성공적으로 hi spring boot가 출력 된 것을 볼수 있다. 엉뚱한 값을 다시 넣어서 테스트 해보자.

curl -H "authorization: bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsic3ByaW5nLWJvb3QtYXBwbGljYXRpb24iXSwidXNlcl9uYW1lIjoiYWRtaW4iLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiZXhwIjoxNDY3NjE1ODA2LCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiMzU0NDkyYzctMjg5OC00NjE1LWE0YTEtMWQ1N2I0ODUzYWE4IiwiY2xpZW50X2lkIjoiYmFyIn0.dySxGmpgHZsTegkqi39AGaogEK0Gzvg9-gNqxZDG3MaHWpjbU5-5gbYfPm4olANOeu-y5hyOeetuoWtAkMsMKcTqYEqJweKaPfbVo_5m6W6MaMv2VqUUNsOXZFlSR6-RYsixE8dC92pu5YXQ--edog8pJ43skPvk7XD63967dVNJLkp0nDGWwO-Pmv_6t6QzG0eQA_la4o1xl88Cv8gkweH7SNYjnm3UKMiTCS9fxO_wHFnEuQ4PthgMrAOk0myWVmnYOIxR-_FzxkhN_MCGdhvy_PqDhbVRTrEM6MXWd-SEWX3coqZrrgllr1MYQWupKjz8-M8DFKFHmyb-hYb4vA11" http://localhost:8080/simple

그럼 잘못된 토근이라고 리턴해준다.

{"error":"invalid_token","error_description":"Cannot convert access token to JSON"}

우리는 이렇게 spring security jwt에 대해 알아봤다.

참고:
keytool -export -keystore server.jks -alias hello -file example.cer
키 저장소 비밀번호 입력: qweqwe
인증서가 <example.cer> 파일에 저장되었습니다.

openssl x509 -inform der -in example.cer -pubkey -noout
—–BEGIN PUBLIC KEY—–
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgdy5rIIFN3gKeBb+pDkR
QabaDo+Rk/bz8NsiAn+OR1OavsEe5SqgbJw+KhpaFubMcfWbC5b4AkLqQh9xzIRE
cRAKlDhOPoaoTaFeq2ocpCjOipq7s6UuVAqmx7WOj5PbcasyG6rMeTEr0rZrwFSw
S6NQCoTM0n5rpxXL9S2qTTIUUYY1fjJ/y1Hocmpg9opIvU8xc0YXnoHoucmogpOE
4dcwIfXMSwXPkiFAcZnApcXjH4VBYsrhho+acqZDUSzCr8OttzHifHCj2+tFnKYI
ggddxP7tmwlkU7gPEmO9okXfg5mzlnif6FUepmRTsT1CrcBqGhic6TZCOEVcZGS+
QQIDAQAB
—–END PUBLIC KEY—–

public key 추출하는 방법이다.

security:
  oauth2:
    resource:
      jwt:
        key-value: |
            -----BEGIN PUBLIC KEY-----
            MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgdy5rIIFN3gKeBb+pDkR
            QabaDo+Rk/bz8NsiAn+OR1OavsEe5SqgbJw+KhpaFubMcfWbC5b4AkLqQh9xzIRE
            cRAKlDhOPoaoTaFeq2ocpCjOipq7s6UuVAqmx7WOj5PbcasyG6rMeTEr0rZrwFSw
            S6NQCoTM0n5rpxXL9S2qTTIUUYY1fjJ/y1Hocmpg9opIvU8xc0YXnoHoucmogpOE
            4dcwIfXMSwXPkiFAcZnApcXjH4VBYsrhho+acqZDUSzCr8OttzHifHCj2+tFnKYI
            ggddxP7tmwlkU7gPEmO9okXfg5mzlnif6FUepmRTsT1CrcBqGhic6TZCOEVcZGS+
            QQIDAQAB
            -----END PUBLIC KEY-----