Как защитить веб сервисы при помощи шлюза OpenIG

от автора

Обеспечение безопасности веб сервисов — одна из важных частей процесса разработки. Если если в инфраструктуре несколько сервисов, то каждый из них должен быть должным образом защищен. Если реализовывать проверки политик безопасности в каждом сервисе, то затраты на разработку и поддержку таких сервисов существенно возрастают. При этом не избежать дублирования кода и ошибок разработки. Поэтому, управление защитой сервисов должно быть централизованным. Далее мы рассмотрим, как организовать централизованную защиту приложений на примере API-шлюза с открытым исходным кодом OpenIG, а так же добавим проверку авторизации доступа с JWT токеном

Исходный код для статьи https://github.com/maximthomas/openig-protect-ws/

Демонстрационный сервис

Пусть у нас есть сервис, разработанный на Spring Boot, с двумя endpoint / — публичной и /secure — приватной, доступ к которой могут иметь только аутентифицированные пользователи.

Пример сервиса:

package org.openidentityplatform.sampleservice;  import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;  import javax.servlet.http.HttpServletRequest; import java.util.Collections; import java.util.Map;  @SpringBootApplication public class SampleServiceApplication {     public static void main(String[] args) {         SpringApplication.run(SampleServiceApplication.class, args);     }      @RestController     public class IndexController {         @RequestMapping("/")         public Map<String, String> index() {             return Collections.singletonMap("hello", "world");         }          @RequestMapping("/secure")         public Map<String, String> secure(HttpServletRequest request) {             return Collections.singletonMap("hello", request.getHeader("X-Auth-Username"));         }     } } 

Запуск демонстрационного сервиса

Создайте docker-compose.yaml файл и добавьте в него демонстрационный сервис:

services:   sample-service: image: maximthomas/sample-service     restart: always 

Демонстрационный сервис будет работать без доступа из внешней сети. Далее мы добавим Docker контейнер со шлюзом OpenIG, который будет валидировать запросы и проксировать их до демонстрационного сервиса

Настройка OpenIG

Создайте директорию с конфигурацией OpenIG — openig-config в этой папке создайте еще одну директорию config . В папке openig-config/config создайте 2 файла конфигурации:

admin.json

{   "prefix" : "openig",   "mode": "PRODUCTION" } 

и config.json

{   "heap": [   ],   "handler": {     "type": "Chain",     "config": {       "filters": [        ],       "handler": {         "type": "Router",         "name": "_router",         "capture": "all"       }     }   } } 

Добавьте сервис OpenIG в файл docker-compose.yaml Смонтируйте папку конфигурации openig-config к Docker контейнеру OpenIG. Значение системной опции  -Dopenig.base должно указывать на смонтированную в контейнере директорию.

services:   sample-service:     build:       context: ./sample-service     restart: always    #OpenIG service   openig:     image: openidentityplatform/openig:latest     restart: always     volumes:       - ./openig-config:/usr/local/openig-config     environment:       #OpenIG options       CATALINA_OPTS: -Dopenig.base=/usr/local/openig-config     ports:       - "8080:8080" 

Проксирование запросов к сервису

Настроим проксирование запросов через OpenIG к демонстрационному сервису. Добавьте системную настройку -Dendpoint.api. Она будет указывать на URL демонстрационного сервиса и будет использоваться в настройках маршрутов OpenIG. Вы, конечно, можете прописать конечные точки непосредственно в маршруте, но, использование системных опций является рекомендуемым подходом.

docker-compose.yaml:

...   openig:     image: openidentityplatform/openig:latest     restart: always     volumes:       - ./openig-config:/usr/local/openig-config     environment:       #OpenIG options       CATALINA_OPTS: -Dopenig.base=/usr/local/openig-config -Dendpoint.api=http://sample-service:8080/     ports:       - "8080:808 

Добавьте маршрут, который будет проксировать запросы на сервис. Создайте папку routes с маршрутами в директории openig-config/config/. И добавьте в нее файл конфигурации маршрута

10-api.json

{   "name": "${matches(request.uri.path, '^/')}",   "condition": "${matches(request.uri.path, '^/')}",   "monitor": true,   "timer": true,   "handler": {     "type": "Chain",     "config": {       "filters": [        ],       "handler": "EndpointHandler"     }   },   "heap": [     {       "name": "EndpointHandler",       "type": "DispatchHandler",       "config": {         "bindings": [           {             "handler": "ClientHandler",             "capture": "all",             "baseURI": "${system['endpoint.api']}"           }         ]       }     }   ] } 

