Всем привет! Хабр жив! Этот пост вряд ли наберет массу просмотров и комментариев, но я надеюсь, что он немного поможет здоровью хаба.
В этой статье мы рассмотрим принцип аутентификации в веб-приложениях на платформе Spring с использованием относительно нового механизма аутентификации — Веб-токен JSON (JWT) .
Этот механизм уже протестирован и реализован для многих языков программирования.
Использование токена позволяет серверу не беспокоиться о сохранении состояния между запросами (HTTP-сессиями) и уменьшает количество запросов к базе данных — необходимые для восстановления данные могут храниться в токене.
Немного о токене JWT: сервер смешивает полезную нагрузку JSON (заголовок и тело) с секретным ключом и генерирует хеш, прикрепляя его в качестве подписи к полезной нагрузке.
Полезная нагрузка закодирована в формате Base64Url, поэтому, естественно, вам не следует передавать секретные данные в токен.
Стандарт JWT не предусматривает шифрование полезной нагрузки.
Если хотите, зашифруйте отдельно сами, а задача токена — только обеспечить аутентификацию.
Предполагается, что читатель знаком с основами Spring Security. Вы можете прочитать о нем Здесь
1).
Генерация токенов Для моего примера я взял один из реализации Спецификации JWT. Токен генерируется следующим образом:
В результате мы получаем строку вида .package com.example.security; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.impl.crypto.MacProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.stereotype.Service; import java.util.*; @Service public class GetTokenServiceImpl implements GetTokenService { @Autowired private UserDetailsService userDetailsService; @Override public TokenObject getToken(String username, String password) throws Exception { if (username == null || password == null) return null; User user = (User) userDetailsService.loadUserByUsername(username); Map<String, Object> tokenData = new HashMap<>(); if (password.equals(user.getPassword())) { tokenData.put("clientType", "user"); tokenData.put("userID", user.getUserId().
toString()); tokenData.put("username", authorizedUser.getUsername()); tokenData.put("token_create_date", new Date().
getTime()); Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.YEAR, 100); tokenData.put("token_expiration_date", calendar.getTime()); JwtBuilder jwtBuilder = Jwts.builder(); jwtBuilder.setExpiration(calendar.getTime()); jwtBuilder.setClaims(tokenData); String key = "abc123"; String token = jwtBuilder.signWith(SignatureAlgorithm.HS512, key).
compact(); return token; } else { throw new Exception("Authentication error"); } } }
.
, который мы отправляем клиенту Теперь о Spring Security. Чтобы реализовать наш собственный механизм аутентификации, нам нужно реализовать свой собственный фильтр И менеджер аутентификации .
2).
Реализация фильтра Фильтр — это объект класса, реализующий интерфейс.
javax.servlet.Фильтр , который перехватывает запросы определенных URL-адресов и выполняет некоторые действия.
Если фильтров несколько, то они образуют цепочку фильтров — HTTP-запрос после получения приложением проходит через эту цепочку.
Каждый фильтр в цепочке может обработать запрос, передать его следующим фильтрам в цепочке или не передать, сразу отправив ответ клиенту.
Задача нашего фильтра — передать токен из запроса менеджеру аутентификации и, если аутентификация прошла успешно, установить контекст безопасности приложения.
package com.example.security;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class TokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public TokenAuthenticationFilter() {
super("/rest/**");
setAuthenticationSuccessHandler((request, response, authentication) ->
{
SecurityContextHolder.getContext().
setAuthentication(authentication); request.getRequestDispatcher(request.getServletPath() + request.getPathInfo()).
forward(request, response); }); setAuthenticationFailureHandler((request, response, authenticationException) -> { response.getOutputStream().
print(authenticationException.getMessage()); }); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { String token = request.getHeader("token"); if (token == null) token = request.getParameter("token"); if (token == null) { TokenAuthentication authentication = new TokenAuthentication(null, null); authentication.setAuthenticated(false); return authentication; } TokenAuthentication tokenAuthentication = new TokenAuthentication(token); Authentication authentication = getAuthenticationManager().
authenticate(tokenAuthentication);
return authentication;
}
@Override
public void doFilter(ServletRequest req, ServletResponse res,
FilterChain chain) throws IOException, ServletException {
super.doFilter(req, res, chain);
}
}
Мы унаследовали от абстрактного класса org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter , который специально разработан для аутентификации.
Если URL-адрес запроса соответствует шаблону "/отдых/**" функция будет вызвана автоматически попыткаАутентификация() .
Также мы установили в конструктор два обработчика — Обработчик успеха аутентификации И Обработчик сбоя аутентификации .
Если попытка аутентификации вернет объект Аутентификация , тогда сработает первый обработчик, второй обработчик сработает при вызове метода попытка аутентификации исключения Исключение аутентификации .
Как мы видим, после успешной аутентификации мы устанавливаем контекст безопасности приложения с помощью SecurityContextHolder.getContext().
setAuthentication(аутентификация) .
Контекст, установленный таким образом, является переменной ThreadLocal , т.е.
доступен до тех пор, пока жив поток работы с клиентом.
После установки контекста мы направляем запрос пользователя на сервлет с первоначально запрошенным URL-адресом.
3).
Менеджер аутентификации.
Менеджер аутентификации — это объект класса, реализующий интерфейс org.springframework.security.authentication.AuthenticationManager только с одним методом аутентифицировать() .
Этому методу необходимо передать частично заполненный объект, реализующий интерфейс.
org.springframework.security.core.Authentication (контекст безопасности приложения).
Задача менеджера аутентификации — заполнить весь объект в случае успешной аутентификации.
Аутентификация и верните его.
При заполнении необходимо указать пользователя ( главный ), его права ( власти ), выполнять setAuthenticated (истина) .
Если это не удается, менеджер аутентификации должен выдать исключение.
Исключение аутентификации .
Приведем пример реализации интерфейса org.springframework.security.core.Authentication : package com.example.security;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
import java.util.List;
import java.util.Map;
public class TokenAuthentication implements Authentication {
private String token;
private Collection<? extends GrantedAuthority> authorities;
private boolean isAuthenticated;
private UserDetails principal;
public TokenAuthentication(String token) {
this.token = token;
this.details = request;
}
public TokenAuthentication(String token, Collection<SimpleGrantedAutority> authorities, boolean isAuthenticated,
UserDetails principal) {
this.token = token;
this.authorities = authorities;
this.isAuthenticated = isAuthenticated;
this.principal = principal;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getDetails() {
return details;
}
@Override
public String getName() {
if (principal != null)
return ((UserDetails) principal).
getUsername();
else
return null;
}
@Override
public Object getPrincipal() {
return principal;
}
@Override
public boolean isAuthenticated() {
return isAuthenticated;
}
@Override
public void setAuthenticated(boolean b) throws IllegalArgumentException {
isAuthenticated = b;
}
public String getToken() {
return token;
}
}
Вот реализация менеджера аутентификации: package com.example.security;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.impl.DefaultClaims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.authentication.AuthenticationServiceException
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;
import org.springframework.security.core.GrantedAuthority;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
@Service
public class TokenAuthenticationManager implements AuthenticationManager {
@Autowired
private UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
try {
if (authentication instanceof TokenAuthentication) {
TokenAuthentication readyTokenAuthentication = processAuthentication((TokenAuthentication) authentication);
return readyTokenAuthentication;
} else {
authentication.setAuthenticated(false);
return authentication;
}
} catch (Exception ex) {
if(ex instanceof AuthenticationServiceException)
throw ex;
}
}
private TokenAuthentication processAuthentication(TokenAuthentication authentication) throws AuthenticationException {
String token = authentication.getToken();
String key = "key123";
DefaultClaims claims;
try {
claims = (DefaultClaims) Jwts.parser().
setSigningKey(key).
parse(token).
getBody();
} catch (Exception ex) {
throw new AuthenticationServiceException("Token corrupted");
}
if (claims.get("TOKEN_EXPIRATION_DATE", Long.class) == null)
throw new AuthenticationServiceException("Invalid token");
Date expiredDate = new Date(claims.get("TOKEN_EXPIRATION_DATE", Long.class));
if (expiredDate.after(new Date()))
return buildFullTokenAuthentication(authentication, claims);
else
throw new AuthenticationServiceException("Token expired date error");
}
private TokenAuthentication buildFullTokenAuthentication(TokenAuthentication authentication, DefaultClaims claims) {
User user = (User) userDetailsService.loadUserByUsername(claims.get("USERNAME", String.class));
if (user.isEnabled()) {
Collection<GrantedAutority> authorities = user.getAuthorities();
TokenAuthentication fullTokenAuthentication =
new TokenAuthentication(authentication.getToken(), authorities, true, user);
return fullTokenAuthentication;
} else {
throw new AuthenticationServiceException("User disabled");;
}
}
}
4).
Как собрать все это вместе Во-первых, необходимо установить фильтр.
Вы можете сделать это двумя способами Первый способ — определить фильтр в файле веб.
xml наше приложение <filter>
<filter-name>springSecurityTokenFilter</filter-name>
<filter-class>com.example.security.TokenAuthenticationFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityTokenFilter</filter-name>
<url-pattern>/rest/**</url-pattern>
</filter-mapping>
При использовании этого метода необходимо сразу указать менеджер аутентификации в конструкторе фильтра, поскольку экземпляр фильтра не будет доступен в контексте приложения Spring. Если вам нужен фильтр или менеджер аутентификации в качестве bean-компонентов Spring, вам нужно использовать второй метод.
Второй способ — установить фильтр в конфигурации Spring Security.
Например, давайте покажем конфигурацию с помощью Java Config. package com.example.security;
import com.example.security.RestTokenAuthenticationFilter;
import com.example.security.TokenAuthenticationManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
@Qualifier("userDetailsService")
UserDetailsService userDetailsService;
@Autowired
TokenAuthenticationManager tokenAuthenticationManager;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.
headers().
frameOptions().
sameOrigin() .
and() .
addFilterAfter(restTokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) .
authorizeRequests() .
antMatchers("/rest/*").
authenticated()
}
@Bean(name = "restTokenAuthenticationFilter")
public RestTokenAuthenticationFilter restTokenAuthenticationFilter() {
RestTokenAuthenticationFilter restTokenAuthenticationFilter = new RestTokenAuthenticationFilter();
tokenAuthenticationManager.setUserDetailsService(userDetailsService);
restTokenAuthenticationFilter.setAuthenticationManager(tokenAuthenticationManager);
return restTokenAuthenticationFilter;
}
}
В соответствии .
addFilterAfter(restTokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) мы добавили наш фильтр в цепочку фильтров после стандартный фильтр Имя пользователяПарольAuthenticationFilter .
На этом базовая настройка механизма аутентификации Spring Security с использованием JSON Web Token завершена.
Желаю всем успехов! Спасибо за внимание! Теги: #java #spring Security #аутентификация #токен #java
-
Обзор Hp Mini 5103
19 Oct, 24 -
Хостинг Для Умных Людей №3
19 Oct, 24