Всем привет! Хабр жив! Данный пост вряд ли соберёт кучу просмотров и комментов, но, надеюсь, немного поможет здоровью хабра.
В данной статье рассмотрим принцип аутентификации в веб-приложениях на платформе Spring с использованием относительно нового механизма аутентификации — JSON Web Token (JWT). Этот механизм уже обкатан и реализован для многих языков программирования.
Использование токена позволяет серверу не заботиться о сохранении состояния между запросами (HTTP-сессии), уменьшить количество запросов к БД — необходимые для восстановления данные могут сохраняться в токене. Непосредственно о токене JWT: сервер смешивает полезную нагрузку в формате JSON (заголовок и тело) с секретным ключом и генерирует хэш, прикрепляя его в качестве сигнатуры к полезной нагрузке. Полезная нагрузка кодируется алгоритмом base64Url, поэтому, естественно, не следует передавать в токене секретные данные. Стандартом JWT шифрование полезной нагрузки не предусмотрено. Шифруйте отдельно сами, если хотите, а задача токена — только обеспечить аутентификацию.
Предполагается, что читатель знаком с основами Spring Secutity. Про него можно прочитать здесь
Для своего примера я взял одну из реализаций спецификации JWT. Токен генерируется следующим образом:
В итоге мы получаем строку вида <Заголовок>.<Тело>.<Сигнатура>, которую и отправляем клиенту
Теперь к Spring Security. Для реализации собственного механизма аутентификации нам необходимо реализовать свой фильтр и менеджер аутентификации.
Фильтр — это объект класса, реализующего интерфейс javax.servlet.Filter, который перехватывает запросы на определённые URL и выполняет некоторые действия. Если имеется несколько фильтров, то они образуют цепочку фильтров — HTTP-запрос после приёма приложением проходит через эту цепочку. Каждый фильтр в цепочке может обработать запрос, пропустить его к следующим фильтрам в цепочке или не пропустить, сразу отправив ответ клиенту.
Задача нашего фильтра — передать токен из запроса менеджеру аутентификации и, в случае успешной аутентификации, установить контекст безопасности приложения.
Мы унаследовались от абстрактного класса org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter, который специально предназначен для аутентификации. При совпадении URL запроса с паттерном "/rest/**" автоматом произойдёт вызов функции attemptAuthentication().
Также в конструкторе мы установили два хэндлэра — AuthenticationSuccessHandler и AuthenticationFailureHandler. Если attemptAuthentication вернет об��ект Authentication, то сработает первый хэндлер, второй хэндлэр сработает при выбросе методом attemptAuthentication исключения AuthenticationException.
Как мы видим, при успешной аутентификации мы устанавливаем контекст безопасности приложения посредством SecurityContextHolder.getContext().setAuthentication(authentication). Установленный таким образом контекст является переменной ThreadLocal, т.е. доступен, пока жив поток работы с клиентом. После установки контекста мы направляем запрос пользователя к сервлету с первоначально запрашиваемым URL.
Менеджер аутентификации — это объект класса, реализующего интерфейс org.springframework.security.authentication.AuthenticationManager с единственным методом authenticate(). Данному методу нужно передать частично заполненный объект, реализующий интерфейс org.springframework.security.core.Authentication (контекстом безопасности приложения).
Задача менеджера аутентификации — в случае успешной аутентификации заполнить полностью объект Authentication и вернуть его. При заполнении нужно установить пользователя (principal), его права (authorities), выполнить setAuthenticated(true). В случае неудачи менеджер аутентификации должен выбросить исключение AuthenticationException.
Приведём пример реализации интерфейса org.springframework.security.core.Authentication:
Приведём реализацию менеджера аутентификации:
Во-первых, нужно установить фильтр. Сделать это можно 2-мя способами
Первый способ — определить фильтр в файле web.xml нашего приложения
При таком способе в конструкторе фильтра нужно сразу задать менеджер аутентификации, так как экземпляр фильтра не будет доступен в контексте приложения Spring. Если необходимо иметь фильтр или менеджер аутентификации в качестве бинов Spring, нужно воспользоваться вторым способом.
Второй способ — установка фильтра в конфигурации Spring Security.
Для примера покажем конфигурацию с использованием Java Config
В строке
.addFilterAfter(restTokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
мы добавили наш фильтр в цепочку фильтров после стандартного фильтра UsernamePasswordAuthenticationFilter.
На этом основная настройка механизма аутентификации в Spring Security с использованием JSON Web Token завершена.
Желаю всем успехов!
Спасибо за внимание!
В данной статье рассмотрим принцип аутентификации в веб-приложениях на платформе Spring с использованием относительно нового механизма аутентификации — JSON Web Token (JWT). Этот механизм уже обкатан и реализован для многих языков программирования.
Использование токена позволяет серверу не заботиться о сохранении состояния между запросами (HTTP-сессии), уменьшить количество запросов к БД — необходимые для восстановления данные могут сохраняться в токене. Непосредственно о токене JWT: сервер смешивает полезную нагрузку в формате JSON (заголовок и тело) с секретным ключом и генерирует хэш, прикрепляя его в качестве сигнатуры к полезной нагрузке. Полезная нагрузка кодируется алгоритмом base64Url, поэтому, естественно, не следует передавать в токене секретные данные. Стандартом JWT шифрование полезной нагрузки не предусмотрено. Шифруйте отдельно сами, если хотите, а задача токена — только обеспечить аутентификацию.
Предполагается, что читатель знаком с основами Spring Secutity. Про него можно прочитать здесь
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.Filter, который перехватывает запросы на определённые 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 запроса с паттерном "/rest/**" автоматом произойдёт вызов функции attemptAuthentication().
Также в конструкторе мы установили два хэндлэра — AuthenticationSuccessHandler и AuthenticationFailureHandler. Если attemptAuthentication вернет об��ект Authentication, то сработает первый хэндлер, второй хэндлэр сработает при выбросе методом attemptAuthentication исключения AuthenticationException.
Как мы видим, при успешной аутентификации мы устанавливаем контекст безопасности приложения посредством SecurityContextHolder.getContext().setAuthentication(authentication). Установленный таким образом контекст является переменной ThreadLocal, т.е. доступен, пока жив поток работы с клиентом. После установки контекста мы направляем запрос пользователя к сервлету с первоначально запрашиваемым URL.
3). Менеджер аутентификации.
Менеджер аутентификации — это объект класса, реализующего интерфейс org.springframework.security.authentication.AuthenticationManager с единственным методом authenticate(). Данному методу нужно передать частично заполненный объект, реализующий интерфейс org.springframework.security.core.Authentication (контекстом безопасности приложения).
Задача менеджера аутентификации — в случае успешной аутентификации заполнить полностью объект Authentication и вернуть его. При заполнении нужно установить пользователя (principal), его права (authorities), выполнить setAuthenticated(true). В случае неудачи менеджер аутентификации должен выбросить исключение AuthenticationException.
Приведём пример реализации интерфейса 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). Как всё это собрать вместе
Во-первых, нужно установить фильтр. Сделать это можно 2-мя способами
Первый способ — определить фильтр в файле web.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. Если необходимо иметь фильтр или менеджер аутентификации в качестве бинов 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)
мы добавили наш фильтр в цепочку фильтров после стандартного фильтра UsernamePasswordAuthenticationFilter.
На этом основная настройка механизма аутентификации в Spring Security с использованием JSON Web Token завершена.
Желаю всем успехов!
Спасибо за внимание!
