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

Внедрение Системы Авторизации OAuth 2.0 С Использованием Jakarta EE

В этой статье показан пример реализации системы авторизации OAuth2 с использованием Jakarta EE и микропрофиля.

Автор оригинала: baeldung.

1. Обзор

В этом учебном пособии мы представим реализацию платформы авторизации OAuth 2.0 с использованием Jakarta EE и МикроПрофиля. Самое главное, мы собираемся реализовать взаимодействие ролей OAuth 2.0 через Тип предоставления кода авторизации . Мотивация написания этой статьи заключается в поддержке проектов, реализуемых с использованием Jakarta EE, поскольку это еще не обеспечивает поддержку OAuth.

Для наиболее важной роли, сервера авторизации, мы собираемся реализовать Конечную точку авторизации, Конечную точку токена и, кроме того , конечную точку ключа JWK , которая полезна серверу ресурсов для извлечения открытого ключа.

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

Прежде чем сразу перейти к теме, важно отметить, что пример в этом руководстве предназначен для образовательных целей. Для производственных систем настоятельно рекомендуется использовать зрелые, хорошо протестированные решения, такие как Брелок для ключей .

2. Обзор OAuth 2.0

В этом разделе мы дадим краткий обзор ролей OAuth 2.0 и потока предоставления кода авторизации.

2.1. Роли

Платформа OAuth 2.0 подразумевает сотрудничество между четырьмя следующими ролями:

  • Владелец ресурса : Обычно это конечный пользователь-это организация, у которой есть некоторые ресурсы, которые стоит защищать
  • Сервер ресурсов : служба, которая защищает данные владельца ресурса, обычно публикуя их через API REST
  • Клиент : Приложение, которое использует данные владельца ресурса
  • Сервер авторизации : приложение, которое предоставляет разрешение – или полномочия – клиентам в виде истекающих токенов

2.2. Типы Разрешений

Тип предоставления – это способ, с помощью которого клиент получает разрешение на использование данных владельца ресурса, в конечном счете, в виде токена доступа.

Естественно, разные типы клиентов предпочитают разные типы грантов :

  • Код авторизации : Предпочтительнее всего является ли это веб-приложением, собственным приложением или одностраничным приложением , хотя собственные и одностраничные приложения требуют дополнительной защиты, называемой PKCE
  • Токен обновления : Специальный грант на продление, подходящий для веб-приложений для обновления существующего токена
  • Учетные данные клиента : Предпочтительно для связи между службами , скажем, когда владелец ресурса не является конечным пользователем
  • Владелец ресурса Пароль : Предпочтительно для сторонней аутентификации собственных приложений , скажите, когда мобильному приложению нужна собственная страница входа

Кроме того, клиент может использовать тип неявный грант. Однако, как правило, более безопасно использовать предоставление кода авторизации с помощью PKCE.

2.3. Поток Предоставления Кода Авторизации

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

Приложение – клиент – запрашивает разрешение путем перенаправления на конечную точку /авторизации сервера авторизации. Этой конечной точке приложение выдает обратный вызов конечной точке.

Сервер авторизации обычно запрашивает разрешение у конечного пользователя-владельца ресурса. Если конечный пользователь предоставляет разрешение, то сервер авторизации перенаправляет обратно на обратный вызов с кодом .

Приложение получает этот код, а затем выполняет аутентифицированный вызов /маркера конечной точки сервера авторизации. Под “аутентификацией” мы подразумеваем, что приложение доказывает, кто оно в рамках этого вызова. Если все отображается в порядке, сервер авторизации отвечает маркером.

С токеном в руке приложение делает запрос к API – серверу ресурсов – и этот API проверит токен. Он может попросить сервер авторизации проверить токен, используя свою конечную точку /introspect . Или, если токен является автономным, сервер ресурсов может оптимизировать работу, локально проверив подпись токена, как в случае с JWT.

2.4. Что поддерживает Jakarta EE?

Пока не очень много. В этом уроке мы построим большинство вещей с нуля.

3. Сервер авторизации OAuth 2.0

В этой реализации мы сосредоточимся на наиболее часто используемом типе предоставления : Код авторизации.

3.1. Регистрация Клиента и Пользователя

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

