Як працює автентифікація на основі лексем
Під час аутентифікації на основі лексеми клієнт обмінюється жорсткими обліковими записами (такими як ім'я користувача та пароль) на частину даних, що називається маркер . Для кожного запиту, замість того, щоб надсилати жорсткі облікові дані, клієнт надсилає маркер серверу для здійснення автентифікації та авторизації.
Кількома словами схема аутентифікації на основі лексем виконайте наступні кроки:
- Клієнт відправляє свої дані (ім’я користувача та пароль) на сервер.
- Сервер ідентифікує облікові дані та, якщо вони дійсні, генерує маркер для користувача.
- Сервер зберігає раніше створений маркер в деякому сховищі разом з ідентифікатором користувача та датою закінчення терміну дії.
- Сервер відправляє згенерований маркер клієнту.
- Клієнт надсилає маркер серверу в кожному запиті.
- У кожному запиті сервер вилучає маркер із вхідного запиту. За допомогою маркера сервер шукає реквізити користувача для здійснення автентифікації.
- Якщо маркер дійсний, сервер приймає запит.
- Якщо маркер недійсний, сервер відмовляється від запиту.
- Після того, як аутентифікація виконана, сервер здійснює авторизацію.
- Сервер може надати кінцеву точку для оновлення маркерів.
Примітка: крок 3 не потрібен, якщо сервер видав підписаний маркер (наприклад, JWT, який дозволяє виконувати автентифікацію без стану ).
Що ви можете зробити з JAX-RS 2.0 (Jersey, RESTEasy та Apache CXF)
Це рішення використовує лише JAX-RS 2.0 API, уникаючи будь-якого конкретного рішення для постачальника . Отже, він повинен працювати з реалізаціями JAX-RS 2.0, такими як Jersey , RESTEasy та Apache CXF .
Варто зазначити, що якщо ви використовуєте аутентифікацію на основі лексем, ви не покладаєтесь на стандартні механізми захисту веб-додатків Java EE, що пропонуються контейнером сервлетів і налаштовуються за допомогою web.xml
дескриптора програми. Це спеціальна аутентифікація.
Аутентифікація користувача з його ім'ям користувача та паролем та видача маркер
Створіть ресурсний метод JAX-RS, який отримує та перевіряє облікові дані (ім'я користувача та пароль) та видає користувачеві маркер:
@Path("/authentication")
public class AuthenticationEndpoint {
@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response authenticateUser(@FormParam("username") String username,
@FormParam("password") String password) {
try {
// Authenticate the user using the credentials provided
authenticate(username, password);
// Issue a token for the user
String token = issueToken(username);
// Return the token on the response
return Response.ok(token).build();
} catch (Exception e) {
return Response.status(Response.Status.FORBIDDEN).build();
}
}
private void authenticate(String username, String password) throws Exception {
// Authenticate against a database, LDAP, file or whatever
// Throw an Exception if the credentials are invalid
}
private String issueToken(String username) {
// Issue a token (can be a random String persisted to a database or a JWT token)
// The issued token must be associated to a user
// Return the issued token
}
}
Якщо під час перевірки облікових даних будуть викинуті будь-які винятки, відповідь зі статусом 403
(Заборонено) буде повернуто.
Якщо облікові дані будуть успішно перевірені, відповідь зі статусом 200
(ОК) буде повернуто, і виданий маркер буде відправлений клієнтові в корисному навантаженні відповіді. Клієнт повинен надсилати маркер на сервер у кожному запиті.
Під час споживання application/x-www-form-urlencoded
клієнт повинен надіслати облікові дані у такому форматі у корисному завантаженні запиту:
username=admin&password=123456
Замість параметри форми можна вкласти ім’я користувача та пароль у клас:
public class Credentials implements Serializable {
private String username;
private String password;
// Getters and setters omitted
}
А потім споживайте його як JSON:
@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public Response authenticateUser(Credentials credentials) {
String username = credentials.getUsername();
String password = credentials.getPassword();
// Authenticate the user, issue a token and return a response
}
Використовуючи такий підхід, клієнт повинен надіслати облікові дані у такому форматі у корисному навантаженні запиту:
{
"username": "admin",
"password": "123456"
}
Вилучення маркера із запиту та перевірка його
Клієнт повинен надіслати маркер у стандартному Authorization
заголовку HTTP запиту. Наприклад:
Authorization: Bearer <token-goes-here>
Ім'я стандартного заголовка HTTP невдале, оскільки воно містить інформацію про автентифікацію , а не авторизацію . Однак це стандартний HTTP-заголовок для надсилання облікових даних на сервер.
JAX-RS забезпечує @NameBinding
метаанотацію, яка використовується для створення інших приміток для прив'язки фільтрів та перехоплювачів до класів та методів ресурсів. Визначте @Secured
примітку таким чином:
@NameBinding
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface Secured { }
Вищеописана анотація, що пов'язує імена, буде використана для прикраси класу фільтрів, який реалізується ContainerRequestFilter
, що дозволяє перехопити запит перед тим, як його обробляти ресурсним методом. ContainerRequestContext
Може бути використаний для доступу заголовків запиту HTTP , а потім витягти маркер:
@Secured
@Provider
@Priority(Priorities.AUTHENTICATION)
public class AuthenticationFilter implements ContainerRequestFilter {
private static final String REALM = "example";
private static final String AUTHENTICATION_SCHEME = "Bearer";
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
// Get the Authorization header from the request
String authorizationHeader =
requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
// Validate the Authorization header
if (!isTokenBasedAuthentication(authorizationHeader)) {
abortWithUnauthorized(requestContext);
return;
}
// Extract the token from the Authorization header
String token = authorizationHeader
.substring(AUTHENTICATION_SCHEME.length()).trim();
try {
// Validate the token
validateToken(token);
} catch (Exception e) {
abortWithUnauthorized(requestContext);
}
}
private boolean isTokenBasedAuthentication(String authorizationHeader) {
// Check if the Authorization header is valid
// It must not be null and must be prefixed with "Bearer" plus a whitespace
// The authentication scheme comparison must be case-insensitive
return authorizationHeader != null && authorizationHeader.toLowerCase()
.startsWith(AUTHENTICATION_SCHEME.toLowerCase() + " ");
}
private void abortWithUnauthorized(ContainerRequestContext requestContext) {
// Abort the filter chain with a 401 status code response
// The WWW-Authenticate header is sent along with the response
requestContext.abortWith(
Response.status(Response.Status.UNAUTHORIZED)
.header(HttpHeaders.WWW_AUTHENTICATE,
AUTHENTICATION_SCHEME + " realm=\"" + REALM + "\"")
.build());
}
private void validateToken(String token) throws Exception {
// Check if the token was issued by the server and if it's not expired
// Throw an Exception if the token is invalid
}
}
Якщо під час перевірки маркера виникнуть якісь проблеми, відповідь зі статусом 401
(Несанкціонований) буде повернуто. Інакше запит перейде до ресурсного методу.
Забезпечення кінцевих точок REST
Щоб прив’язати фільтр аутентифікації до методів ресурсів або класів ресурсів, анотуйте їх за допомогою @Secured
анотації, створеної вище. Для методів та / або класів, які коментуються, фільтр буде виконаний. Це означає, що до таких кінцевих точок буде досягнуто лише, якщо запит буде виконано з дійсним маркером.
Якщо деяким методам чи класам не потрібна автентифікація, просто не коментуйте їх:
@Path("/example")
public class ExampleResource {
@GET
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Response myUnsecuredMethod(@PathParam("id") Long id) {
// This method is not annotated with @Secured
// The authentication filter won't be executed before invoking this method
...
}
@DELETE
@Secured
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Response mySecuredMethod(@PathParam("id") Long id) {
// This method is annotated with @Secured
// The authentication filter will be executed before invoking this method
// The HTTP request must be performed with a valid token
...
}
}
У наведеному вище прикладі фільтр буде виконаний лише для mySecuredMethod(Long)
методу, оскільки він анотований до @Secured
.
Ідентифікація поточного користувача
Цілком ймовірно, що вам потрібно буде знати користувача, який виконує запит, після отримання вашого REST API. Для її досягнення можуть бути використані наступні підходи:
Переосмислення контексту безпеки поточного запиту
У межах вашого ContainerRequestFilter.filter(ContainerRequestContext)
методу SecurityContext
для поточного запиту може бути встановлений новий екземпляр. Потім замініть SecurityContext.getUserPrincipal()
, повертаючи Principal
екземпляр:
final SecurityContext currentSecurityContext = requestContext.getSecurityContext();
requestContext.setSecurityContext(new SecurityContext() {
@Override
public Principal getUserPrincipal() {
return () -> username;
}
@Override
public boolean isUserInRole(String role) {
return true;
}
@Override
public boolean isSecure() {
return currentSecurityContext.isSecure();
}
@Override
public String getAuthenticationScheme() {
return AUTHENTICATION_SCHEME;
}
});
Використовуйте маркер, щоб знайти ідентифікатор користувача (ім'я користувача), яке буде Principal
ім'ям.
Введіть SecurityContext
будь-який клас ресурсів JAX-RS:
@Context
SecurityContext securityContext;
Те ж саме можна зробити в ресурсному методі JAX-RS:
@GET
@Secured
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Response myMethod(@PathParam("id") Long id,
@Context SecurityContext securityContext) {
...
}
А потім отримайте Principal
:
Principal principal = securityContext.getUserPrincipal();
String username = principal.getName();
Використання CDI (введення контексту та залежності)
Якщо з якихось причин ви не хочете їх перекривати SecurityContext
, ви можете використовувати CDI ( введення контексту та залежності), яка надає корисні функції, такі як події та виробники.
Створіть класифікатор CDI:
@Qualifier
@Retention(RUNTIME)
@Target({ METHOD, FIELD, PARAMETER })
public @interface AuthenticatedUser { }
У AuthenticationFilter
створеному вище введіть Event
примітку із приміткою @AuthenticatedUser
:
@Inject
@AuthenticatedUser
Event<String> userAuthenticatedEvent;
Якщо аутентифікація пройде успішно, запустіть подію, передаючи ім'я користувача як параметр (пам'ятайте, маркер видається користувачеві, і маркер буде використовуватися для пошуку ідентифікатора користувача):
userAuthenticatedEvent.fire(username);
Дуже ймовірно, що у вашій програмі є клас, який представляє користувача. Давайте назвемо цей клас User
.
Створіть компакт-диск CDI для обробки події аутентифікації, знайдіть User
примірник з відповідним ім'ям користувача та призначте його в authenticatedUser
поле виробника:
@RequestScoped
public class AuthenticatedUserProducer {
@Produces
@RequestScoped
@AuthenticatedUser
private User authenticatedUser;
public void handleAuthenticationEvent(@Observes @AuthenticatedUser String username) {
this.authenticatedUser = findUser(username);
}
private User findUser(String username) {
// Hit the the database or a service to find a user by its username and return it
// Return the User instance
}
}
authenticatedUser
Поле виробляє User
екземпляр , який може бути введений в контейнер керованих компонентів, таких як послуги JAX-RS, CDI бобів, сервлетів і EJBs. Використовуйте наступний фрагмент коду, щоб вставити User
екземпляр (адже це проксі-сервер CDI):
@Inject
@AuthenticatedUser
User authenticatedUser;
Зауважте, що @Produces
примітка CDI відрізняється від @Produces
анотації JAX-RS :
Будьте впевнені, що ви використовуєте @Produces
примітку CDI у своїй AuthenticatedUserProducer
квасолі.
Ключовим тут є квасоля, в якій зазначається @RequestScoped
, що дозволяє обмінюватися даними між фільтрами та вашими зернами. Якщо ви не хочете використовувати події, ви можете змінити фільтр, щоб зберігати автентифікованого користувача в обміні запиту, а потім прочитати його з ваших ресурсних класів JAX-RS.
Порівняно з підходом, який перекриває SecurityContext
, підхід CDI дозволяє отримувати аутентифікованого користувача з бобів, крім ресурсів та постачальників JAX-RS.
Підтримка рольової авторизації
Будь ласка, зверніться до моєї іншої відповіді, щоб отримати детальну інформацію про те, як підтримувати рольову авторизацію.
Видача жетонів
Маркер може бути:
- Непрозорий: не виявляє жодної деталі, крім самого значення (як випадкова рядок)
- Самостійний: Містить інформацію про сам маркер (наприклад, JWT).
Дивіться деталі нижче:
Випадковий рядок як маркер
Маркер можна видавати, генеруючи випадкову рядок і зберігаючи її в базі даних разом з ідентифікатором користувача та датою закінчення терміну дії. Хороший приклад того, як генерувати випадкову рядок на Java, можна побачити тут . Ви також можете використовувати:
Random random = new SecureRandom();
String token = new BigInteger(130, random).toString(32);
JWT (веб-маркер JSON)
JWT (JSON Web Token) - це стандартний метод надійного представлення претензій між двома сторонами і визначається RFC 7519 .
Це автономний маркер, який дозволяє зберігати деталі у претензіях . Ці претензії зберігаються в токені корисного навантаження, який є JSON, закодованим як Base64 . Ось деякі претензії, зареєстровані в RFC 7519, і що вони означають (читайте повний RFC для подальшої інформації):
iss
: Директор, який видав маркер.
sub
: Директор, що є предметом JWT.
exp
: Дата закінчення терміну дії для маркера.
nbf
: Час, коли маркер почне прийматись для обробки.
iat
: Час, в який видано маркер.
jti
: Унікальний ідентифікатор для маркера.
Майте на увазі, що ви не повинні зберігати в маркер чутливі дані, такі як паролі.
Клієнт може прочитати корисну навантаження, а цілісність маркера легко перевірити, перевіривши його підпис на сервері. Підпис - це те, що запобігає підробці маркера.
Вам не потрібно буде зберігати маркери JWT, якщо вам не потрібно відстежувати їх. Все-таки, зберігаючи жетони, ви матимете можливість скасувати та скасувати доступ до них. Щоб відслідковувати маркери JWT, замість того, щоб зберігати весь маркер на сервері, ви можете зберегти ідентифікатор ( jti
претензію) маркера разом з деякими іншими деталями, такими як користувач, якому ви видали маркер, термін придатності тощо.
Під час збереження маркерів завжди слід видалити старі, щоб запобігти зростанню вашої бази даних на невизначений термін.
Використання JWT
Є кілька бібліотек Java для видачі та підтвердження JWT-маркерів, таких як:
Щоб знайти інші чудові ресурси для роботи з JWT, ознайомтеся з http://jwt.io .
Поводження з відкликанням жетонів за допомогою JWT
Якщо ви хочете відкликати жетони, ви повинні слідкувати за ними. Вам не потрібно зберігати весь маркер на стороні сервера, зберігайте лише ідентифікатор маркера (який повинен бути унікальним) та деякі метадані, якщо вам це потрібно. Для ідентифікатора токена ви можете використовувати UUID .
jti
Вимога має бути використано для зберігання ідентифікатора маркера на маркер. Перевіряючи маркер, переконайтесь, що він не був відкликаний, перевіривши значення jti
претензії щодо ідентифікаторів жетонів, які є на стороні сервера.
З метою безпеки анулюйте всі маркери для користувача, коли він змінює свій пароль.
Додаткова інформація
- Не має значення, який тип аутентифікації ви вирішили використовувати. Завжди робіть це у верхній частині HTTPS-з'єднання, щоб запобігти атаці "людина-посеред" .
- Перегляньте це питання з інформаційної безпеки для отримання додаткової інформації про маркери.
- У цій статті ви знайдете корисну інформацію про аутентифікацію на основі токенів.
The server stores the previously generated token in some storage along with the user identifier and an expiration date. The server sends the generated token to the client.
Як це RESTful?