Это вторая часть поста spring security, который я начал.
Json Web Токен: стандарт, который определяет автономный способ передачи информации в виде объекта JSON. Состоит из трех частей, разделенных точками.
- Заголовок: алгоритм подписи (SHA256,HS512…) + тип токена
- Полезная нагрузка: содержит утверждения.
- Подпись: заголовок (в кодировке base64) + полезная нагрузка (в кодировке base64) + секрет, и все это закодировано с помощью алгоритма, указанного в заголовке.
Утверждение: часть информации в теле токена.
Прежде всего, добавьте зависимость , которая позволяет нам создавать jwt и проверять их.
io.jsonwebtoken jjwt 0.9.1
Аутентификация пользователя и возврат токена
Мы собираемся создать класс, который имеет все связанные с jwt функции. В следующем примере при создании токена я просто добавляю субъекта, полномочия этого пользователя (у нас есть только один, и это ROLE_SENSEI, проверьте MyUserDetails class) и время истечения срока действия. Вы можете добавлять пользовательские утверждения с помощью claim(ключ, значение) или передайте карту утверждений в set Claims() . Я подписываю токен строкой “key” для этого примера. В реальном проекте он может быть извлечен из файла конфигурации приложения.
Чтобы прочитать jwt, вам нужно передать ключ для проверки подписи токена и вызвать parse Claims/|/с помощью токена, тогда вы сможете получить тело.
@Service public class JwtService { private static final int EXPIRATION_TIME = 1000 * 60 * 60; private static final String AUTHORITIES = "authorities"; private final String SECRET_KEY; public JwtService() { SECRET_KEY = Base64.getEncoder().encodeToString("key".getBytes()); } public String createToken(UserDetails userDetails) { String username = userDetails.getUsername(); Collection extends GrantedAuthority> authorities = userDetails.getAuthorities(); return Jwts.builder() .setSubject(username) .claim(AUTHORITIES, authorities) .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) .signWith(SignatureAlgorithm.HS512, SECRET_KEY) .compact(); } public Boolean hasTokenExpired(String token) { return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody() .getExpiration() .before(new Date()); } public Boolean validateToken(String token, UserDetails userDetails) { String username = extractUsername(token); return (userDetails.getUsername().equals(username) && !hasTokenExpired(token)); } public String extractUsername(String token) { return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody() .getSubject(); } public Collection extends GrantedAuthority> getAuthorities(String token) { Claims claims = Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody(); return (Collection extends GrantedAuthority>) claims.get(AUTHORITIES); } }
Когда пользователь пытается войти в систему, мы ожидаем ввода имени пользователя и пароля (запрос аутентификации пользователя), и если аутентификация пройдет успешно, мы ответим токеном (AuthenticationResponse).
@Data @NoArgsConstructor @AllArgsConstructor public class AuthenticationRequest { private String username; private String password; }
@Data @NoArgsConstructor @AllArgsConstructor public class AuthenticationResponse { private String token; }
В нашем контроллере мы автоматически подключаем диспетчер аутентификации, чтобы мы могли аутентифицировать переданный объект. Нам нужно создать объект аутентификации с помощью UsernamePasswordAuthenticationToken , он получает два параметра: основной (имя пользователя) и учетные данные (пароль). Этот объект передается поставщику аутентификации, который отвечает за выполнение проверки. Если проверка прошла успешно, токен создается, в противном случае генерируется исключение.
@RestController @RequiredArgsConstructor public class UserController { private final AuthenticationManager authenticationManager; private final MyUserDetailService myUserDetailService; private final JwtService jwtService; @PostMapping("/login") public AuthenticationResponse createToken(@RequestBody AuthenticationRequest authenticationRequest) throws Exception { try { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(), authenticationRequest.getPassword()); authenticationManager.authenticate(authentication); } catch (BadCredentialsException e) { throw new Exception("Invalid username or password", e); } UserDetails userDetails = myUserDetailService.loadUserByUsername(authenticationRequest.getUsername()); String token = jwtService.createToken(userDetails); return new AuthenticationResponse(token); } }
Мой класс конфигурации безопасности выглядит следующим образом. Вам необходимо переопределить authenticationManagerBean, чтобы автоматически подключить его. Здесь я разрешаю всем /войти в систему но для любого другого ресурса вы должны пройти аутентификацию.
@EnableWebSecurity public class WebSecurity extends WebSecurityConfigurerAdapter { @Autowired private MyUserDetailService myUserDetailService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(myUserDetailService); } @Override public void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests().antMatchers("/login").permitAll() .anyRequest().authenticated(); } @Bean public PasswordEncoder getPasswordEncoder() { return NoOpPasswordEncoder.getInstance(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
Помните, что у меня есть тот же класс UserDetails, что и в предыдущем посте, поэтому пароль – “pass”, а имя пользователя может быть любым. Итак, если мы попытаемся войти в систему, мы увидим возвращенный токен.
Запрос на прерывание
Контекст безопасности используется для хранения сведений о текущем аутентифицированном пользователе.
Теперь мы собираемся извлечь токен из заголовка авторизации и проверить его. Чтобы перехватить запрос, мы используем фильтры. Сначала мы создаем фильтр авторизации Jwt, который будет выполняться один раз для каждого запроса и отвечает за авторизацию пользователя. Теперь мы получаем токен из заголовка, извлекаем имя пользователя и проверяем, что токен действителен. Если все в порядке, мы создаем объект аутентификации с этими данными пользователя, устанавливаем пользователя в контексте безопасности и разрешаем запросу двигаться дальше с помощью FilterChain.доФильтр . Я определил свои константы как поля класса, но было бы лучше создать класс ‘JwtConstants’ и иметь их все там.
@Component public class JwtAuthorizationFilter extends OncePerRequestFilter { private static final String HEADER_TOKEN_PREFIX = "Bearer "; private static final String HEADER_AUTHORIZATION = "Authorization"; @Autowired private MyUserDetailService myUserDetailService; @Autowired private JwtService jwtService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String authorizationHeader = request.getHeader(HEADER_AUTHORIZATION); if (authorizationHeader != null && authorizationHeader.startsWith(HEADER_TOKEN_PREFIX)) { String token = authorizationHeader.replace(HEADER_TOKEN_PREFIX, ""); String username = jwtService.extractUsername(token); UserDetails userDetails = myUserDetailService.loadUserByUsername(username); if (jwtService.validateToken(token, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } } filterChain.doFilter(request, response); } }
В нашем классе веб-безопасности мы добавим управление сеансами без состояния, потому что мы не хотим, чтобы spring создавал какой-либо сеанс. Во-вторых, мы добавим созданный фильтр. Это означает, что прочитайте фильтр авторизации Jwt перед фильтром UsernamePasswordFilter.
@Autowired private JwtAuthorizationFilter jwtAuthorizationFilter; //... @Override public void configure(HttpSecurity http) throws Exception { http .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests().antMatchers("/login").permitAll() .anyRequest().authenticated() .and().addFilterBefore(jwtAuthorizationFilter, UsernamePasswordAuthenticationFilter.class); } //...
Давайте попробуем это на postman, я создал это приложение, чтобы проверить, что мы сделали.
@GetMapping("/test") public String getTest(){ return "test"; }
Чтобы проверить отсутствие гражданства, попробуйте сделать запрос снова без заголовка Authorization , вы увидите, как вы получите 403 Forbidden , потому что каждый запрос является автономным.
Оригинал: “https://dev.to/jhonifaber/getting-started-with-spring-security-adding-jwt-485c”