Для простоты, однако, мы будем использовать предварительно настроенный клиент:

INSERT INTO clients (client_id, client_secret, redirect_uri, scope, authorized_grant_types) 
VALUES ('webappclient', 'webappclientsecret', 'http://localhost:9180/callback', 
  'resource.read resource.write', 'authorization_code refresh_token');
@Entity
@Table(name = "clients")
public class Client {
    @Id
    @Column(name = "client_id")
    private String clientId;
    @Column(name = "client_secret")
    private String clientSecret;

    @Column(name = "redirect_uri")
    private String redirectUri;

    @Column(name = "scope")
    private String scope;

    // ...
}

И предварительно настроенный пользователь:

INSERT INTO users (user_id, password, roles, scopes)
VALUES ('appuser', 'appusersecret', 'USER', 'resource.read resource.write');
@Entity
@Table(name = "users")
public class User implements Principal {
    @Id
    @Column(name = "user_id")
    private String userId;

    @Column(name = "password")
    private String password;

    @Column(name = "roles")
    private String roles;

    @Column(name = "scopes")
    private String scopes;

    // ...
}

Обратите внимание, что для этого урока мы использовали пароли в виде обычного текста, но в производственной среде они должны быть хэшированы .

В остальной части этого урока мы покажем, как пользователь приложения – владелец ресурса – может предоставить доступ к webappclient – приложению – путем реализации Кода авторизации.

3.2. Конечная точка авторизации

Основная роль конечной точки авторизации состоит в том, чтобы сначала аутентифицировать пользователя, а затем запросить разрешения – или области–, которые нужны приложению.

Как указано в спецификациях OAuth2 , эта конечная точка должна поддерживать метод HTTP GET, хотя она также может поддерживать метод HTTP POST. В этой реализации мы будем поддерживать только метод HTTP GET.

Во-первых, конечная точка авторизации требует, чтобы пользователь прошел аутентификацию . Спецификация здесь не требует определенного способа, поэтому давайте использовать аутентификацию формы из Jakarta EE 8 Security API :

@FormAuthenticationMechanismDefinition(
  loginToContinue = @LoginToContinue(loginPage = "/login.jsp", errorPage = "/login.jsp")
)

Пользователь будет перенаправлен на /login.jsp для аутентификации, а затем будет доступен как CallerPrincipal через S ecurityContext API:

Principal principal = securityContext.getCallerPrincipal();

Мы можем собрать их вместе, используя JAX-RS:

@FormAuthenticationMechanismDefinition(
  loginToContinue = @LoginToContinue(loginPage = "/login.jsp", errorPage = "/login.jsp")
)
@Path("authorize")
public class AuthorizationEndpoint {
    //...    
    @GET
    @Produces(MediaType.TEXT_HTML)
    public Response doGet(@Context HttpServletRequest request,
      @Context HttpServletResponse response,
      @Context UriInfo uriInfo) throws ServletException, IOException {
        
        MultivaluedMap params = uriInfo.getQueryParameters();
        Principal principal = securityContext.getCallerPrincipal();
        // ...
    }
}

На этом этапе конечная точка авторизации может начать обработку запроса приложения, который должен содержать response_type и client_id параметры и – необязательно, но рекомендуется – redirect_uri, область действия, и состояние параметры.

client_id должен быть действительным клиентом, в нашем случае из таблицы базы данных клиенты .

redirect_uri , если указано, также должен соответствовать тому, что мы находим в таблице базы данных клиенты .

И, поскольку мы выполняем код авторизации, response_type является кодом.

Поскольку авторизация является многоступенчатым процессом, мы можем временно сохранить эти значения в сеансе:

request.getSession().setAttribute("ORIGINAL_PARAMS", params);

А затем приготовьтесь спросить пользователя, какие разрешения может использовать приложение, перенаправив его на эту страницу:

String allowedScopes = checkUserScopes(user.getScopes(), requestedScope);
request.setAttribute("scopes", allowedScopes);
request.getRequestDispatcher("/authorize.jsp").forward(request, response);

3.3. Утверждение областей действия Пользователя

На этом этапе браузер отображает пользовательский интерфейс авторизации для пользователя, и пользователь делает выбор. Затем браузер отправляет выбор пользователя в HTTP-СООБЩЕНИЕ :