Такой маршрут проксирует все запросы на демонстрационный сервис из возвращает ответы без каких либо проверок.

Добавьте маршрут по умолчанию, который будет возвращать 404 статус на все остальные запросы

99-default.json:

{   "name": "99-default",   "handler": {     "type": "StaticResponseHandler",     "config": {       "status": 404,       "reason": "Not Found",       "headers": {         "Content-Type": [ "application/json" ]       },       "entity": "{ \"error\": \"Not Found\"}"     }   },   "audit": "/404" } 

Запустим демо сервис и OpenIG в Docker контейнерах:

docker-compose up 

После запуска проверим работоспособность

curl -v -X GET http://localhost:8080/ Note: Unnecessary use of -X or --request, GET is already inferred. *   Trying 127.0.0.1... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 8080 (#0) > GET / HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.58.0 > Accept: */* > < HTTP/1.1 200 OK < Server: Apache-Coyote/1.1 < Date: Wed, 24 Apr 2019 15:06:17 GMT < Content-Type: application/json;charset=UTF-8 < Transfer-Encoding: chunked < * Connection #0 to host localhost left intact {"hello":"world"}  curl -v -X GET http://localhost:8080/secure Note: Unnecessary use of -X or --request, GET is already inferred. *   Trying 127.0.0.1... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 8080 (#0) > GET /secure HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.58.0 > Accept: */* > < HTTP/1.1 200 OK < Server: Apache-Coyote/1.1 < Date: Wed, 24 Apr 2019 15:04:49 GMT < Content-Type: application/json;charset=UTF-8 < Transfer-Encoding: chunked < * Connection #0 to host localhost left intact {"name":null} 

После настройки проксирования запросов, давайте обеспечим безопасность демонстрационного сервиса

Защита сервиса

Для примера возьмем рекомендации OWASP по защите REST сервисов.

Ограничение методов HTTP

Добавим возможность пропускать к сервису только GET и POST запросы. Добавим в маршрут 10-api.json фильтр SwitchFilter

{   "name": "${matches(request.uri.path, '^/')}",   "condition": "${matches(request.uri.path, '^/')}",   "monitor": true,   "timer": true,   "handler": {     "type": "Chain",     "config": {       "filters": [         {           "type": "SwitchFilter",           "config": {             "onRequest": [               {                 "condition": "${request.method != 'POST' and request.method != 'GET'}",                 "handler": {                   "type": "StaticResponseHandler",                   "config": {                     "status": 405,                     "reason": "Method not allowed",                     "headers": {                       "Content-Type": [                         "application/json"                       ]                     },                     "entity": "{ \"error\": \"Method not allowed\"}"                   }                 }               }             ]           }         }       ],        "handler": "EndpointHandler"     }   },   "heap": [     {       "name": "EndpointHandler",       "type": "DispatchHandler",       "config": {         "bindings": [           {             "handler": "ClientHandler",             "capture": "all",             "baseURI": "${system['endpoint.api']}"           }         ]       }     }   ] } 

Проверим, что если запрос не GET и не POST шлюз вернет статус 405:

$ curl -v -X PUT http://localhost:8080/ *   Trying 127.0.0.1... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 8080 (#0) > PUT / HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.58.0 > Accept: */* > < HTTP/1.1 405 Method Not Allowed < Server: Apache-Coyote/1.1 < Content-Type: application/json < Content-Length: 32 < Date: Wed, 24 Apr 2019 15:13:04 GMT < * Connection #0 to host localhost left intact { "error": "Method not allowed"}  

Проверка заголовка запроса Content-Type

Пусть для демонстрационного сервиса будут допустимы POST запросы только с Content-Type: application/json . Для этого добавьте в SwitchFilter проверку заголовка Content-Type

10-api.json:

...       {           "type": "SwitchFilter",           "config": {             "onRequest": [               {                 "condition": "${request.method != 'POST' and request.method != 'GET'}",                 "handler": {                   "type": "StaticResponseHandler",                   "config": {                     "status": 405,                     "reason": "Method not allowed",                     "headers": {                       "Content-Type": [                         "application/json"                       ]                     },                     "entity": "{ \"error\": \"Method not allowed\"}"                   }                 }               },               {                 "condition": "${request.method == 'POST' and request.headers['Content-Type'][0].split(';')[0] != 'application/json'}",                 "handler": {                   "type": "StaticResponseHandler",                   "config": {                     "status": 415,                     "reason": "Unsupported Media Type",                     "headers": {                       "Content-Type": [ "application/json" ]                     },                     "entity": "{ \"error\": \"Unsupported Media Type\"}"                   }                 }               }             ]           }         } ...         

Проверим, что ограничение работает для Content-Type: application/xml

$ curl -v -X POST -H 'Content-Type: application/xml' http://localhost:8080/ *   Trying 127.0.0.1... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 8080 (#0) > POST / HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.58.0 > Accept: */* > Content-Type: application/xml > < HTTP/1.1 415 Unsupported Media Type < Server: Apache-Coyote/1.1 < Content-Type: application/json < Content-Length: 36 < Date: Wed, 24 Apr 2019 15:21:04 GMT < * Connection #0 to host localhost left intact { "error": "Unsupported Media Type"} 

Проверка совпадения заголовков Accept запроса и Content-Type ответа

Значение заголовка Content-Type ответа должно совпадать со значением заголовка Accept запроса. Добавьте условие проверки в объект config фильтра SwitchFilter маршрута:

10-api.json:

...           "onResponse" : [               {                 "condition" : "${response.headers['Content-Type'][0].split(';')[0] != request.headers['Accept'][0].split(';')[0] }",                 "handler": {                   "type": "StaticResponseHandler",                   "config": {                     "status": 406,                     "reason": "Not Acceptable",                     "headers": {                       "Content-Type": [ "application/json" ]                     },                     "entity": "{ \"error\": \"Not Acceptable\"}"                   }                 }               }             ] ...              

Проверим запрос с заголовком Accept: application/xml

curl -v -X POST -H 'Content-Type: application/json' -H 'Accept: application/xml'  http://localhost:8080/ *   Trying 127.0.0.1... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 8080 (#0) > POST / HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.58.0 > Content-Type: application/json > Accept: application/xml > < HTTP/1.1 406 Not Acceptable < Server: Apache-Coyote/1.1 < Content-Type: application/json < Content-Length: 28 < Date: Wed, 24 Apr 2019 15:28:54 GMT < * Connection #0 to host localhost left intact { "error": "Not Acceptable"} 

Добавление заголовков безопасности X-Frame-Options и X-Content-Type-Options

OpenIG должен возвращать клиенту заголовки X-Frame-Options: deny и X-Content-Type-Options: nosniff, чтобы предотвратить MIME sniffing, XSS и drag’n drop clickjacking атаки. Для этого добавьте HeaderFilter в цепочку фильтров после SwitchFilter:

10-api.json:

{   "type": "HeaderFilter",   "comment": "Add security headers to response",   "config": {     "messageType": "response",     "add": {       "X-Frame-Options": [ "deny" ],       "X-Content-Type-Options": [ "nosniff" ]     }   } } 

Проверим заголовки ответа:

curl -v -X POST -H 'Content-Type: application/json' -H 'Accept: application/json'  http://localhost:8080/ *   Trying 127.0.0.1... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 8080 (#0) > POST / HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.58.0 > Content-Type: application/json > Accept: application/json > < HTTP/1.1 200 OK < Server: Apache-Coyote/1.1 < Date: Wed, 24 Apr 2019 15:31:31 GMT < X-Content-Type-Options: nosniff < X-Frame-Options: deny < Content-Type: application/json;charset=UTF-8 < Transfer-Encoding: chunked < * Connection #0 to host localhost left intact {"hello":"world"} 

Проверка аутентификации и авторизации

Если вам нужно защитить сервис от не аутентифицированного доступа, нет необходимости реализовывать проверку аутентификации для каждого сервиса. Вы можете проверить доступ непосредственно на OpenIG. И если запрос аутентифицирован, обогатить запрос заголовком информацией об учетной записи. Например, сервис аутентификации возвращает клиенту подписанный JSON Web Token (JWT) и шлюз использует переданный клиентом JWT для авторизации доступа к сервису. В конфигурации OpenIG лежит публичный ключ и OpenIG проверяет подпись JWT с этим ключом, для того, чтобы удостовериться в подлинности JWT.

