私はJWTが初めてです。私はここに最後の手段として来たので、ウェブにはあまり多くの情報がありません。スプリングセッションを使用するスプリングセキュリティを使用するスプリングブートアプリケーションを既に開発しました。現在、春のセッションの代わりに、JWTに移行しています。リンクがほとんど見つかりませんでしたが、ユーザーを認証してトークンを生成できるようになりました。難しい部分は、サーバーへのすべてのリクエストを認証するフィルターを作成することです。
必要なことを実行できるフィルターを次に示します。
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;
}
}
}
質問に答えるために:
/login
URI(/authenticate
私のコードで)コードの実装を考慮せずに、JWTの一般的なヒントに焦点を当てます(他の回答を参照)
フィルターはトークンをどのように検証しますか? (署名を検証するだけで十分ですか?)
RFC7519は、JWTの検証方法( 7.2。JWTの検証 を参照)、基本的には構文検証および署名検証を指定します。
JWTが認証フローで使用されている場合、OpenID接続仕様 .1.3.4 IDトークン検証 によって提案された検証を見ることができます。要約:
iss
には発行者IDが含まれています(およびaud
にはclient_id
oauthを使用する場合)
iat
とexp
の間の現在の時間
秘密鍵を使用してトークンの署名を検証します
sub
は有効なユーザーを識別します
他の誰かがトークンを盗んで残りの電話をかけた場合、どのように確認しますか。
JWTの所有は、認証の証明です。トークンを盗む攻撃者は、ユーザーになりすますことができます。トークンを安全に保つ
TLSを使用して通信チャネルを暗号化
トークンにsecure storageを使用します。 Webフロントエンドを使用する場合は、XSSまたはCSRF攻撃からlocalStorage/cookiesを保護するために、追加のセキュリティ対策を追加することを検討してください
認証トークンに短い有効期限を設定し、トークンの有効期限が切れた場合に資格情報を要求する
フィルターでログインリクエストをバイパスするにはどうすればよいですか?許可ヘッダーがないため。
ユーザー資格情報を検証するため、ログインフォームにJWTトークンは必要ありません。フォームをフィルターの範囲外にしてください。認証が成功した後にJWTを発行し、認証フィルターを残りのサービスに適用します
次に、フィルターはログインフォームを除くすべてのリクエストをインターセプトし、以下を確認します。
ユーザーが認証されたら?スローしない場合401-Unauthorized
ユーザーが要求されたリソースを承認した場合スローしない場合403-Forbidden
アクセスが許可されました。ユーザーデータをリクエストのコンテキストに配置する(例:ThreadLocalを使用)
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
にログインします。