Начиная с версии 5.2, Spring представила новую библиотеку Сервер ресурсов OAuth 2.0 , обрабатывающую JWT, так что нам больше не нужно вручную добавлять фильтр для извлечения утверждений из токена JWT и проверки токена.
Что такое сервер ресурсов?
Сервер ресурсов предоставляет защищенные ресурсы. Он связывается со своим сервером авторизации для проверки запроса на доступ к защищенному ресурсу. Обычно конечные точки сервера ресурсов защищены на основе областей Oauth2 и ролей пользователей. Пожалуйста, обратитесь к этому для получения более подробной информации.
Пример токена
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vbXkubWljcm9zZXJ2aWNlLmNvbS8iLCJzdWIiOiJzdWJqZWN0Iiwic2NvcGUiOlsicmVhZCJdLCJleHAiOjQ3NDA1NDczODcsImp0aSI6ImM4YWEyZjc3LTY2NjYtNDdmNy1iNTZlLTQyNGUxYzFlMThjYiIsImlhdCI6MTU4Njk0NzM4N30.PmHnPwf7M1vTPxskfzPgVvJhhJ9azLgoqTA6C4EsTSU
Когда вы декодируете его из jwt.io , вы обнаружите, что структура JWT состоит из 3 частей: заголовок, Полезная нагрузка, Подпись.
Заголовок
Обычно он содержит два поля:
- Тип токена, тип , JWT
- Алгоритм подписи, 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 конечные точки:
- “/” конечная точка – принимает метод HTTP GET и ожидает HTTP-заголовок с “Авторизация: Предъявитель (токен JWT)”.
- “/сообщение” конечные точки принимают 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); } }
Мы определяем правила безопасности для конечной точки/message. Конечная точка сообщения проверит, если
- запрос имеет полномочия читать для метода GET
- запрос имеет право писать для метода POST
Мы также сообщаем Spring, что собираемся использовать сервер ресурсов OAuth2 с JSON Сеть Токен (JWT).
Мы отключаем
- Управление сеансами – это предотвратит создание файлов 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. Например,
- токен не распознается эмитентом.
- срок действия жетона истек.
- маркер является недопустимой структурой.
- и т.д.
Конечная точка запроса сообщения с использованием 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 ConvertergetJwtAuthenticationConverter() { 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 ConvertergetJwtAuthenticationConverter() { 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”