Сгенерируйте пару ключей

Публичный

openssl genrsa -out private_key.pem 4096 

И приватный

openssl rsa -pubout -in private_key.pem -out public_key.pem 

Уже сгенерированные ключи лежат в GitHub репозитории https://github.com/maximthomas/openig-protect-ws/tree/master/openig-config/keys

Сгенерируйте JWT при помощи сайта https://jwt.io и сгенерированного приватного ключа private_key.pem

Пример JWT:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzYW1wbGUtc2VydmljZSIsInN1YiI6IjEyMzQ1Njc4OTAiLCJuYW1lIjoiSm9obiBEb2UiLCJyb2xlIjoibWFuYWdlciIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNzI2MjM5MDIyfQ.bhzhwj2cY1iYbpx7Mzbukfi1jOCvWP-Pdd9dm3hf7lZDDuokNVDUXU3jvHial4QN-bOTSNCUKVy907hokcVeQaFwbiYoZs485Kr230Z0y9MU6zbDe8yQp68-71TDgJGIZ78YYOKvJTrzCWgWgE_Py1DskG_ViSxfGFlETpFQa056Rk2bty-9iuc_Cx5_Wr6RCcJTG6WYRrBtdWGIFxljEjxSAcJYmGPPA8dHHORDOnmka2OAjWURnsqbz50aI_SrWpnqp4i2eXVA1b5QD5rlsgc_QAqJptghrijBlRPhasTk1N-kXE8Ozboa0FwGfIRr7gNiG-3if7INZe2R5NUCmjlAlywcSfOunWuSzY8tLGTHV2swnQPP8lBXwVJcS5nJMqBNIbcLcFWHk3ryvvtf-LYty_jAY8v1zDe9-DwFPWI0rry8fmiZj7yhAnvX5EHZHvSQtp_zyPpVWvOXFPwasI0jdKoxhWvyJpbmw-D95J5CgJAMfkrWPDQKVt3ipebwnMJStA3xAPPyl28mTBYhJrT6gzIOS3DseoVKK4adn34ZrQi2Hm-wyUtbdulopK739MKM73NYgoFXSJeVUqcy4iw3-In5XmOhdRnUL50TSiaNBbkys8iK7r00HD3kI3CH0GfaPdrcgRgaFXKmVDhX-tEaPJYcuEUTHfQAxWwMdiw 

Проверка JWT в OpenIG

Добавим проверку аутентификации для конечно точки /secured демонстрационного сервиса. Для этого добавим еще один SwitchFilter , который, в свою очередь, вызовет обработчикChain если целевая конечная точка является secured . Добавьте в обработчик Chain фильтр ScriptableFilter , который будет проверять валидность JWT и обогащать запрос идентификатором учетной записи из JWT.

10-api.json:

...         {           "type": "SwitchFilter",           "config": {             "onRequest": [               {                 "condition": "${matches(request.uri.path, '^/secure')}",                 "handler": {                   "type": "Chain",                   "config": {                     "filters": [                       {                         "type": "ScriptableFilter",                         "config": {                           "type": "application/x-groovy",                           "file": "jwt.groovy",                           "args": {                             "iss": {                               "sample-service": "${read('/usr/local/openig-config/keys/public_key.pem')}"                             }                           }                         }                       }                     ],                     "handler": "EndpointHandler"                   }                 }               }             ]           }         } ...         

Добавьте файл jwt.groovy в папку  /openig-config/scripts/groovy/ . Скрипт проверяет подпись, и, если подпись верна, проверяет срок истечения JWT. Если JWT валиден, скрипт обогащает запрос заголовком X-Auth-Username из поля name полезной нагрузки JWT. В противном случае возвращается 401 статус HTTP.

jwt.groovy:

