Рубрики
Без рубрики

Пружинная защита с JWT: Сервер ресурсов OAuth2

Начиная с версии 5.2, Spring представила новую библиотеку OAuth 2.0 Resources ever, которая обрабатывает JWT south… Помечен как java, spring, безопасность, jwt.

Начиная с версии 5.2, Spring представила новую библиотеку Сервер ресурсов OAuth 2.0 , обрабатывающую JWT, так что нам больше не нужно вручную добавлять фильтр для извлечения утверждений из токена JWT и проверки токена.

Что такое сервер ресурсов?

Сервер ресурсов предоставляет защищенные ресурсы. Он связывается со своим сервером авторизации для проверки запроса на доступ к защищенному ресурсу. Обычно конечные точки сервера ресурсов защищены на основе областей Oauth2 и ролей пользователей. Пожалуйста, обратитесь к этому для получения более подробной информации.

Пример токена

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vbXkubWljcm9zZXJ2aWNlLmNvbS8iLCJzdWIiOiJzdWJqZWN0Iiwic2NvcGUiOlsicmVhZCJdLCJleHAiOjQ3NDA1NDczODcsImp0aSI6ImM4YWEyZjc3LTY2NjYtNDdmNy1iNTZlLTQyNGUxYzFlMThjYiIsImlhdCI6MTU4Njk0NzM4N30.PmHnPwf7M1vTPxskfzPgVvJhhJ9azLgoqTA6C4EsTSU

Когда вы декодируете его из jwt.io , вы обнаружите, что структура JWT состоит из 3 частей: заголовок, Полезная нагрузка, Подпись.

Заголовок

Обычно он содержит два поля:

  1. Тип токена, тип , JWT
  2. Алгоритм подписи, alg , HS256
{
  "typ": "JWT",
  "alg": "HS256"
}

Полезная нагрузка

Полезная нагрузка содержит набор утверждений . например мкс (эмитент), exp (срок действия), подзаголовок (тема)

{
  "iss": "http://my.microservice.com/",
  "sub": "subject",
  "scope": [
    "read"
  ],
  "exp": 4740547387,
  "jti": "c8aa2f77-6666-47f7-b56e-424e1c1e18cb",
  "iat": 1586947387
}

И утверждение, которое будет использоваться для авторизации наших конечных точек, – это область действия : читать .

Согласно this , сервер ресурсов Spring OAuth2 по умолчанию ищет имена моллюсков: область действия и scp , поскольку они являются хорошо известными заявками на авторизацию. Если вы собираетесь использовать пользовательское имя претензии, вы можете посмотреть пример в конце этого поста.

Мы собираемся использовать Spring Initializr для создания проекта Spring Boot с нуля.

Вот зависимости внутри файла build.gradle:

plugins {
  id 'org.springframework.boot' version '2.2.6.RELEASE'
  id 'io.spring.dependency-management' version '1.0.9.RELEASE'
  id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
repositories {
  mavenCentral()
}
dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
  implementation 'org.springframework.boot:spring-boot-starter-web'
  testImplementation('org.springframework.boot:spring-boot-starter-test') {
    exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
  }
}
test {
  useJUnitPlatform()
}

Как вы можете видеть, мы используем Spring Boot версии 2.2.6.RELEASE. Сервер spring-boot-starter-oauth2-resource-server включает версию spring-security-oauth2-jose версии 5.2.5., содержащую библиотеку nimbus-jose-jwt для поддержки декодирования JWT.

Мы создали 2 конечные точки:

  1. “/” конечная точка – принимает метод HTTP GET и ожидает HTTP-заголовок с “Авторизация: Предъявитель (токен JWT)”.
  2. “/сообщение” конечные точки принимают 2 HTTP-метода: GET и POST
import org.springframework.security.core.annotation.AuthenticationPrincipal;  
import org.springframework.security.oauth2.jwt.Jwt;  
import org.springframework.web.bind.annotation.GetMapping;  
import org.springframework.web.bind.annotation.PostMapping;  
import org.springframework.web.bind.annotation.RequestBody;  
import org.springframework.web.bind.annotation.RestController;  

@RestController  
public class Controller {  

