Рубрики
Без рубрики

Начало работы с Spring Security – Добавление JWT

Это вторая часть поста spring security, который я начал. Веб-токен Json: стандарт, который определяет… С тегами java, security, jwt, spring.

Это вторая часть поста 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 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 getAuthorities(String token) {
        Claims claims = Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
        return (Collection) 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”