import java.security.KeyFactory import org.forgerock.json.jose.builders.JwtBuilderFactory import org.forgerock.json.jose.jws.SignedJwt import org.forgerock.json.jose.jws.SigningManager import org.forgerock.http.protocol.Status import java.security.spec.X509EncodedKeySpec  //extract jwt from request header def jwt = request.headers['Authorization']?.firstValue  if (jwt!=null && jwt.startsWith("Bearer eyJ")) {     jwt=jwt.replace("Bearer ", "")      try {         //parse jwt         def sjwt=new JwtBuilderFactory().reconstruct(jwt, SignedJwt.class)          //verify jwt signature         if (!sjwt.verify(new SigningManager().newRsaSigningHandler(getKey(sjwt.getClaimsSet())))) {             throw new Exception("invalid signature")         }          //check jwt expiration         if ((sjwt.getClaimsSet().getExpirationTime()!=null && sjwt.getClaimsSet().getExpirationTime().before(new Date()))) {             throw new Exception("signature expired "+sjwt.getClaimsSet().getExpirationTime())         }          //add name from JWT claim to header         request.headers.put('X-Auth-Username', sjwt.getClaimsSet().getClaim("name"))          return next.handle(new org.forgerock.openig.openam.StsContext(context, jwt), request)     } catch(Exception e) {         e.printStackTrace();         return getErrorResponse(Status.UNAUTHORIZED, e.getMessage())     } } else {     //returns 401 status if JWT not present in request     return getErrorResponse(Status.UNAUTHORIZED, "Not Authenticated") }  return next.handle(context, request)  def getErrorResponse(status, message) {     def response = new Response()     response.status = status     response.headers['Content-Type'] = "application/json"     response.setEntity("{'error' : '" + message + "'}")     return response }  def getKey(claims) {     def pem=iss[claims.getIssuer()]     if (pem != null) {         def pemReplaced = pem.replaceFirst("(?m)(?s)^---*BEGIN.*---*\$(.*)^---*END.*---*\$.*", "\$1")         byte[] encoded = Base64.getMimeDecoder().decode(pemReplaced)         def kf = KeyFactory.getInstance("RSA")         def pubKey = kf.generatePublic(new X509EncodedKeySpec(encoded))         println 'got pub key' + pubKey         return pubKey     }      throw new Exception('Unknown issuer') }  

Проверим запрос c валидным JWT:

curl -v GET -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzYW1wbGUtc2VydmljZSIsInN1YiI6IjEyMzQ1Njc4OTAiLCJuYW1lIjoiSm9obiBEb2UiLCJyb2xlIjoibWFuYWdlciIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNzI2MjM5MDIyfQ.bhzhwj2cY1iYbpx7Mzbukfi1jOCvWP-Pdd9dm3hf7lZDDuokNVDUXU3jvHial4QN-bOTSNCUKVy907hokcVeQaFwbiYoZs485Kr230Z0y9MU6zbDe8yQp68-71TDgJGIZ78YYOKvJTrzCWgWgE_Py1DskG_ViSxfGFlETpFQa056Rk2bty-9iuc_Cx5_Wr6RCcJTG6WYRrBtdWGIFxljEjxSAcJYmGPPA8dHHORDOnmka2OAjWURnsqbz50aI_SrWpnqp4i2eXVA1b5QD5rlsgc_QAqJptghrijBlRPhasTk1N-kXE8Ozboa0FwGfIRr7gNiG-3if7INZe2R5NUCmjlAlywcSfOunWuSzY8tLGTHV2swnQPP8lBXwVJcS5nJMqBNIbcLcFWHk3ryvvtf-LYty_jAY8v1zDe9-DwFPWI0rry8fmiZj7yhAnvX5EHZHvSQtp_zyPpVWvOXFPwasI0jdKoxhWvyJpbmw-D95J5CgJAMfkrWPDQKVt3ipebwnMJStA3xAPPyl28mTBYhJrT6gzIOS3DseoVKK4adn34ZrQi2Hm-wyUtbdulopK739MKM73NYgoFXSJeVUqcy4iw3-In5XmOhdRnUL50TSiaNBbkys8iK7r00HD3kI3CH0GfaPdrcgRgaFXKmVDhX-tEaPJYcuEUTHfQAxWwMdiw' http://localhost:8080/secure  * Could not resolve host: GET * Closing connection 0 curl: (6) Could not resolve host: GET *   Trying 127.0.0.1:8080... * Connected to localhost (127.0.0.1) port 8080 (#1) > GET /secure HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/8.1.2 > Content-Type: application/json > Accept: application/json > Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzYW1wbGUtc2VydmljZSIsInN1YiI6IjEyMzQ1Njc4OTAiLCJuYW1lIjoiSm9obiBEb2UiLCJyb2xlIjoibWFuYWdlciIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNzI2MjM5MDIyfQ.bhzhwj2cY1iYbpx7Mzbukfi1jOCvWP-Pdd9dm3hf7lZDDuokNVDUXU3jvHial4QN-bOTSNCUKVy907hokcVeQaFwbiYoZs485Kr230Z0y9MU6zbDe8yQp68-71TDgJGIZ78YYOKvJTrzCWgWgE_Py1DskG_ViSxfGFlETpFQa056Rk2bty-9iuc_Cx5_Wr6RCcJTG6WYRrBtdWGIFxljEjxSAcJYmGPPA8dHHORDOnmka2OAjWURnsqbz50aI_SrWpnqp4i2eXVA1b5QD5rlsgc_QAqJptghrijBlRPhasTk1N-kXE8Ozboa0FwGfIRr7gNiG-3if7INZe2R5NUCmjlAlywcSfOunWuSzY8tLGTHV2swnQPP8lBXwVJcS5nJMqBNIbcLcFWHk3ryvvtf-LYty_jAY8v1zDe9-DwFPWI0rry8fmiZj7yhAnvX5EHZHvSQtp_zyPpVWvOXFPwasI0jdKoxhWvyJpbmw-D95J5CgJAMfkrWPDQKVt3ipebwnMJStA3xAPPyl28mTBYhJrT6gzIOS3DseoVKK4adn34ZrQi2Hm-wyUtbdulopK739MKM73NYgoFXSJeVUqcy4iw3-In5XmOhdRnUL50TSiaNBbkys8iK7r00HD3kI3CH0GfaPdrcgRgaFXKmVDhX-tEaPJYcuEUTHfQAxWwMdiw >  < HTTP/1.1 200  < Date: Wed, 19 Jun 2024 08:59:06 GMT < X-Content-Type-Options: nosniff < X-Frame-Options: deny < Content-Type: application/json < Transfer-Encoding: chunked <  * Connection #1 to host localhost left intact {"hello":"John Doe"} 

