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-----