OAuth 2.0 ресурсен сървър с пролетна сигурност 5

1. Общ преглед

В този урок ще научим как да настроим сървър за ресурси на OAuth 2.0, използвайки Spring Security 5 .

Ще направим това, използвайки JWT, както и непрозрачни маркери, двата вида токени на носител, поддържани от Spring Security.

Преди да преминем към примерите за внедряване и код, ще установим малко предистория.

2. Малко предистория

2.1. Какво представляват JWT и непрозрачни жетони?

JWT или JSON Web Token е начин за безопасно прехвърляне на чувствителна информация в широко приетия формат JSON. Съдържаната информация може да е за потребителя или за самия токен, като изтичането му и издателя му.

От друга страна, непрозрачен жетон, както подсказва името, е непрозрачен по отношение на информацията, която носи. Токенът е само идентификатор, който сочи към информацията, съхранявана на сървъра за оторизация - той се потвърждава чрез самоанализ в края на сървъра.

2.2. Какво е сървър за ресурси?

В контекста на OAuth 2.0, ресурсният сървър е приложение, което защитава ресурси чрез OAuth маркери . Тези маркери се издават от сървър за оторизация, обикновено на клиентско приложение. Работата на ресурсния сървър е да проверява маркера, преди да обслужва ресурс на клиента.

Валидността на маркера се определя от няколко неща:

  • Този маркер идва ли от конфигурирания сървър за оторизация?
  • Неизтекъл ли е?
  • Дали този ресурсен сървър е предназначена за аудитория?
  • Токенът има ли необходимите правомощия за достъп до искания ресурс?

За да визуализираме, нека да разгледаме диаграма на последователността за потока на оторизационния код и да видим всички действащи лица:

Както можем да видим в стъпка 8, когато клиентското приложение извиква API на ресурсния сървър за достъп до защитен ресурс, то първо отива към сървъра за оторизация, за да провери маркера, съдържащ се в заглавката Authorization: Bearer на заявката , и след това отговаря на клиента.

Стъпка 9 е това, върху което се фокусираме в този урок.

Добре, сега нека да преминем към частта с кода. Ще настроим сървър за оторизация, използвайки Keycloak, сървър за ресурси, валидиращ JWT маркери, друг сървър за ресурси, потвърждаващ непрозрачни маркери и няколко теста JUnit за симулиране на клиентски приложения и проверка на отговорите.

3. Сървър за оторизация

Първо ще настроим сървър за оторизация или нещо, което издава токени.

За това ще използваме Keycloak, вграден в Spring Boot Application . Keycloak е решение за управление на самоличността и достъпа с отворен код. Тъй като ние се фокусираме върху ресурсния сървър в този урок, няма да се задълбочаваме повече.

Нашият вграден Keycloak Server има два дефинирани клиента - fooClient и barClient - съответстващи на нашите две сървърни приложения за ресурси.

4. Ресурсен сървър - Използване на JWT

Нашият ресурсен сървър ще има четири основни компонента:

  • Модел - ресурсът за защита
  • API - REST контролер за излагане на ресурса
  • Конфигурация на защитата - клас за дефиниране на контрола на достъпа за защитения ресурс, който API излага
  • application.yml - конфигурационен файл за деклариране на свойства, включително информация за сървъра за оторизация

Нека ги видим един по един за нашия ресурсен сървър, обработващ JWT токени, след като надникнем в зависимостите.

4.1. Зависимости на Maven

Основно ще ни трябва Spring-boot-starter-oauth2-resource-server , стартер на Spring Boot за поддръжка на ресурсен сървър. Този стартер включва Spring Security по подразбиране, така че не е необходимо да го добавяме изрично:

 org.springframework.boot spring-boot-starter-web 2.2.6.RELEASE   org.springframework.boot spring-boot-starter-oauth2-resource-server 2.2.6.RELEASE   org.apache.commons commons-lang3 3.9 

Освен това сме добавили и уеб поддръжка.

За нашите демонстрационни цели ще генерираме ресурси на случаен принцип, вместо да ги получаваме от база данни, с известна помощ от библиотеката commons-lang3 на Apache .

4.2. Модел

Поддържайки го просто, ще използваме Foo , POJO, като наш защитен ресурс:

public class Foo { private long id; private String name; // constructor, getters and setters } 

4.3. API

Ето нашия контролер за почивка, за да направим Foo достъпен за манипулация:

@RestController @RequestMapping(value = "/foos") public class FooController { @GetMapping(value = "/{id}") public Foo findOne(@PathVariable Long id) { return new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4)); } @GetMapping public List findAll() { List fooList = new ArrayList(); fooList.add(new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4))); fooList.add(new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4))); fooList.add(new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4))); return fooList; } @ResponseStatus(HttpStatus.CREATED) @PostMapping public void create(@RequestBody Foo newFoo) { logger.info("Foo created"); } }

Както е видно, ние имаме разпоредбата за да получите всички Foo ите, може да получи Foo с идентификатор и публикувате Foo .

4.4. Конфигурация на защитата

В този клас на конфигурация дефинираме нивата на достъп за нашия ресурс:

@Configuration public class JWTSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests(authz -> authz .antMatchers(HttpMethod.GET, "/foos/**").hasAuthority("SCOPE_read") .antMatchers(HttpMethod.POST, "/foos").hasAuthority("SCOPE_write") .anyRequest().authenticated()) .oauth2ResourceServer(oauth2 -> oauth2.jwt()); } } 

Всеки, който има токен за достъп, имащ обхвата на четене, може да получи Foo s. За да публикува нов Foo , техният маркер трябва да има обхват на запис .