Запрос без JWT:

curl -v GET -H 'Content-Type: application/json' -H 'Accept: application/json'  http://localhost:8080/secure * Could not resolve host: GET * Closing connection 0 curl: (6) Could not resolve host: GET *   Trying 127.0.0.1:8080... * Connected to localhost (127.0.0.1) port 8080 (#1) > GET /secure HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/8.1.2 > Content-Type: application/json > Accept: application/json >  < HTTP/1.1 401  < X-Content-Type-Options: nosniff < X-Frame-Options: deny < Content-Type: application/json < Content-Length: 31 < Date: Wed, 19 Jun 2024 08:59:43 GMT <  * Connection #1 to host localhost left intact {'error' : 'Not Authenticated'} 

Проверка авторизации

Настроим OpenIG таким образом, чтобы он авторизовывал доступ доступ только пользователям с ролью manager . Роль будем брать claim JWT role. Если в JWT роль отсутствует или отлична от manager, вернем HTTP статус 403 Forbidden.

Добавим в маршрут в фильтр ScriptableFilter параметр allowedRole, чтобы можно было устанавливать допустимую роль в маршруте, не меняя скрипт.

... {   "type": "ScriptableFilter",   "config": {     "type": "application/x-groovy",     "file": "jwt.groovy",     "args": {       "iss": {         "sample-service": "${read('/usr/local/openig-config/keys/public_key.pem')}"                                     },       "allowedRole": "manager"     }   } } ... 

Добавим в jwt.groovy проверку роли после проверки срока действия:

//check jwt expiration if ((sjwt.getClaimsSet().getExpirationTime()!=null && sjwt.getClaimsSet().getExpirationTime().before(new Date()))) {     throw new Exception("signature expired "+sjwt.getClaimsSet().getExpirationTime()) }  //check role  if (!sjwt.getClaimsSet().keys().contains("role")      || !allowedRole.equals(sjwt.getClaimsSet().getClaim("role", String.class))) {      return getErrorResponse(Status.FORBIDDEN, "Forbidden")             } 

Проверим запрос с валидным JWT

