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

Веб-сервисы в Java 3 – Аннотация авторизации

Защитите свои Java-API с помощью аннотаций. Помеченный как java, api, rest, безопасность.

При предоставлении API-интерфейсов в Интернете мы иногда (или в большинстве случаев) хотим управлять тем, кто, что и где находится. Или, другими словами, кто и где может получить доступ к каким данным.

Некоторая теория

Вот тут-то и вступает в игру авторизация. Некоторые (особенно я, пока я не погуглил) думают, что авторизация – это то же самое, что аутентификация, но вот: это не так. Когда мы говорим об аутентификации, мы имеем в виду процесс, при котором клиент представляет себя поставщику. Например: часть, где мы вводим ваше имя пользователя и пароль. В результате в большинстве случаев получается какая-то форма входного пропуска. Например, сеанс, ключ API или что-то еще.

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

Давайте приступим к работе

Теперь, когда мы знаем, чего мы хотим достичь, давайте взглянем на наш Java-код.

Java предлагает множество способов связать так называемый фильтр – компонент, который выполняется по запросу – с маршрутом или ресурсом. Многие из них включают в себя web.xml или подобные сложные вопросы. Вот почему мы сосредоточимся на подходе, основанном на аннотациях:

Чего мы хотим достичь:

@Path("")
public class ExampleResource {
    @GET
    @Path("all")
    @Produces(MediaType.APPLICATION_JSON)
    @Authorization("example:read")
    public String getAll() throws Exception{
        return "{ \"message\": \"Hello World\"}";
    }
}

Хорошо то, что большинство javax.ws.rs реализации уже реализуют большинство из этих функций . Единственное, что нам нужно будет реализовать, – это аннотацию @Authorization .

1. Интерфейс аннотаций

Чтобы создать аннотацию, нам нужен интерфейс, объявляющий ее. Это выглядит так:

// Retention declares the time of evaluation.
// As we want to have it evaluated at runtime, we declare it runtime.
@Retention(RUNTIME)
@Target({TYPE, METHOD}) // This annotation specifies where this annotation can be used.
public @interface Authorization {
    /**
     * List of roles that are permitted access.
     */
    String[] value();
}

2. Класс обработчика аннотаций

Хорошо, теперь у нас есть аннотация… но сейчас он вообще ничего не делает. Вот почему нам нужно объявить фильтр для обработки аннотированных ресурсов:

/**
 * When deploying your application as .war or .jar into a web server,
 * this annotation declares the class as provider for a filter.
 */
@Provider
public class AuthorizationFilter implements ContainerRequestFilter
{
     @Override
     public void filter(ContainerRequestContext requestContext) {
         // TODO: Fill
     }
}

Наш фильтр реализует интерфейс ContainerRequestFilter из расширения Java REST Services. Для этого интерфейса требуется метод, называемый “фильтр”. Этот метод будет вызван, когда наш фильтр будет применен к запросу. Ему передается экземпляр ContainerRequestContext , который предоставляет метаданные о запросе. Для получения дополнительной информации ознакомьтесь с Официальным JavaDoc !

Наш фильтр будет обрабатывать авторизацию в своем методе фильтр . Но сначала нам нужно проверить, оформлен ли вызываемый ресурс нашей аннотацией. Для достижения этой цели мы предоставим нашему классу Информацию о ресурсах . Это объект, который содержит информацию о методе Java, который оценивается в этом вызове запроса.

public class AuthorizationFilter implements ContainerRequestFilter
{
    // Provides information about the called resource
    @Context
    private ResourceInfo resourceInfo;

    @Override
    public void filter(ContainerRequestContext requestContext) {
        // TODO: Fill
    }
}

Аннотация @Context указывает серверу автоматически предоставлять этому классу соответствующий экземпляр RequestContext . Так что нам не нужно беспокоиться о том, как эта информация попадет сюда. Аккуратно!

Чтобы сохранить согласованность используемых идентификаторов и упростить будущие изменения, мы также должны объявить постоянные переменные в нашем заголовке и нашей схеме:

...
    // Provides information about the called resource
    @Context
    private ResourceInfo resourceInfo;
    private static final String AUTHORIZATION_HEADER = "Authorization";
    private static final String AUTHORIZATION_SCHEME = "Bearer";

    @Override
    public void filter(ContainerRequestContext requestContext) {
        // TODO: Fill
    }
