Как создать хороший фильтр проверки подлинности JWT

Я новичок в JWT. В Интернете мало информации, так как я пришел сюда в крайнем случае. Я уже разработал приложение загрузки spring, используя spring безопасность, используя сеанс spring. Теперь вместо сеанса spring мы переходим к JWT. Я нашел несколько ссылок, и теперь я могу проверить подлинность пользователя и создать токен. Теперь сложная часть, я хочу создать фильтр, который будет аутентифицировать каждый запрос на сервер,

  • Как фильтр проверит токен? (Достаточно проверить подпись достаточно?)
  • Если кто-то еще украл токен и сделал вызов для отдыха, как я это проверю.
  • Как я буду обходить запрос входа в фильтр? Поскольку он не имеет заголовка авторизации.

Ответ 1

Вот фильтр, который может делать то, что вам нужно:

public class JWTFilter extends GenericFilterBean {

    private static final Logger LOGGER = LoggerFactory.getLogger(JWTFilter.class);

    private final TokenProvider tokenProvider;

    public JWTFilter(TokenProvider tokenProvider) {

        this.tokenProvider = tokenProvider;
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException,
        ServletException {

        try {
            HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
            String jwt = this.resolveToken(httpServletRequest);
            if (StringUtils.hasText(jwt)) {
                if (this.tokenProvider.validateToken(jwt)) {
                    Authentication authentication = this.tokenProvider.getAuthentication(jwt);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
            filterChain.doFilter(servletRequest, servletResponse);

            this.resetAuthenticationAfterRequest();
        } catch (ExpiredJwtException eje) {
            LOGGER.info("Security exception for user {} - {}", eje.getClaims().getSubject(), eje.getMessage());
            ((HttpServletResponse) servletResponse).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            LOGGER.debug("Exception " + eje.getMessage(), eje);
        }
    }

    private void resetAuthenticationAfterRequest() {
        SecurityContextHolder.getContext().setAuthentication(null);
    }

    private String resolveToken(HttpServletRequest request) {

        String bearerToken = request.getHeader(SecurityConfiguration.AUTHORIZATION_HEADER);
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            String jwt = bearerToken.substring(7, bearerToken.length());
            return jwt;
        }
        return null;
    }
}

И включение фильтра в цепочку фильтров:

public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    public final static String AUTHORIZATION_HEADER = "Authorization";

    @Autowired
    private TokenProvider tokenProvider;

    @Autowired
    private AuthenticationProvider authenticationProvider;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(this.authenticationProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        JWTFilter customFilter = new JWTFilter(this.tokenProvider);
        http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);

        // @formatter:off
        http.authorizeRequests().antMatchers("/css/**").permitAll()
        .antMatchers("/images/**").permitAll()
        .antMatchers("/js/**").permitAll()
        .antMatchers("/authenticate").permitAll()
        .anyRequest().fullyAuthenticated()
        .and().formLogin().loginPage("/login").failureUrl("/login?error").permitAll()
        .and().logout().permitAll();
        // @formatter:on
        http.csrf().disable();

    }
}

Класс TokenProvider:

public class TokenProvider {

    private static final Logger LOGGER = LoggerFactory.getLogger(TokenProvider.class);

    private static final String AUTHORITIES_KEY = "auth";

    @Value("${spring.security.authentication.jwt.validity}")
    private long tokenValidityInMilliSeconds;

    @Value("${spring.security.authentication.jwt.secret}")
    private String secretKey;

    public String createToken(Authentication authentication) {

        String authorities = authentication.getAuthorities().stream().map(authority -> authority.getAuthority()).collect(Collectors.joining(","));

        ZonedDateTime now = ZonedDateTime.now();
        ZonedDateTime expirationDateTime = now.plus(this.tokenValidityInMilliSeconds, ChronoUnit.MILLIS);

        Date issueDate = Date.from(now.toInstant());
        Date expirationDate = Date.from(expirationDateTime.toInstant());

        return Jwts.builder().setSubject(authentication.getName()).claim(AUTHORITIES_KEY, authorities)
                    .signWith(SignatureAlgorithm.HS512, this.secretKey).setIssuedAt(issueDate).setExpiration(expirationDate).compact();
    }

    public Authentication getAuthentication(String token) {

        Claims claims = Jwts.parser().setSigningKey(this.secretKey).parseClaimsJws(token).getBody();

        Collection<? extends GrantedAuthority> authorities = Arrays.asList(claims.get(AUTHORITIES_KEY).toString().split(",")).stream()
                    .map(authority -> new SimpleGrantedAuthority(authority)).collect(Collectors.toList());

        User principal = new User(claims.getSubject(), "", authorities);

        return new UsernamePasswordAuthenticationToken(principal, "", authorities);
    }