curl -v GET -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzYW1wbGUtc2VydmljZSIsInN1YiI6IjEyMzQ1Njc4OTAiLCJuYW1lIjoiSm9obiBEb2UiLCJyb2xlIjoibWFuYWdlciIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNzI2MjM5MDIyfQ.bhzhwj2cY1iYbpx7Mzbukfi1jOCvWP-Pdd9dm3hf7lZDDuokNVDUXU3jvHial4QN-bOTSNCUKVy907hokcVeQaFwbiYoZs485Kr230Z0y9MU6zbDe8yQp68-71TDgJGIZ78YYOKvJTrzCWgWgE_Py1DskG_ViSxfGFlETpFQa056Rk2bty-9iuc_Cx5_Wr6RCcJTG6WYRrBtdWGIFxljEjxSAcJYmGPPA8dHHORDOnmka2OAjWURnsqbz50aI_SrWpnqp4i2eXVA1b5QD5rlsgc_QAqJptghrijBlRPhasTk1N-kXE8Ozboa0FwGfIRr7gNiG-3if7INZe2R5NUCmjlAlywcSfOunWuSzY8tLGTHV2swnQPP8lBXwVJcS5nJMqBNIbcLcFWHk3ryvvtf-LYty_jAY8v1zDe9-DwFPWI0rry8fmiZj7yhAnvX5EHZHvSQtp_zyPpVWvOXFPwasI0jdKoxhWvyJpbmw-D95J5CgJAMfkrWPDQKVt3ipebwnMJStA3xAPPyl28mTBYhJrT6gzIOS3DseoVKK4adn34ZrQi2Hm-wyUtbdulopK739MKM73NYgoFXSJeVUqcy4iw3-In5XmOhdRnUL50TSiaNBbkys8iK7r00HD3kI3CH0GfaPdrcgRgaFXKmVDhX-tEaPJYcuEUTHfQAxWwMdiw' http://localhost:8080/secure * Could not resolve host: GET * Closing connection 0 curl: (6) Could not resolve host: GET *   Trying 127.0.0.1:8080... * Connected to localhost (127.0.0.1) port 8080 (#1) > GET /secure HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/8.1.2 > Content-Type: application/json > Accept: application/json > Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzYW1wbGUtc2VydmljZSIsInN1YiI6IjEyMzQ1Njc4OTAiLCJuYW1lIjoiSm9obiBEb2UiLCJyb2xlIjoibWFuYWdlciIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNzI2MjM5MDIyfQ.bhzhwj2cY1iYbpx7Mzbukfi1jOCvWP-Pdd9dm3hf7lZDDuokNVDUXU3jvHial4QN-bOTSNCUKVy907hokcVeQaFwbiYoZs485Kr230Z0y9MU6zbDe8yQp68-71TDgJGIZ78YYOKvJTrzCWgWgE_Py1DskG_ViSxfGFlETpFQa056Rk2bty-9iuc_Cx5_Wr6RCcJTG6WYRrBtdWGIFxljEjxSAcJYmGPPA8dHHORDOnmka2OAjWURnsqbz50aI_SrWpnqp4i2eXVA1b5QD5rlsgc_QAqJptghrijBlRPhasTk1N-kXE8Ozboa0FwGfIRr7gNiG-3if7INZe2R5NUCmjlAlywcSfOunWuSzY8tLGTHV2swnQPP8lBXwVJcS5nJMqBNIbcLcFWHk3ryvvtf-LYty_jAY8v1zDe9-DwFPWI0rry8fmiZj7yhAnvX5EHZHvSQtp_zyPpVWvOXFPwasI0jdKoxhWvyJpbmw-D95J5CgJAMfkrWPDQKVt3ipebwnMJStA3xAPPyl28mTBYhJrT6gzIOS3DseoVKK4adn34ZrQi2Hm-wyUtbdulopK739MKM73NYgoFXSJeVUqcy4iw3-In5XmOhdRnUL50TSiaNBbkys8iK7r00HD3kI3CH0GfaPdrcgRgaFXKmVDhX-tEaPJYcuEUTHfQAxWwMdiw >  < HTTP/1.1 200  < Date: Wed, 19 Jun 2024 09:05:31 GMT < X-Content-Type-Options: nosniff < X-Frame-Options: deny < Content-Type: application/json < Transfer-Encoding: chunked <  * Connection #1 to host localhost left intact {"hello":"John Doe"}%   

Проверим запрос с JWT с ролью, отличной от manager

