Angular-JWTトークンを使用して安全なGraphQL APIクエリを実装する

こんにちはHabr! Angularプロジェクトを実装するとき、Angular 4のgraphqlクエリのセキュリティに関して疑問が生じました。選択はJSON Web Tokensにかかっていました。 これはRFC 7519のオープンスタンダードです。



JWTは、次のスキームに従って機能します。

画像



私は比較的最近、半年前にプログラミングとAngularの学習を始めました。そして、素朴なチュクチの子供です。 したがって、コードとロジックに関する批判は、フレンドリーなアドバイスとして受け入れます。



graphqlクライアントでは、 apollo-angulardocsgithub )を使用し、GraphQL APIへの各リクエストのヘッダーにJWTトークンが必要でした。



AuthService認証サービスを作成します。 プライマリトークンの取得は、RESTを介して実装されます。



login(username: string, password: string){ let headers = new Headers({ "content-type": "application/json;charset=utf-8"}); let options = new RequestOptions({ headers: headers }); return this.http.post('http://localhost:8080/login', ({ username: username, password: password }), options) .map((res : any) => { if (res.status === 200) { this.commonToken = res.json(); let data = this.commonToken; this.accessToken = JSON.stringify(data.accessToken); this.refreshToken = JSON.stringify(data.refreshToken); sessionStorage.setItem('accessToken', this.accessToken); sessionStorage.setItem('refreshToken', this.refreshToken); return true; } }) };
      
      





accessTokenを取得して、ブラウザのsessionStorageに書き込みます。



ここでは、タブ/ブラウザが閉じられるまでsessionStorageが存続し、ユーザーがそれを閉じると、すべてのコンテンツがリセットされ、その結果、トークンが失われることに注意する価値があります。 代替: localStorageまたはcookies この場合、トークンは手動で削除されるまでユーザーに残ります。

ただし、落とし穴があります。 この記事でどんな種類の石が読めるか。
まだrefreshTokenがあります。 彼については少し後で。



次に、APIを操作するクライアントが必要です。 apollo-clientを使用します。



 import ApolloClient, { createNetworkInterface } from 'apollo-client'; const networkInterface = createNetworkInterface({ uri: 'http://localhost:8080/graphql', opts: { mode: 'cors' } }); networkInterface.use([ { applyMiddleware(req, next) { if (!req.options.headers) { req.options.headers = {}; } if (sessionStorage.getItem('accessToken')) { req.options.headers['authorization'] = `${JSON.parse(sessionStorage.getItem('accessToken'))}`; } next(); } } ]); const apolloClient = new ApolloClient({ networkInterface }); export function provideClient(): ApolloClient { return apolloClient; } export class GraphqlClient{}
      
      





このコードでは、sessionStorageからトークンを取得し、 承認ヘッダーに書き込みます。

Apollo-clientにはnetworkInterface用のメソッドが2つあります: ミドルウェアとアフターウェアです。 私たちの場合、ミドルウェアが使用されました。その目的は、APIにリクエストを送信する前に特定のパラメーターを適用することです。



そしてもう1つ重要な瞬間です。 optsパラメーターでは、 モード: 'cors'が指定されます 。 これは、バックエンドがスピンしているSpring Securityで行われます。バックエンドにクロスオリジンHTTPリクエストフィルターがない場合、MODを「no-cors」に切り替えることができます。



これで、apollo-clientを通過するすべてのリクエストまたはミューテーションは、ヘッダーにjwtトークンを持ちます。 バックエンドで、このトークンの有効性と実行可能性のチェックが実装されます。 コードは私のものではありません。



  private TokenAuthentication processAuthentication(TokenAuthentication authentication) throws AuthenticationException { String token = authentication.getToken(); DefaultClaims claims; try { claims = (DefaultClaims) Jwts.parser().setSigningKey(DefaultTokenService.KEY).parse(token).getBody(); } catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException | SignatureException ex) { throw new AuthenticationServiceException("Invalid JWT token:", ex); } catch (ExpiredJwtException expiredEx) { throw new AuthenticationServiceException("JWT Token expired", expiredEx); } return buildFullTokenAuthentication(authentication, claims); if (claims.get("TOKEN_EXPIRATION_DATE", Long.class) == null) throw new AuthenticationServiceException("Invalid tokens"); Date expiredDate = new Date(claims.get("TOKEN_EXPIRATION_DATE", Long.class)); if (expiredDate.after(new Date())) return buildFullTokenAuthentication(authentication, claims); else throw new AuthenticationServiceException("Token expired date error"); } private TokenAuthentication buildFullTokenAuthentication(TokenAuthentication authentication, DefaultClaims claims) { String username = claims.get("username", String.class); Long userId = Long.valueOf(claims.get("userId", String.class)); String auth = claims.get("authorities", String.class); if(Roles.REFRESH_TOKEN == auth) { throw new AuthenticationServiceException("Refresh token can't be used for authorization!!!"); } List<SimpleGrantedAuthority> authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority(auth)); TokenAuthentication fullTokenAuthentication = new TokenAuthentication(authentication.getToken(), true, authorities, username, userId); return fullTokenAuthentication; }
      
      





さて、refreshTokenについて。 refreshTokenタスクは、非推奨のaccessTokenを更新することです。



実装は異なる場合があります。AngularのAuthGuardサービスでのプリミティブチェックから始まり、特定の時間間隔でトークンを更新するスケジューラーサービスで終わります。 私の場合、最初のオプションが作成されました。 よりスマートなオプションを考えるとき、それを実装します。 これまでのところ、私はそうすることができました。



そのため、 AuthGuard検証サービスがトークンの有効期限が切れたことを通知した場合に呼び出されるメソッドをAuthServiceサービスに作成します。



 refresh() { let token = sessionStorage.getItem('accessToken'); let refToken = sessionStorage.getItem('refreshToken'); let headers = new Headers({ "content-type": "application/x-www-form-urlencoded"}); let options = new RequestOptions({headers: headers}); let body = new URLSearchParams(); body.set('RefreshToken', refToken); if (token != null && refToken != null) { return this.http.post('http://localhost:8080/login/refresh', body, options) .subscribe((res : any) => { if (res) { this.commonToken = res.json(); let data = this.commonToken; this.accessToken = JSON.stringify(data.accessToken); sessionStorage.setItem('accessToken', this.accessToken); } }) } else { console.error('An error occurred'); } }
      
      





次に、 AuthGuard検証サービス自体を作成します



 import { Injectable } from '@angular/core'; import {Router, CanActivate, RouterStateSnapshot, ActivatedRouteSnapshot} from '@angular/router'; import {JwtHelper} from "angular2-jwt"; import {AuthService} from "./auth.service"; @Injectable() export class AuthGuard implements CanActivate { jwtHelper: JwtHelper = new JwtHelper(); constructor(private authService: AuthService, private router: Router) { } canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { let accessToken = sessionStorage.getItem('accessToken'); let refreshToken = sessionStorage.getItem('refreshToken'); if (accessToken && refreshToken) { if (this.jwtHelper.isTokenExpired(accessToken)){ this.authService.refresh() } else { return true } } this.router.navigateByUrl('/unauthorized'); } }
      
      





ここでは、 angular2-jwtライブラリとそのisTokenExpired()メソッドが使用されます 。 メソッドがtrueを返す場合、以前に作成されたrefresh()メソッドを呼び出してトークンを更新します。



誰かがJWTについて読むことに興味があるなら、ここに英語での良いレビューがありますJSON Web Tokenとは何ですか?



批判し、良いアドバイスをうれしく思います。



All Articles