  @GetMapping("/")  
  public String index(@AuthenticationPrincipal Jwt jwt) {  
    return String.format("Hello, %s!", jwt.getSubject());  
  }  

  @GetMapping("/message")  
  public String message() {  
    return "secret message";  
  }  

  @PostMapping("/message")  
  public String createMessage(@RequestBody String message) {  
    return String.format("Message was created. Content: %s", message);  
  }  
}
  1. Мы определяем правила безопасности для конечной точки/message. Конечная точка сообщения проверит, если

    • запрос имеет полномочия читать для метода GET
    • запрос имеет право писать для метода POST
  2. Мы также сообщаем Spring, что собираемся использовать сервер ресурсов OAuth2 с JSON Сеть Токен (JWT).

  3. Мы отключаем

    • Управление сеансами – это предотвратит создание файлов cookie сеанса.
    • Базовая аутентификация HTTP
    • Страница входа в систему Spring по умолчанию
    • КСО .
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.annotation.web.configuration.WebSecurityConfigurerAdapter;  
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;  


@EnableWebSecurity  
public class OAuth2ResourceServerSecurityConfiguration extends WebSecurityConfigurerAdapter {  

  @Override  
  protected void configure(HttpSecurity http) throws Exception {  
    http  
        .httpBasic().disable()  
        .formLogin(AbstractHttpConfigurer::disable)  
        .csrf(AbstractHttpConfigurer::disable)  
        .authorizeRequests(authorize -> authorize  
            .mvcMatchers(HttpMethod.GET, "/messages/**").hasAuthority("SCOPE_read")  
            .mvcMatchers(HttpMethod.POST, "/messages/**").hasAuthority("SCOPE_write")  
            .anyRequest().authenticated()  
        )  
        .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)  
        .sessionManagement(sessionManagement ->  
            sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))  
    ;  
  }  
}

Обратите внимание, что этот файл конфигурации выражен как DSL . В отличие от традиционного подхода с построением цепочки, мы можем использовать Java 8 lambda для выражения конфигураций.

spring:  
  security:  
    oauth2:  
      resourceserver:  
        jwt:  
          jwk-set-uri: https://login.domain.com/xxx/keys # JSON Web Key URI to use to verify the JWT token.

Вы получите сообщение HTTP 403 при вызове защищенной конечной точки с недопустимыми утверждениями. Например, вы отправили токен с функцией чтения область действия но конечные точки ожидают записи области видимости.

Вы получите сообщение HTTP 401 при сбое авторизации JWT. Например,

  1. токен не распознается эмитентом.
  2. срок действия жетона истек.
  3. маркер является недопустимой структурой.
  4. и т.д.

Конечная точка запроса сообщения с использованием HTTP ПОЛУЧИТЬ . Маркер содержит область чтения

GET http://localhost:8080/message
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vbXkubWljcm9zZXJ2aWNlLmNvbS8iLCJzdWIiOiJzdWJqZWN0Iiwic2NvcGUiOlsicmVhZCJdLCJleHAiOjQ3NDA1NDczODcsImp0aSI6ImM4YWEyZjc3LTY2NjYtNDdmNy1iNTZlLTQyNGUxYzFlMThjYiIsImlhdCI6MTU4Njk0NzM4N30.PmHnPwf7M1vTPxskfzPgVvJhhJ9azLgoqTA6C4EsTSU
HTTP/1.1 200 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: text/plain;charset=UTF-8
Content-Length: 33
Date: Wed, 15 Apr 2020 16:12:03 GMT

secret message

Response code: 200; Time: 1261ms; Content length: 337bytes

Конечная точка запроса сообщения с использованием HTTP пост . Маркер содержит область чтения . Но он ожидает записи области видимости.

POST http://localhost:8080/message
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vbXkubWljcm9zZXJ2aWNlLmNvbS8iLCJzdWIiOiJzdWJqZWN0Iiwic2NvcGUiOlsicmVhZCJdLCJleHAiOjQ3NDA1NDczODcsImp0aSI6ImM4YWEyZjc3LTY2NjYtNDdmNy1iNTZlLTQyNGUxYzFlMThjYiIsImlhdCI6MTU4Njk0NzM4N30.PmHnPwf7M1vTPxskfzPgVvJhhJ9azLgoqTA6C4EsTSU
HTTP/1.1 403 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 15 Apr 2020 20:12:00 GMT

