React Apollo: JWT 및 새로 고침 토큰
JWT(JSON Web Token)를 인증으로 선택합니다. JWT를 구현할 때 액세스 토큰과 새로 고침 토큰을 발급합니다.AccessToken 및 RefreshToken
Refresh Token 덕분에 더욱 안전한 Access Token을 관리할 수 있습니다.
'갱신 토큰이 유출되면 어떡하지?'라고 물을 수 있습니다. 우리를 더 안전하게 만드는 많은 전략이 있습니다. RTR(Refresh Token Rotation)처럼요.
간단히 말해서 refresh API는 액세스 토큰과 새로 고침 토큰을 발급하고 새로 고침 토큰을 만료시킵니다. 새로 고침 토큰이 두 번 이상 사용되면 토큰이 유출되었을 것이라고 가정합니다.
이 설명서auth0-refresh-token-rotation를 읽는 것이 좋습니다.
이 게시물에서는 더 이상 JWT에 대해 이야기하지 않고 계속 진행하겠습니다.
새로 고침 토큰 구현
NestJS를 사용하여 테스트 서버를 만들었습니다. 3명의 리졸버와 2명의 가드가 있습니다.근위 연대
Authorization 헤더에서 액세스 토큰이 유효한지 인증합니다. Authorization 헤더에서 유효한지 인증합니다. 두 토큰 모두 각 요청의
Authorization 헤더에 전달되고 localStorage에 저장됩니다.더 나은 보안을 위해 httpOnly 속성 및 SameSite 속성과 함께
cookie 를 사용할 수 있습니다.아피스
401 error를 반환합니다. 401 error DTO
import { ObjectType, Field } from '@nestjs/graphql';
@ObjectType()
export class CreateTokenResponse {
@Field()
accessToken: string;
@Field()
refreshToken: string;
}
@ObjectType()
export class RefreshTokenResponse {
@Field()
accessToken: string;
}
리졸버
@Resolver()
export class AuthResolver {
constructor(private readonly authService: AuthService) {}
@Mutation(() => CreateTokenResponse)
async createToken(): Promise<CreateTokenResponse> {
return this.authService.createToken();
}
@UseGuards(JwtAuthGuard)
@Query(() => Boolean)
async ping() {
return true;
}
@UseGuards(JwtRefreshAuthGuard)
@Mutation(() => RefreshTokenResponse)
async refreshToken(): Promise<RefreshTokenResponse> {
return this.authService.refreshToken();
}
}
대본

이 시나리오에는 6단계가 있습니다.
시나리오에서는 액세스 토큰의 만료 시간을 5초로 설정했습니다.
아폴로 클라이언트 반응
유형 및 쿼리
/**
* Types
*/
interface Tokens {
accessToken: string;
refreshToken: string;
}
interface AccessToken {
accessToken: string;
}
/**
* Queries
*/
const CREATE_TOKEN = gql`
mutation createToken {
createToken {
accessToken
refreshToken
}
}
`;
const REFRESH_TOKEN = gql`
mutation refreshToken {
refreshToken {
accessToken
}
}
`;
const PING = gql`
query ping {
ping
}
`;
페이지
/**
* React Components
*/
function App() {
const [createToken, { data: createTokenData }] = useMutation<{
createToken: Tokens;
}>(CREATE_TOKEN);
const [ping] = useLazyQuery(PING, {
fetchPolicy: 'network-only',
});
const requestToken = () => {
createToken();
};
const sendPing = () => {
ping();
};
useEffect(() => {
if (!createTokenData) return;
const { accessToken, refreshToken } = createTokenData.createToken;
// Save tokens in localStorage
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
}, [createTokenData]);
return (
<Container>
<button type="button" onClick={requestToken}>
login
</button>
<button type="button" onClick={sendPing}>
ping
</button>
</Container>
);
}
function ApolloWrapper() {
return (
<ApolloProvider client={client}>
<App />
</ApolloProvider>
);
}
/**
* Styles
*/
const Container = styled.div`
display: flex;
flex-direction: column;
row-gap: 12px;
padding: 24px;
> button {
width: 200px;
height: 24px;
}
`;
export default ApolloWrapper;
두 개의 버튼이 있습니다. 하나는
createToken용이고 다른 하나는 pass용입니다.refreshToken 요청 및 실패한 요청 재시도
/**
* Apollo Setup
*/
function isRefreshRequest(operation: GraphQLRequest) {
return operation.operationName === 'refreshToken';
}
// Returns accesstoken if opoeration is not a refresh token request
function returnTokenDependingOnOperation(operation: GraphQLRequest) {
if (isRefreshRequest(operation))
return localStorage.getItem('refreshToken') || '';
else return localStorage.getItem('accessToken') || '';
}
const httpLink = createHttpLink({
uri: 'http://localhost:3000/graphql',
});
const authLink = setContext((operation, { headers }) => {
let token = returnTokenDependingOnOperation(operation);
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
},
};
});
const errorLink = onError(
({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
for (let err of graphQLErrors) {
switch (err.extensions.code) {
case 'UNAUTHENTICATED':
// ignore 401 error for a refresh request
if (operation.operationName === 'refreshToken') return;
const observable = new Observable<FetchResult<Record<string, any>>>(
(observer) => {
// used an annonymous function for using an async function
(async () => {
try {
const accessToken = await refreshToken();
if (!accessToken) {
throw new GraphQLError('Empty AccessToken');
}
// Retry the failed request
const subscriber = {
next: observer.next.bind(observer),
error: observer.error.bind(observer),
complete: observer.complete.bind(observer),
};
forward(operation).subscribe(subscriber);
} catch (err) {
observer.error(err);
}
})();
}
);
return observable;
}
}
}
if (networkError) console.log(`[Network error]: ${networkError}`);
}
);
const client = new ApolloClient({
link: ApolloLink.from([errorLink, authLink, httpLink]),
cache: new InMemoryCache(),
});
// Request a refresh token to then stores and returns the accessToken.
const refreshToken = async () => {
try {
const refreshResolverResponse = await client.mutate<{
refreshToken: AccessToken;
}>({
mutation: REFRESH_TOKEN,
});
const accessToken = refreshResolverResponse.data?.refreshToken.accessToken;
localStorage.setItem('accessToken', accessToken || '');
return accessToken;
} catch (err) {
localStorage.clear();
throw err;
}
};
요청이
refreshToken에 대한 것인지 아닌지를 operation.operationName를 통해 구분합니다.요점은
onError를 사용하여 Observable에서 재시도 요청 논리를 구현할 수 있다는 것입니다.Observable에서 onError 개체를 반환한 다음 함수에서 새 액세스 토큰을 가져오고 forward을 사용하여 요청을 다시 시도합니다.링크 순서가 원하는 대로 올바른지 확인하십시오.
this repository에서 결과를 gif 이미지와 코드로 볼 수 있습니다.
그게 다야, 누군가에게 도움이 되길 바랍니다.
행복한 코딩!
Reference
이 문제에 관하여(React Apollo: JWT 및 새로 고침 토큰), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/lico/react-apollo-refresh-tokens-5h0k텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념
(Collection and Share based on the CC Protocol.)