...

Мило! А теперь давайте приступим к работе:

Сначала нам нужно проверить наличие аннотации:

    ...
    @Override
    public void filter(ContainerRequestContext requestContext) {
        Method calledMethod = resourceInfo.getResourceMethod();
        if(calledMethod.isAnnotationPresent(Authorization.class) {
           // Handle authorization
        }
    }
    ...

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

    ...
    if(calledMethod.isAnnotationPresent(Authorization.class) {
        String authorization = requestContext.getHeaderString(AUTHORIZATION_HEADER);
        //If no authorization information present; block access
        if(authorization == null || authorization.isEmpty())
        {
           throw new ForbiddenException("Resource Forbidden");
        }
    }
    ...
  • Аннотация: Подарок
  • Заголовок: Заполненный
  • Требования к доступу: еще не получены

Мы сделаем это, обратившись к массиву строк, которые мы передали в аннотацию.

    ...
    if(calledMethod.isAnnotationPresent(Authorization.class) {
        ...
        Authorization authAnnotation = calledMethod.getAnnotation(Authorization.class);
        Set accessRequirements = new HashSet<>(Arrays.asList(authAnnotation.value()));
    }
    ...

Хорошо. Во входящем запросе заголовок авторизации форматируется следующим образом:

Авторизация Носитель

Это означает, что нашему знаку предшествует слово Предъявитель . Поэтому нам нужно разделить содержимое заголовка авторизации для доступа к токену:

    ...
    if(calledMethod.isAnnotationPresent(Authorization.class) {
        ...
        Authorization authAnnotation = calledMethod.getAnnotation(Authorization.class);
        Set accessRequirements = new HashSet<>(Arrays.asList(authAnnotation.value()));
        final String key = authorization.replaceFirst(AUTHENTICATION_SCHEME + " ", "");
    }
    ...

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

Вот почему мы внедрим статическую карту с нашими пользовательскими данными.

    // Provides information about the called resource
    @Context
    private ResourceInfo resourceInfo;
    private static final String AUTHORIZATION_HEADER = "Authorization";
    private static final String AUTHORIZATION_SCHEME = "Bearer";

    private static final Map> userData = new HashMap>() {{
        put("admin", new HashSet(){{
            add("example:read");
        }});
        put("example_user", new HashSet() {{
            add("example:write");
        }});
    }};

    @Override
    public void filter(ContainerRequestContext requestContext) {
        ...
    }
...

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

администратор [пример:читать]
пример_пользователя [пример:написать]

Теперь мы должны проверить, имеет ли отправленный токен правильные права доступа. Чтобы сделать наш код более читабельным, мы действительно должны включить эту проверку в свой собственный метод:

...
    private boolean authorize(final String key, final Set accessRights)
    {
        boolean isAllowed = false;
        Set permittedActions = userData.get(key);

        if(permittedActions != null && permittedActions.stream().anyMatch(accessRights::contains))
            isAllowed = true;

        return isAllowed;
    }
}

И проверьте его результат

...
    Set accessRequirements = new HashSet<>(Arrays.asList(authAnnotation.value()));
    final String key = authorization.replaceFirst(AUTHENTICATION_SCHEME + " ", "");
    if(!authorized(key, acessRequirements))
        throw new ForbiddenException("Resource forbidden");
}

Вот и все! Это наша обработка авторизации. Если вам нужен полный код, ознакомьтесь это суть .

Теперь, когда у нас есть наши аннотации и наша обработка аннотаций, нам нужно предоставить наше решение серверу. Поскольку это часть серии, я буду ссылаться на тип сервера, который мы реализовали в части 1. В этом случае мы просто добавляем его к ресурсам, объявленным в нашей конфигурации ресурсов:

// Main.java
...
ResourceConfig resourceConfig = new ResourceConfig(); 
resourceConfig.register(ExampleResource.class); // our rest resource
// the parser for JSON and XML request bodies
resourceConfig.register(JacksonFeature.class);
resourceConfig.register(AuthorizationFilter.class);

Теперь у нас есть способ защитить наш API. Наслаждайтесь!

Титры изображения: Фото Сергея Горбача на Unsplash

Оригинал: “https://dev.to/funcke/web-services-in-java-3-authorization-annotation-2kd8”