Привет Хабр! При реализации Angular проекта, остро встал вопрос о безопасности graphql запросов в Angular 4. Выбор пал на JSON Web Tokens. Это открытый стандарт по RFC 7519.
Работает JWT по следующей схеме:
Я начал изучать программирование и Angular относительно недавно, полгода назад, и являюсь наивным чукотским малъчиком. Поэтому любую критику относительно кода и логики, приму как дружеский совет.
Клиентом graphql мы использовали apollo-angular (docs, github), и токены JWT нужны были в хедере каждого запроса к GraphQL API.
Создаем наш сервис авторизации AuthService. Первичное получение токена реализовано через REST:
Получаем accessToken и пишем его в sessionStorage браузера.
Далее, нам необходим клиент, для работы с API. Используем apollo-client:
В этом куске кода, мы берем наш токен из sessionStorage, и пишем его в хедер authorization.
У apollo-client есть пара методов для networkInterface: Middleware и Afterware. В нашем случае, использовался Middleware, его целью является применение тех или иных параметров, перед отправкой запроса к API.
И еще один немаловажный момент. В параметре opts, указан mode: 'cors'. Это сделано для Spring Security, на котором крутится мой бэк, в случае если на бекэнде нет cross-origin HTTP request фильтра, мод можно переключить на 'no-cors'.
Теперь, все запросы или мутации, уходящие посредством apollo-client, будут иметь в хедере наш jwt-токен. На бэкенде, реализована проверка этого токена на валидность и жизнеспособность по времени. Код не мой.
Теперь о refreshToken. Задача refreshToken'a обновить устаревший accessToken.
Реализации могут быть разные, начиная от примитивной проверки в AuthGuard сервисе Angular'a, заканчивая scheduler сервисом, который будет обновлять токен по заданному интервалу времени. В моем случае, был сделан первый вариант. Когда додумаюсь до более умного варианта, реализую. Пока что смог только так.
Итак, создаем метод в нашем сервисе AuthService, который будет вызываться, если наш сервис проверки AuthGuard заметит, что истек срок действия токена:
Далее, создаем собственно сервис проверки AuthGuard:
Здесь используется библиотека angular2-jwt и ее метод isTokenExpired(). Если метод возвращает true, вызываем созданный ранее метод refresh() и обновляем токен.
Если кому то будет интересно почитать насчет JWT, то вот хороший обзор на английском What is a JSON Web Token?
Рад критике и хорошим советам.
Работает JWT по следующей схеме:
Я начал изучать программирование и Angular относительно недавно, полгода назад, и являюсь наивным чукотским малъчиком. Поэтому любую критику относительно кода и логики, приму как дружеский совет.
Клиентом graphql мы использовали apollo-angular (docs, github), и токены JWT нужны были в хедере каждого запроса к GraphQL API.
Создаем наш сервис авторизации 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, и пишем его в хедер authorization.
У apollo-client есть пара методов для networkInterface: Middleware и Afterware. В нашем случае, использовался Middleware, его целью является применение тех или иных параметров, перед отправкой запроса к API.
И еще один немаловажный момент. В параметре opts, указан mode: 'cors'. Это сделано для Spring Security, на котором крутится мой бэк, в случае если на бекэнде нет cross-origin HTTP request фильтра, мод можно переключить на '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'a обновить устаревший accessToken.
Реализации могут быть разные, начиная от примитивной проверки в AuthGuard сервисе Angular'a, заканчивая scheduler сервисом, который будет обновлять токен по заданному интервалу времени. В моем случае, был сделан первый вариант. Когда додумаюсь до более умного варианта, реализую. Пока что смог только так.
Итак, создаем метод в нашем сервисе AuthService, который будет вызываться, если наш сервис проверки AuthGuard заметит, что истек срок действия токена:
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, то вот хороший обзор на английском What is a JSON Web Token?
Рад критике и хорошим советам.