@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.TEXT_HTML)
public Response doPost(@Context HttpServletRequest request, @Context HttpServletResponse response,
  MultivaluedMap params) throws Exception {
    MultivaluedMap originalParams = 
      (MultivaluedMap) request.getSession().getAttribute("ORIGINAL_PARAMS");

    // ...

    String approvalStatus = params.getFirst("approval_status"); // YES OR NO

    // ... if YES

    List approvedScopes = params.get("scope");

    // ...
}

Затем мы создаем временный код, который ссылается на идентификатор пользователя, идентификатор клиента, и перенаправление_ури, все из которых приложение будет использовать позже, когда оно достигнет конечной точки токена.

Итак, давайте создадим Код авторизации Сущность JPA с автоматически сгенерированным идентификатором :

@Entity
@Table(name ="authorization_code")
public class AuthorizationCode {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
@Column(name = "code")
private String code;

//...

}

А затем заселить его:

AuthorizationCode authorizationCode = new AuthorizationCode();
authorizationCode.setClientId(clientId);
authorizationCode.setUserId(userId);
authorizationCode.setApprovedScopes(String.join(" ", authorizedScopes));
authorizationCode.setExpirationDate(LocalDateTime.now().plusMinutes(2));
authorizationCode.setRedirectUri(redirectUri);

Когда мы сохраняем компонент, атрибут кода автоматически заполняется, поэтому мы можем получить его и отправить обратно клиенту:

appDataRepository.save(authorizationCode);
String code = authorizationCode.getCode();

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

Затем мы перенаправляем обратно в redirect_uri приложения, предоставляя ему код, а также любой состояние параметр, указанный приложением в его /авторизации запросе:

StringBuilder sb = new StringBuilder(redirectUri);
// ...

sb.append("?code=").append(code);
String state = params.getFirst("state");
if (state != null) {
    sb.append("&state=").append(state);
}
URI location = UriBuilder.fromUri(sb.toString()).build();
return Response.seeOther(location).build();

Еще раз обратите внимание, что redirectUri – это все, что существует в таблице клиенты , а не параметр redirect_url запрос.

Итак, наш следующий шаг заключается в том, чтобы клиент получил этот код и обменял его на токен доступа с помощью конечной точки токена.

3.4. Конечная точка токена

В отличие от конечной точки авторизации, конечной точке токена не нужен браузер для связи с клиентом , и поэтому мы реализуем ее как конечную точку JAX-RS:

@Path("token")
public class TokenEndpoint {

    List supportedGrantTypes = Collections.singletonList("authorization_code");

    @Inject
    private AppDataRepository appDataRepository;

    @Inject
    Instance authorizationGrantTypeHandlers;

    @POST
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    public Response token(MultivaluedMap params,
       @HeaderParam(HttpHeaders.AUTHORIZATION) String authHeader) throws JOSEException {
        //...
    }
}

Для конечной точки токена требуется сообщение, а также кодирование параметров с использованием application/x-www-форма-url-кодированный тип носителя.

Как мы уже обсуждали, мы будем поддерживать только код авторизации тип гранта:

List supportedGrantTypes = Collections.singletonList("authorization_code");

Таким образом, полученный grant_type в качестве требуемого параметра должен поддерживаться:

String grantType = params.getFirst("grant_type");
Objects.requireNonNull(grantType, "grant_type params is required");
if (!supportedGrantTypes.contains(grantType)) {
    JsonObject error = Json.createObjectBuilder()
      .add("error", "unsupported_grant_type")
      .add("error_description", "grant type should be one of :" + supportedGrantTypes)
      .build();
    return Response.status(Response.Status.BAD_REQUEST)
      .entity(error).build();
}

Затем мы проверяем аутентификацию клиента с помощью базовой аутентификации HTTP. То есть мы проверяем , соответствует ли полученный client_id и client_secret , через Заголовок авторизации , зарегистрированному клиенту:

String[] clientCredentials = extract(authHeader);
String clientId = clientCredentials[0];
String clientSecret = clientCredentials[1];
Client client = appDataRepository.getClient(clientId);
if (client == null || clientSecret == null || !clientSecret.equals(client.getClientSecret())) {
    JsonObject error = Json.createObjectBuilder()
      .add("error", "invalid_client")
      .build();
    return Response.status(Response.Status.UNAUTHORIZED)
      .entity(error).build();
}