Освен това добавихме извикване към jwt (), използвайки oauth2ResourceServer () DSL, за да посочим типа маркери, поддържани от нашия сървър тук .

4.5. приложение.имм

В свойствата на приложението, в допълнение към обичайния номер на порт и контекстен път, трябва да дефинираме пътя до URI на издателя на нашия сървър за оторизация, така че сървърът на ресурсите да може да открие конфигурацията на доставчика си :

server: port: 8081 servlet: context-path: /resource-server-jwt spring: security: oauth2: resourceserver: jwt: issuer-uri: //localhost:8083/auth/realms/baeldung

Ресурсният сървър използва тази информация, за да провери JWT маркерите, идващи от клиентското приложение, съгласно стъпка 9 от нашата диаграма на последователността.

За да работи тази проверка, използвайки свойството издател-uri , сървърът за оторизация трябва да работи и да работи. В противен случай ресурсният сървър не би стартирал.

Ако трябва да го стартираме самостоятелно, вместо това можем да предоставим свойството jwk-set-uri, за да сочим към крайната точка на сървъра за оторизация, излагаща публични ключове:

jwk-set-uri: //localhost:8083/auth/realms/baeldung/protocol/openid-connect/certs

И това е всичко, от което се нуждаем, за да накараме нашия сървър да валидира JWT токени.

4.6. Тестване

За тестване ще настроим JUnit. За да изпълним този тест, ни е необходим сървър за оторизация, както и сървър за ресурси, който работи и работи.

Нека проверим, че можем да получим Foo s от ресурс-сървър-jw t с токен с обхват на четене в нашия тест:

@Test public void givenUserWithReadScope_whenGetFooResource_thenSuccess() { String accessToken = obtainAccessToken("read"); Response response = RestAssured.given() .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) .get("//localhost:8081/resource-server-jwt/foos"); assertThat(response.as(List.class)).hasSizeGreaterThan(0); }

В горния код, на ред # 3 получаваме маркер за достъп с обхват на четене от сървъра за оторизация, покриващ стъпки от 1 до 7 от нашата диаграма на последователността.

Стъпка 8 се извършва от RestAssured е GET () повикване. Стъпка 9 се изпълнява от ресурсния сървър с конфигурациите, които видяхме и е прозрачна за нас като потребители.

5. Сървър за ресурси - Използване на непрозрачни токени

След това нека видим същите компоненти за нашия ресурсен сървър, обработващ непрозрачни маркери.

5.1. Зависимости на Maven

За да поддържаме непрозрачни маркери, допълнително ще се нуждаем от зависимостта oauth2-oidc-sdk :

 com.nimbusds oauth2-oidc-sdk 8.19 runtime 

5.2. Model and Controller

For this one, we'll add a Bar resource:

public class Bar { private long id; private String name; // constructor, getters and setters } 

We'll also have a BarController with endpoints similar to our FooController before, to dish out Bars.

5.3. application.yml

In the application.yml here, we'll need to add an introspection-uri corresponding to our authorization server's introspection endpoint. As mentioned before, this is how an opaque token gets validated:

server: port: 8082 servlet: context-path: /resource-server-opaque spring: security: oauth2: resourceserver: opaque: introspection-uri: //localhost:8083/auth/realms/baeldung/protocol/openid-connect/token/introspect introspection-client-id: barClient introspection-client-secret: barClientSecret

5.4. Security Configuration

Keeping access levels similar to that of Foo for the Bar resource as well, this configuration class also makes a call to opaqueToken() using the oauth2ResourceServer() DSL to indicate the use of the opaque token type:

@Configuration public class OpaqueSecurityConfig extends WebSecurityConfigurerAdapter { @Value("${spring.security.oauth2.resourceserver.opaque.introspection-uri}") String introspectionUri; @Value("${spring.security.oauth2.resourceserver.opaque.introspection-client-id}") String clientId; @Value("${spring.security.oauth2.resourceserver.opaque.introspection-client-secret}") String clientSecret; @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests(authz -> authz .antMatchers(HttpMethod.GET, "/bars/**").hasAuthority("SCOPE_read") .antMatchers(HttpMethod.POST, "/bars").hasAuthority("SCOPE_write") .anyRequest().authenticated()) .oauth2ResourceServer(oauth2 -> oauth2 .opaqueToken(token -> token.introspectionUri(this.introspectionUri) .introspectionClientCredentials(this.clientId, this.clientSecret))); } } 

Here we're also specifying the client credentials corresponding to the authorization server's client we'll be using. We defined these earlier in our application.yml.

5.5. Testing

We'll set up a JUnit for our opaque token-based resource server, similar to how we did it for the JWT one.

In this case, let's check if a write scoped access token can POST a Bar to resource-server-opaque:

@Test public void givenUserWithWriteScope_whenPostNewBarResource_thenCreated() { String accessToken = obtainAccessToken("read write"); Bar newBar = new Bar(Long.parseLong(randomNumeric(2)), randomAlphabetic(4)); Response response = RestAssured.given() .contentType(ContentType.JSON) .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) .body(newBar) .log() .all() .post("//localhost:8082/resource-server-opaque/bars"); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED.value()); }

If we get a status of CREATED back, it means the resource server successfully validated the opaque token and created the Bar for us.

6. Conclusion

In this tutorial, we saw how to configure a Spring Security based resource server application for validating JWT as well as opaque tokens.

As we saw, with minimal setup, Spring made it possible to seamlessly validate the tokens with an issuer and send resources to the requesting party – in our case, a JUnit test.

As always, source code is available over on GitHub.