Jakarta Security
Удалять записи блога должна только Анна-администратор, а смотреть свой профиль — любой вошедший автор. Jakarta Security решает две задачи. Аутентификация — узнать, кто пришёл: проверить имя и пароль. Авторизация — решить, что ему можно: обычно по ролям, например admin и author.
Способ входа и роли
// src/main/java/com/example/blog/SecurityConfig.java
package com.example.blog;
import jakarta.annotation.security.DeclareRoles;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.security.enterprise.authentication.mechanism.http.BasicAuthenticationMechanismDefinition;
@BasicAuthenticationMechanismDefinition(realmName = "blog")
@DeclareRoles({"admin", "author"})
@ApplicationScoped
public class SecurityConfig {
}@BasicAuthenticationMechanismDefinition включает HTTP Basic: клиент отправляет имя и пароль в заголовке Authorization с каждым запросом. @DeclareRoles перечисляет роли приложения.
Хранилище пользователей
IdentityStore проверяет имя и пароль и сообщает группы пользователя. Группы с теми же именами, что и роли, становятся ролями.
// src/main/java/com/example/blog/BlogIdentityStore.java
package com.example.blog;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.security.enterprise.credential.UsernamePasswordCredential;
import jakarta.security.enterprise.identitystore.CredentialValidationResult;
import jakarta.security.enterprise.identitystore.IdentityStore;
import java.util.Map;
import java.util.Set;
@ApplicationScoped
public class BlogIdentityStore implements IdentityStore {
private record User(String password, Set<String> groups) {
}
// только для учебного примера: в реальном приложении пользователи лежат в базе
private final Map<String, User> users = Map.of(
"anna", new User("changeme", Set.of("admin", "author")),
"oleg", new User("changeme", Set.of("author")));
public CredentialValidationResult validate(UsernamePasswordCredential credential) {
User user = users.get(credential.getCaller());
if (user == null || !credential.compareTo(credential.getCaller(), user.password())) {
return CredentialValidationResult.INVALID_RESULT;
}
return new CredentialValidationResult(credential.getCaller(), user.groups());
}
}Сервер находит бин, реализующий IdentityStore, и вызывает метод validate для каждого входа. В настоящем приложении пароли хранят в базе в виде хешей — для этого есть готовое хранилище @DatabaseIdentityStoreDefinition.
Защита методов
Удаление записи разрешим только роли admin:
// src/main/java/com/example/blog/PostResource.java (фрагмент)
@DELETE
@Path("/{id}")
@RolesAllowed("admin")
public void delete(@PathParam("id") long id) {
repository.delete(id);
}Аннотация @RolesAllowed из пакета jakarta.annotation.security. Методы без неё остаются открытыми.
Кто вошёл
SecurityContext из Jakarta Security рассказывает о текущем пользователе:
// src/main/java/com/example/blog/MeResource.java
package com.example.blog;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.security.enterprise.SecurityContext;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/me")
@Produces(MediaType.APPLICATION_JSON)
public class MeResource {
public record Me(String name, boolean admin) {
}
@Inject
SecurityContext security;
@GET
@RolesAllowed({"admin", "author"})
public Me me() {
return new Me(security.getCallerPrincipal().getName(),
security.isCallerInRole("admin"));
}
}Проверка
Флаг curl -u имя:пароль отправляет заголовок Basic:
curl -i http://localhost:8080/blog/api/me
curl -u anna:changeme http://localhost:8080/blog/api/me
curl -u oleg:changeme http://localhost:8080/blog/api/me
curl -i -u oleg:changeme -X DELETE http://localhost:8080/blog/api/posts/2
curl -i -u anna:changeme -X DELETE http://localhost:8080/blog/api/posts/2HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="blog"
{"admin":true,"name":"anna"}
{"admin":false,"name":"oleg"}
HTTP/1.1 403 Forbidden
HTTP/1.1 204 No Content
| Код | Значение |
|---|---|
401 Unauthorized | Не представился или пароль неверный |
403 Forbidden | Пользователь известен, но роли не хватает |
Олег вошёл успешно, но удалять записи ему нельзя — отсюда 403. Анне можно.
Частая ошибка
Включить Basic на обычном HTTP в рабочей системе. Заголовок Authorization: Basic YW5uYTpjaGFuZ2VtZQ== — это лишь имя и пароль в кодировке Base64, а не шифрование: любой, кто видит трафик, прочитает пароль. Basic допустим только поверх HTTPS — Open Liberty уже слушает порт 8443.