Наконец, мы делегируем создание ответа токена соответствующему обработчику типа гранта:

public interface AuthorizationGrantTypeHandler {
    TokenResponse createAccessToken(String clientId, MultivaluedMap params) throws Exception;
}

Поскольку нас больше интересует тип предоставления кода авторизации, мы предоставили адекватную реализацию в виде компонента CDI и украсили его аннотацией С именем :

@Named("authorization_code")

Во время выполнения и в соответствии с полученным значением grant_type соответствующая реализация активируется через механизм экземпляра CDI :

String grantType = params.getFirst("grant_type");
//...
AuthorizationGrantTypeHandler authorizationGrantTypeHandler = 
  authorizationGrantTypeHandlers.select(NamedLiteral.of(grantType)).get();

Теперь пришло время выдать /токен ответ.

3.5. Закрытые и открытые ключи RSA

Перед созданием токена нам нужен закрытый ключ RSA для подписи токенов.

Для этой цели мы будем использовать OpenSSL:

# PRIVATE KEY
openssl genpkey -algorithm RSA -out private-key.pem -pkeyopt rsa_keygen_bits:2048

Закрытый ключ.pem предоставляется серверу через конфигурацию микропрофиля ключ подписи свойство с использованием файла META-INF/microprofile-config.properties:

signingkey=/META-INF/private-key.pem

Сервер может прочитать свойство, используя введенный Config объект:

String signingkey = config.getValue("signingkey", String.class);

Аналогично, мы можем сгенерировать соответствующий открытый ключ:

# PUBLIC KEY
openssl rsa -pubout -in private-key.pem -out public-key.pem

И используйте конфигурацию микропрофиля Ключ проверки , чтобы прочитать его:

verificationkey=/META-INF/public-key.pem

Сервер должен сделать его доступным для сервера ресурсов для цели проверки. Это делается через конечную точку JWK.

Nimbus JOSE+JWT – это библиотека, которая может оказать здесь большую помощь. Давайте сначала добавим | нимбус-хосе-jwt зависимость :


    com.nimbusds
    nimbus-jose-jwt
    7.7

И теперь мы можем использовать поддержку JWK Nimbus для упрощения нашей конечной точки:

@Path("jwk")
@ApplicationScoped
public class JWKEndpoint {

    @GET
    public Response getKey(@QueryParam("format") String format) throws Exception {
        //...

        String verificationkey = config.getValue("verificationkey", String.class);
        String pemEncodedRSAPublicKey = PEMKeyUtils.readKeyAsString(verificationkey);
        if (format == null || format.equals("jwk")) {
            JWK jwk = JWK.parseFromPEMEncodedObjects(pemEncodedRSAPublicKey);
            return Response.ok(jwk.toJSONString()).type(MediaType.APPLICATION_JSON).build();
        } else if (format.equals("pem")) {
            return Response.ok(pemEncodedRSAPublicKey).build();
        }

        //...
    }
}

Мы использовали параметр format для переключения между форматами PEM и JWK. Микропрофиль JWT, который мы будем использовать для реализации сервера ресурсов, поддерживает оба этих формата.

3.6. Ответ Конечной Точки Токена

Теперь настало время для данного обработчика Типа предоставления авторизации создать ответ на маркер. В этой реализации мы будем поддерживать только структурированные токены JWT.

Для создания токена в этом формате мы снова будем использовать Nimbus JOSE+JWT библиотека , но есть множество других библиотек JWT .

Итак, чтобы создать подписанный JWT, сначала нам нужно создать заголовок JWT:

JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.RS256).type(JOSEObjectType.JWT).build();

Затем мы создаем полезную нагрузку , которая представляет собой Набор стандартизированных и пользовательских утверждений:

Instant now = Instant.now();
Long expiresInMin = 30L;
Date in30Min = Date.from(now.plus(expiresInMin, ChronoUnit.MINUTES));