{
  "timestamp": "2019-04-15T12:27:25.020+0000",
  "status": 403,
  "error": "Forbidden",
  "message": "Access Denied",
  "path": "/message"
}

Response code: 403; Time: 28ms; Content length: 125 byte

Что делать, если наш JWT не содержит хорошо известных утверждений ( область применения , scp ) для авторизации?

Мы будем использовать имя утверждения: роли в качестве примера.

Жетон с претензией: роли

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vbXkubWljcm9zZXJ2aWNlLmNvbS8iLCJzdWIiOiJzdWJqZWN0Iiwicm9sZXMiOlsic3R1ZGVudCJdLCJleHAiOjQ3NDA1NzAxMjMsImp0aSI6IjM3OWVhNzYxLTNlNTAtNDM2Mi04ZTEyLWQwNzIzNDZhN2JlMSIsImlhdCI6MTU4Njk3MDEyM30.qJCgYrMb17D6Y6MWKrpsaTLBmWKZXvc4wTGsZg2YGGY

Полезная нагрузка

{
  "iss": "http://my.microservice.com/",
  "sub": "subject",
  "roles": [
    "student"
  ],
  "exp": 4740570123,
  "jti": "379ea761-3e50-4362-8e12-d072346a7be1",
  "iat": 1586970123
}

В этом разделе будет показано, как изменить Конвертер JWT по умолчанию .

Мы изменим наш существующий файл конфигурации следующим образом:

package com.example.resourcesever;  

import org.springframework.core.convert.converter.Converter;  
import org.springframework.http.HttpMethod;  
import org.springframework.security.authentication.AbstractAuthenticationToken;  
import org.springframework.security.config.annotation.web.builders.HttpSecurity;  
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;  
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;  
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;  
import org.springframework.security.config.http.SessionCreationPolicy;  
import org.springframework.security.core.GrantedAuthority;  
import org.springframework.security.oauth2.jwt.Jwt;  
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;  
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;  

import java.util.Collection;  


@EnableWebSecurity  
public class OAuth2ResourceServerSecurityCustomConfiguration extends WebSecurityConfigurerAdapter {  

  private static final String AUTHORITY_PREFIX = "ROLE_";  
 private static final String CLAIM_ROLES = "roles";  

  @Override  
  protected void configure(HttpSecurity http) throws Exception {  
    http  
        .httpBasic().disable()  
        .formLogin(AbstractHttpConfigurer::disable)  
        .csrf(AbstractHttpConfigurer::disable)  
        .authorizeRequests(authorize -> authorize  
            .mvcMatchers(HttpMethod.GET, "/messages/**").hasAuthority("ROLE_student")  
            .mvcMatchers(HttpMethod.POST, "/messages/**").hasAuthority("ROLE_admin")  
            .anyRequest().authenticated()  
        )  
        .oauth2ResourceServer(oauth2ResourceServer ->  
            oauth2ResourceServer  
                .jwt(jwt ->  
                    jwt.jwtAuthenticationConverter(getJwtAuthenticationConverter()))  
        )  
        .sessionManagement(sessionManagement ->  
            sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))  
    ;  
  }  

  private Converter getJwtAuthenticationConverter() {  
    JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();  
  jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(getJwtGrantedAuthoritiesConverter());  
 return jwtAuthenticationConverter;  
  }  

  private Converter> getJwtGrantedAuthoritiesConverter() {  
    JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();  
  converter.setAuthorityPrefix(AUTHORITY_PREFIX);  
  converter.setAuthoritiesClaimName(CLAIM_ROLES);  
 return converter;  
  }  
}

Сверху, во-первых, мы сообщаем Spring, что хотим использовать имя претензии роли вместо области применения роли вместо область действия

  • роли вместо
  • область действия или scp

роли вместо область действия

роли вместо область действия или

package com.example.resourcesever;  

import org.springframework.core.convert.converter.Converter;  
import org.springframework.http.HttpMethod;  
import org.springframework.security.authentication.AbstractAuthenticationToken;  
import org.springframework.security.config.annotation.web.builders.HttpSecurity;  
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;  
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;  
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;  
import org.springframework.security.config.http.SessionCreationPolicy;  
import org.springframework.security.core.GrantedAuthority;  
import org.springframework.security.oauth2.jwt.Jwt;  
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;  
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;  

