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.)