JWTClaimsSet jwtClaims = new JWTClaimsSet.Builder()
  .issuer("http://localhost:9080")
  .subject(authorizationCode.getUserId())
  .claim("upn", authorizationCode.getUserId())
  .audience("http://localhost:9280")
  .claim("scope", authorizationCode.getApprovedScopes())
  .claim("groups", Arrays.asList(authorizationCode.getApprovedScopes().split(" ")))
  .expirationTime(in30Min)
  .notBeforeTime(Date.from(now))
  .issueTime(Date.from(now))
  .jwtID(UUID.randomUUID().toString())
  .build();
SignedJWT signedJWT = new SignedJWT(jwsHeader, jwtClaims);

В дополнение к стандартным утверждениям JWT мы добавили еще два утверждения – upn и группы – по мере необходимости для JWT микропрофиля. upn будет сопоставлен с безопасностью Jakarta EE Принципал вызывающего абонента и группы будут сопоставлены с Jakarta EE Роли.

Теперь, когда у нас есть заголовок и полезная нагрузка, нам нужно подписать маркер доступа закрытым ключом RSA . Соответствующий открытый ключ RSA будет предоставлен через конечную точку JWK или станет доступным другими способами, чтобы сервер ресурсов мог использовать его для проверки маркера доступа.

Поскольку мы предоставили закрытый ключ в формате PEM, мы должны извлечь его и преобразовать в RSAPrivateKey:

SignedJWT signedJWT = new SignedJWT(jwsHeader, jwtClaims);
//...
String signingkey = config.getValue("signingkey", String.class);
String pemEncodedRSAPrivateKey = PEMKeyUtils.readKeyAsString(signingkey);
RSAKey rsaKey = (RSAKey) JWK.parseFromPEMEncodedObjects(pemEncodedRSAPrivateKey);

Далее, мы подписываем и сериализуем JWT:

signedJWT.sign(new RSASSASigner(rsaKey.toRSAPrivateKey()));
String accessToken = signedJWT.serialize();

И, наконец, мы создаем ответный токен:

return Json.createObjectBuilder()
  .add("token_type", "Bearer")
  .add("access_token", accessToken)
  .add("expires_in", expiresInMin * 60)
  .add("scope", authorizationCode.getApprovedScopes())
  .build();

который, благодаря JASON-P, сериализуется в формат JSON и отправляется клиенту:

{
  "access_token": "acb6803a48114d9fb4761e403c17f812",
  "token_type": "Bearer",  
  "expires_in": 1800,
  "scope": "resource.read resource.write"
}

4. Клиент OAuth 2.0

В этом разделе мы будем создавать веб-клиент OAuth 2.0 с использованием сервлета, Конфигурации микропрофиля и клиентских API JAX RS.

Точнее, мы будем реализовывать два основных сервлета: один для запроса конечной точки авторизации сервера авторизации и получения кода с использованием типа предоставления кода авторизации, а другой сервлет для использования полученного кода и запроса маркера доступа с конечной точки маркера сервера авторизации.

Кроме того, мы будем реализовывать еще два сервлета: один для получения нового маркера доступа с использованием типа предоставления маркера обновления, а другой для доступа к API сервера ресурсов.

4.1. Сведения о клиенте OAuth 2.0

Поскольку клиент уже зарегистрирован на сервере авторизации, нам сначала необходимо предоставить регистрационную информацию о клиенте:

  • client_id: Идентификатор клиента, который обычно выдается сервером авторизации в процессе регистрации.
  • client_secret: Секрет клиента.
  • redirect_uri: Место, где можно получить код авторизации.
  • область действия: Запрошенные клиентом разрешения.

Кроме того, клиент должен знать конечные точки авторизации и токенов сервера авторизации:

  • authorization_uri: Расположение конечной точки авторизации сервера авторизации, которую мы можем использовать для получения кода.
  • token_uri: Местоположение конечной точки маркера сервера авторизации, которую мы можем использовать для получения маркера.

Вся эта информация предоставляется через конфигурационный файл микропрофиля, META-INF/microprofile-config.properties:

# Client registration
client.clientId=webappclient
client.clientSecret=webappclientsecret
client.redirectUri=http://localhost:9180/callback
client.scope=resource.read resource.write

# Provider
provider.authorizationUri=http://127.0.0.1:9080/authorize
provider.tokenUri=http://127.0.0.1:9080/token