curl -v -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzYW1wbGUtc2VydmljZSIsInN1YiI6IjEyMzQ1Njc4OTAiLCJuYW1lIjoiSm9obiBEb2UiLCJyb2xlIjoiYmFkIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3MjYyMzkwMjJ9.UezPgiGOcbp9CMM7hkrbvFsPmFIOnPnph5n60wF9jEWfGAIpS3dgBYvsprsVx0iZaUfhj2GTTLXhQUKrEM08n6jhUBSlwQ22LYBEHhBY57-AwtUhFZVJL8En00tc3HTGLV_El55PyvJvuLRbQ_fZB7rfp27OMPS0y2ciehz21_90TGKvUWUUGJgqDvRPchSKdO7LVa97oigGUp8vi7XiutMxopMLoms63f7FbasbIxMfgEFa48cuJTTcmk7genlPpMX8CBeBUjVriK0452uYdONvSFllqX2rdHwi7idKV-wB0qeUdNq2MDgcVqTrztxRQ8_ezoZVMnn3OLzuSABSpHKtPM3G3uVctY2X408zwOqe86BFvahT1eyBsEmrtszaIL-REy6vy-6P8JJ7iZdD720F1h3VyXj7PWNQiA-v3TumBLpRiML4Clb0SmqpB2iIvPhAz2-ob1w9BBxbvES6n95JEvFDlsv0JqOpvs-ZqQeR1pL7ML0RDR6ZR7xMWE6iVC4hlHEyX5Ufi6CBvkzVLVSnbIPyIBSBc4bzDzqdRkgt139bEdD-htrKWFmGkJKl_yvNcW_rYCkeMmb60km389XUtpiBoSc5CmKkcxrjsarvEMRh-AkIqB5R7Hz0KVKFdp1Hlzj4v1CQKK8eM4Poiq0NoO9IgHFJtgZKMosD7Qc ' http://localhost:8080/secure *   Trying 127.0.0.1:8080... * Connected to localhost (127.0.0.1) port 8080 (#0) > GET /secure HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/8.1.2 > Content-Type: application/json > Accept: application/json > Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzYW1wbGUtc2VydmljZSIsInN1YiI6IjEyMzQ1Njc4OTAiLCJuYW1lIjoiSm9obiBEb2UiLCJyb2xlIjoiYmFkIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3MjYyMzkwMjJ9.UezPgiGOcbp9CMM7hkrbvFsPmFIOnPnph5n60wF9jEWfGAIpS3dgBYvsprsVx0iZaUfhj2GTTLXhQUKrEM08n6jhUBSlwQ22LYBEHhBY57-AwtUhFZVJL8En00tc3HTGLV_El55PyvJvuLRbQ_fZB7rfp27OMPS0y2ciehz21_90TGKvUWUUGJgqDvRPchSKdO7LVa97oigGUp8vi7XiutMxopMLoms63f7FbasbIxMfgEFa48cuJTTcmk7genlPpMX8CBeBUjVriK0452uYdONvSFllqX2rdHwi7idKV-wB0qeUdNq2MDgcVqTrztxRQ8_ezoZVMnn3OLzuSABSpHKtPM3G3uVctY2X408zwOqe86BFvahT1eyBsEmrtszaIL-REy6vy-6P8JJ7iZdD720F1h3VyXj7PWNQiA-v3TumBLpRiML4Clb0SmqpB2iIvPhAz2-ob1w9BBxbvES6n95JEvFDlsv0JqOpvs-ZqQeR1pL7ML0RDR6ZR7xMWE6iVC4hlHEyX5Ufi6CBvkzVLVSnbIPyIBSBc4bzDzqdRkgt139bEdD-htrKWFmGkJKl_yvNcW_rYCkeMmb60km389XUtpiBoSc5CmKkcxrjsarvEMRh-AkIqB5R7Hz0KVKFdp1Hlzj4v1CQKK8eM4Poiq0NoO9IgHFJtgZKMosD7Qc >  >  < HTTP/1.1 403  < X-Content-Type-Options: nosniff < X-Frame-Options: deny < Content-Type: application/json < Content-Length: 23 < Date: Wed, 19 Jun 2024 09:06:32 GMT <  * Connection #0 to host localhost left intact {'error' : 'Forbidden'}%  


ссылка на оригинал статьи https://habr.com/ru/articles/823212/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *