web-dev-qa-db-ja.com

優れたJWT認証フィルターを設計する方法

私はJWTが初めてです。私はここに最後の手段として来たので、ウェブにはあまり多くの情報がありません。スプリングセッションを使用するスプリングセキュリティを使用するスプリングブートアプリケーションを既に開発しました。現在、春のセッションの代わりに、JWTに移行しています。リンクがほとんど見つかりませんでしたが、ユーザーを認証してトークンを生成できるようになりました。難しい部分は、サーバーへのすべてのリクエストを認証するフィルターを作成することです。

  1. フィルターはトークンをどのように検証しますか? (署名を検証するだけで十分ですか?)
  2. 他の誰かがトークンを盗み、残りの電話をかける場合、どのように確認しますか。
  3. フィルターでログインリクエストをバイパスするにはどうすればよいですか?許可ヘッダーがないため。
30
arunan

必要なことを実行できるフィルターを次に示します。

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;
        }
    }
}

質問に答えるために:

  1. このフィルターで完了
  2. HTTPリクエストを保護し、HTTPSを使用します
  3. /login URI(/authenticate私のコードで)
23
Matthieu Saleta

コードの実装を考慮せずに、JWTの一般的なヒントに焦点を当てます(他の回答を参照)

フィルターはトークンをどのように検証しますか? (署名を検証するだけで十分ですか?)

RFC7519は、JWTの検証方法( 7.2。JWTの検証 を参照)、基本的には構文検証および署名検証を指定します。

JWTが認証フローで使用されている場合、OpenID接続仕様 .1.3.4 IDトークン検証 によって提案された検証を見ることができます。要約:

  • issには発行者IDが含まれています(およびaudにはclient_id oauthを使用する場合)

  • iatexpの間の現在の時間

  • 秘密鍵を使用してトークンの署名を検証します

  • subは有効なユーザーを識別します

他の誰かがトークンを盗んで残りの電話をかけた場合、どのように確認しますか。

JWTの所有は、認証の証明です。トークンを盗む攻撃者は、ユーザーになりすますことができます。トークンを安全に保つ

  • TLSを使用して通信チャネルを暗号化

  • トークンにsecure storageを使用します。 Webフロントエンドを使用する場合は、XSSまたはCSRF攻撃からlocalStorage/cookiesを保護するために、追加のセキュリティ対策を追加することを検討してください

  • 認証トークンに短い有効期限を設定し、トークンの有効期限が切れた場合に資格情報を要求する

フィルターでログインリクエストをバイパスするにはどうすればよいですか?許可ヘッダーがないため。

ユーザー資格情報を検証するため、ログインフォームにJWTトークンは必要ありません。フォームをフィルターの範囲外にしてください。認証が成功した後にJWTを発行し、認証フィルターを残りのサービスに適用します

次に、フィルターはログインフォームを除くすべてのリクエストをインターセプトし、以下を確認します。

  1. ユーザーが認証されたら?スローしない場合401-Unauthorized

  2. ユーザーが要求されたリソースを承認した場合スローしない場合403-Forbidden

  3. アクセスが許可されました。ユーザーデータをリクエストのコンテキストに配置する(例:ThreadLocalを使用)

7
pedrofb

this プロジェクトを見てください。非常にうまく実装されており、必要なドキュメントがあります。

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セッションを作成することで身を守ることができます。また、セッションの一意のIDとベアラー値(トークン)をMap(たとえばAPIスコープを持つBeanを作成する)。

@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は、すべてのセッションで使用できます。これで、すべてのリクエストでトークンを検証するだけでなく、セッションを計算するかどうかも確認します(リクエストセッションIDがSessionMapBeanに保存されているものと一致するかどうかを確認します)。もちろん、セッションIDも盗まれる可能性があるため、通信を保護する必要があります。セッションIDを盗む最も一般的な方法は、セッションスニッフィング(または中間の男性)およびクロスサイトスクリプト攻撃。この種の攻撃から身を守る方法を読むことができる、それらについてはこれ以上詳しく述べません。

3。リンクしたプロジェクトで確認できます。最も単純には、フィルターはすべての/api/*を検証し、たとえば/user/loginにログインします。

1
Lazar Lazarov