4.2. Запрос Кода Авторизации

Процесс получения кода авторизации начинается с клиента путем перенаправления браузера на конечную точку авторизации сервера авторизации.

Как правило, это происходит, когда пользователь пытается получить доступ к API защищенного ресурса без авторизации или явно путем вызова клиента /авторизация путь:

@WebServlet(urlPatterns = "/authorize")
public class AuthorizationCodeServlet extends HttpServlet {

    @Inject
    private Config config;

    @Override
    protected void doGet(HttpServletRequest request, 
      HttpServletResponse response) throws ServletException, IOException {
        //...
    }
}

В методе doGet() мы начинаем с создания и сохранения значения состояния безопасности:

String state = UUID.randomUUID().toString();
request.getSession().setAttribute("CLIENT_LOCAL_STATE", state);

Затем мы получаем информацию о конфигурации клиента:

String authorizationUri = config.getValue("provider.authorizationUri", String.class);
String clientId = config.getValue("client.clientId", String.class);
String redirectUri = config.getValue("client.redirectUri", String.class);
String scope = config.getValue("client.scope", String.class);

Затем мы добавим эти фрагменты информации в качестве параметров запроса к конечной точке авторизации сервера авторизации:

String authorizationLocation = authorizationUri + "?response_type=code"
  + "&client_id=" + clientId
  + "&redirect_uri=" + redirectUri
  + "&scope=" + scope
  + "&state=" + state;

И, наконец, мы перенаправим браузер на этот URL:

response.sendRedirect(authorizationLocation);

После обработки запроса конечная точка авторизации сервера авторизации сгенерирует и добавит код , в дополнение к параметру полученного состояния, в redirect_uri и перенаправит браузер обратно http://localhost:9081/callback?code=A123&state=Y .

4.3. Запрос токена Доступа

Сервлет обратного вызова клиента, /обратный вызов, начинается с проверки принятого состояния:

String localState = (String) request.getSession().getAttribute("CLIENT_LOCAL_STATE");
if (!localState.equals(request.getParameter("state"))) {
    request.setAttribute("error", "The state attribute doesn't match!");
    dispatch("/", request, response);
    return;
}

Затем мы будем использовать полученный ранее код для запроса маркера доступа через конечную точку маркера сервера авторизации:

String code = request.getParameter("code");
Client client = ClientBuilder.newClient();
WebTarget target = client.target(config.getValue("provider.tokenUri", String.class));

Form form = new Form();
form.param("grant_type", "authorization_code");
form.param("code", code);
form.param("redirect_uri", config.getValue("client.redirectUri", String.class));

TokenResponse tokenResponse = target.request(MediaType.APPLICATION_JSON_TYPE)
  .header(HttpHeaders.AUTHORIZATION, getAuthorizationHeaderValue())
  .post(Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE), TokenResponse.class);

Как мы видим, для этого вызова нет взаимодействия с браузером, и запрос выполняется напрямую с использованием клиентского API JAX-RS в качестве HTTP-СООБЩЕНИЯ.

Поскольку конечная точка токена требует аутентификации клиента, мы включили учетные данные клиента client_id и client_secret в заголовок Авторизация .

Клиент может использовать этот маркер доступа для вызова API сервера ресурсов, который является предметом следующего подраздела.

4.4. Защищенный Доступ К Ресурсам

На данный момент у нас есть действительный токен доступа, и мы можем вызвать сервер ресурсов/ читать и/ писать АПИС.

Для этого мы должны предоставить Авторизацию заголовок . Используя клиентский API JAX-RS, это просто делается с помощью вызова .Заголовок конструктора() метод:

resourceWebTarget = webTarget.path("resource/read");
Invocation.Builder invocationBuilder = resourceWebTarget.request();
response = invocationBuilder
  .header("authorization", tokenResponse.getString("access_token"))
  .get(String.class);

5. Сервер ресурсов OAuth 2.0

В этом разделе мы будем создавать защищенное веб-приложение на основе JAX-RS, JWT микропрофиля и конфигурации микропрофиля. Микропрофиль JWT заботится о проверке полученных JWT и сопоставлении областей JWT с ролями Jakarta EE .

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