import java.util.Collection;  


@EnableWebSecurity  
public class OAuth2ResourceServerSecurityCustomConfiguration extends WebSecurityConfigurerAdapter {  

  private static final String AUTHORITY_PREFIX = "ROLE_";  
 private static final String CLAIM_ROLES = "roles";  

  @Override  
  protected void configure(HttpSecurity http) throws Exception {  
    http  
        .httpBasic().disable()  
        .formLogin(AbstractHttpConfigurer::disable)  
        .csrf(AbstractHttpConfigurer::disable)  
        .authorizeRequests(authorize -> authorize  
            .mvcMatchers(HttpMethod.GET, "/messages/**").hasRole("student")  
            .mvcMatchers(HttpMethod.POST, "/messages/**").hasRole("admin")  
            .anyRequest().authenticated()  
        )  
        .oauth2ResourceServer(oauth2ResourceServer ->  
            oauth2ResourceServer  
                .jwt(jwt ->  
                    jwt.jwtAuthenticationConverter(getJwtAuthenticationConverter()))  
        )  
        .sessionManagement(sessionManagement ->  
            sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))  
    ;  
  }  

  private Converter getJwtAuthenticationConverter() {  
    JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();  
  jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(getJwtGrantedAuthoritiesConverter());  
 return jwtAuthenticationConverter;  
  }  

  private Converter> getJwtGrantedAuthoritiesConverter() {  
    JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();  
  converter.setAuthorityPrefix(AUTHORITY_PREFIX);  
  converter.setAuthoritiesClaimName(CLAIM_ROLES);  
 return converter;  
  }  
}

роли вместо область действия или scp . Только роль студент пройдет авторизацию метода GET. Только роль администратор будет проходить авторизацию методом POST. Во-вторых, мы хотим установить префикс полномочий с помощью

Конечная точка запроса сообщения с использованием HTTP ПОЛУЧИТЬ . Маркер содержит ученика роль

GET http://localhost:8080/message
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vbXkubWljcm9zZXJ2aWNlLmNvbS8iLCJzdWIiOiJzdWJqZWN0Iiwicm9sZXMiOlsic3R1ZGVudCJdLCJleHAiOjQ3NDA1NzAxMjMsImp0aSI6IjM3OWVhNzYxLTNlNTAtNDM2Mi04ZTEyLWQwNzIzNDZhN2JlMSIsImlhdCI6MTU4Njk3MDEyM30.qJCgYrMb17D6Y6MWKrpsaTLBmWKZXvc4wTGsZg2YGGY
HTTP/1.1 200 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: text/plain;charset=UTF-8
Content-Length: 33
Date: Wed, 15 Apr 2020 17:11:01 GMT

secret message

Response code: 200; Time: 1261ms; Content length: 337bytes

Конечная точка запроса сообщения с использованием HTTP пост . Маркер содержит роль студента . Но он ожидает роль администратора .

POST http://localhost:8080/message
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vbXkubWljcm9zZXJ2aWNlLmNvbS8iLCJzdWIiOiJzdWJqZWN0Iiwicm9sZXMiOlsic3R1ZGVudCJdLCJleHAiOjQ3NDA1NzAxMjMsImp0aSI6IjM3OWVhNzYxLTNlNTAtNDM2Mi04ZTEyLWQwNzIzNDZhN2JlMSIsImlhdCI6MTU4Njk3MDEyM30.qJCgYrMb17D6Y6MWKrpsaTLBmWKZXvc4wTGsZg2YGGY
HTTP/1.1 403 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 15 Apr 2020 22:10:05 GMT

{
  "timestamp": "2019-04-15T12:27:25.020+0000",
  "status": 403,
  "error": "Forbidden",
  "message": "Access Denied",
  "path": "/message"
}

Response code: 403; Time: 28ms; Content length: 125 byte

Библиотека разделения ресурсов OAuth2 предоставляет нам минимальную конфигурацию. Нам больше не нужно писать фильтр.

Оригинал: “https://dev.to/toojannarong/spring-security-with-jwt-the-easiest-way-2i43”