При предоставлении 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”