В дополнение к зависимости Java EE Web API нам также нужны Конфигурация микропрофиля и JWT микропрофиля API:


    javax
    javaee-web-api
    8.0
    provided


    org.eclipse.microprofile.config
    microprofile-config-api
    1.3


    org.eclipse.microprofile.jwt
    microprofile-jwt-auth-api
    1.1

5.2. Механизм аутентификации JWT

Микропрофиль JWT обеспечивает реализацию механизма аутентификации токенов на предъявителя. Это обеспечивает обработку JWT, присутствующего в заголовке Authorization , делает доступным принципала безопасности Jakarta EE в качестве JsonWebToken , который содержит утверждения JWT, и сопоставляет области с ролями Jakarta EE. Взгляните на Jakarta EE Security API для получения дополнительной информации.

Чтобы включить Механизм аутентификации JWT на сервере, нам нужно добавить Конфигурацию входа аннотацию в приложение JAX-RS:

@ApplicationPath("/api")
@DeclareRoles({"resource.read", "resource.write"})
@LoginConfig(authMethod = "MP-JWT")
public class OAuth2ResourceServerApplication extends Application {
}

Кроме того, МикроПрофиль JWT нуждается в открытом ключе RSA для проверки подписи JWT . Мы можем обеспечить это либо путем самоанализа, либо, для простоты, вручную скопировав ключ с сервера авторизации. В любом случае нам необходимо указать местоположение открытого ключа:

mp.jwt.verify.publickey.location=/META-INF/public-key.pem

Наконец, JWT микропрофиля должен проверить утверждение isis входящего JWT, которое должно присутствовать и соответствовать значению свойства конфигурации МикроПрофиля:

mp.jwt.verify.issuer=http://127.0.0.1:9080

Как правило, это местоположение Сервера авторизации.

5.3. Защищенные Конечные Точки

В демонстрационных целях мы добавим API ресурсов с двумя конечными точками. Одним из них является конечная точка чтение , доступная пользователям, имеющим область resource.read , и другая конечная точка запись для пользователей с областью resource.write .

Ограничение областей действия выполняется с помощью @RolesAllowed аннотации:

@Path("/resource")
@RequestScoped
public class ProtectedResource {

    @Inject
    private JsonWebToken principal;

    @GET
    @RolesAllowed("resource.read")
    @Path("/read")
    public String read() {
        return "Protected Resource accessed by : " + principal.getName();
    }

    @POST
    @RolesAllowed("resource.write")
    @Path("/write")
    public String write() {
        return "Protected Resource accessed by : " + principal.getName();
    }
}

6. Запуск Всех Серверов

Чтобы запустить один сервер, нам просто нужно вызвать команду Maven в соответствующем каталоге:

mvn package liberty:run-server

Сервер авторизации, клиент и сервер ресурсов будут запущены и доступны соответственно в следующих местах:

# Authorization Server
http://localhost:9080/

# Client
http://localhost:9180/

# Resource Server
http://localhost:9280/

Итак, мы можем получить доступ к домашней странице клиента, а затем нажать “Получить токен доступа”, чтобы запустить процесс авторизации. После получения маркера доступа мы можем получить доступ к API чтения и записи сервера ресурсов.

В зависимости от предоставленных областей сервер ресурсов ответит либо сообщением об успешном выполнении, либо мы получим статус HTTP 403 запрещено.

7. Заключение

В этой статье мы представили реализацию сервера авторизации OAuth 2.0, который можно использовать с любым совместимым клиентом OAuth 2.0 и сервером ресурсов.

Чтобы объяснить общую структуру, мы также предоставили реализацию для клиента и сервера ресурсов. Для реализации всех этих компонентов мы использовали API Jakarta EE 8, особенно CDI, сервлет, JAX RS, безопасность Jakarta EE. Кроме того, мы использовали псевдо-джакартские API EE для микропрофиля: Конфигурация микропрофиля и JWT микропрофиля.

Полный исходный код примеров доступен на GitHub . Обратите внимание, что код содержит пример как кода авторизации, так и типов предоставления токенов обновления.

Наконец, важно помнить об образовательном характере этой статьи и о том, что приведенный пример не следует использовать в производственных системах.