    public boolean validateToken(String authToken) {

        try {
            Jwts.parser().setSigningKey(this.secretKey).parseClaimsJws(authToken);
            return true;
        } catch (SignatureException e) {
            LOGGER.info("Invalid JWT signature: " + e.getMessage());
            LOGGER.debug("Exception " + e.getMessage(), e);
            return false;
        }
    }
}

Теперь, чтобы ответить на ваши вопросы:

  • Выполнено в этом фильтре
  • Защитите свой HTTP-запрос, используйте HTTPS
  • Просто разрешите все в URI /login (/authenticate в моем коде)

Ответ 2

Я сосредоточусь на общих советах JWT, не обращаясь к выполнению кода (см. другие ответы)

Как фильтр проверит токен? (Достаточно просто проверить подпись?)

RFC7519 определяет, как проверять JWT (см. 7.2. Проверка JWT), в основном синтаксическая проверка синтаксиса и проверка подписи /STRONG > .

Если JWT используется в потоке аутентификации, мы можем посмотреть на валидацию, предложенную спецификацией OpenID connect 3.1.3.4 Проверка токена идентификатора, Подведение итогов:

  • iss содержит идентификатор эмитента (и aud содержит client_id, если используется oauth)

  • текущее время между iat и exp

  • Подтвердить подпись маркера с помощью секретного ключа

  • sub идентифицирует действительного пользователя

Если кто-то еще украл токен и сделал вызов для отдыха, как я это подтвержу.

Помещение JWT является доказательством аутентификации. Злоумышленник, который украл токен, может олицетворять пользователя. Поэтому сохраняйте токены безопасными

  • Шифровать канал связи с помощью TLS

  • Используйте безопасное хранилище для своих токенов. Если вы используете веб-интерфейс, обратите внимание на добавление дополнительных мер безопасности для защиты localStorage/cookies от атак XSS или CSRF.

  • установите короткое время истечения срока действия на токенах аутентификации и потребуйте учетные данные, если токен истек.

Как я могу пропустить запрос на вход в фильтр? Поскольку он не имеет заголовка авторизации.

Форма входа в систему не требует токена JWT, поскольку вы собираетесь проверять учетные данные пользователя. Извлеките форму из области действия фильтра. Выполните JWT после успешной проверки подлинности и примените фильтр проверки подлинности к остальным службам.

Затем фильтр должен перехватить все запросы, кроме формы входа, и проверить:

  • если пользователь аутентифицирован? Если не выбрасывать 401-Unauthorized

  • если пользователь разрешил запрашивать ресурс? Если не выбрасывать 403-Forbidden

  • Доступ разрешен. Поместите пользовательские данные в контексте запроса (например, используя ThreadLocal).

Ответ 3

Взгляните на этот проект, он очень хорошо реализован и имеет необходимую документацию.

1. Это вышеупомянутый проект - это единственное, что нужно для проверки токена, и этого достаточно. Где token - значение Bearer в заголовке запроса.

try {
    final Claims claims = Jwts.parser().setSigningKey("secretkey")
        .parseClaimsJws(token).getBody();
    request.setAttribute("claims", claims);
}
catch (final SignatureException e) {
    throw new ServletException("Invalid token.");
}

2. Кража маркера не так проста, но по моему опыту вы можете защитить себя, создав сеанс Spring вручную для каждого успешного входа в систему. Также сопоставление уникального идентификатора сеанса и значения носителя (токена) в Карта (создание Bean, например, с областью API).

@Component
public class SessionMapBean {
    private Map<String, String> jwtSessionMap;
    private Map<String, Boolean> sessionsForInvalidation;
    public SessionMapBean() {
        this.jwtSessionMap = new HashMap<String, String>();
        this.sessionsForInvalidation = new HashMap<String, Boolean>();
    }
    public Map<String, String> getJwtSessionMap() {
        return jwtSessionMap;
    }
    public void setJwtSessionMap(Map<String, String> jwtSessionMap) {
        this.jwtSessionMap = jwtSessionMap;
    }
    public Map<String, Boolean> getSessionsForInvalidation() {
        return sessionsForInvalidation;
    }
    public void setSessionsForInvalidation(Map<String, Boolean> sessionsForInvalidation) {
        this.sessionsForInvalidation = sessionsForInvalidation;
    }
}

Этот SessionMapBean будет доступен для всех сеансов. Теперь по каждому запросу вы не только проверите токен, но также проверите, соответствует ли он сеансу (проверка идентификатора сеанса запроса соответствует тому, который хранится в SessionMapBean). Конечно, идентификатор сеанса может быть также украден, поэтому вам необходимо обеспечить связь. Наиболее распространенными способами кражи идентификатора сеанса являются Sniffing сеанса (или Men in the middle) и Cross-site script attack. Я не буду более подробно рассказывать о них, вы можете прочитать, как защитить себя от таких атак.

3. Вы можете увидеть его в проекте, который я связал. Самый простой фильтр будет проверять все /api/*, и вы войдете в /user/login, например.