Всем привет! Хабр жив! Данный пост вряд ли соберёт кучу просмотров и комментов, но, надеюсь, немного поможет здоровью хабра.
В данной статье рассмотрим принцип аутентификации в веб-приложениях на платформе 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 завершена.
Желаю всем успехов!
Спасибо за внимание!
