Nextjs 및 Django의 jwt 제작 인증 기능

Django와Nextjs가 좋은 느낌의 jwt 인증을 했습니다.
원래 쿠키에 넣은 jwt를 httpOnly로 바꾸려고 했는데, 단순 jwt의 규격상 쿠키가 필요한 부분은 스스로 만들어야 한다.(simplejwt의 원본 코드를 완전히 읽지 않아서 잘 모르겠습니다.)
그래서 머리를 쥐어짜서 약간 단순 jwt를 만들어서 실현했다.
아직 어른스럽지는 않지만 좋은 걸 만들 수 있을 것 같아서 기사를 썼어요.

참고문과 전제


Django


간단하게 DRF(Django rest frame work)를 사용합니다.
jwt
https://qiita.com/Syoitu/items/bc32b5e1c2fa891c2f96
이분처럼 직접 만든 것도 있지만 귀찮아서 단순 jwt를 썼어요.
https://django-rest-framework-simplejwt.readthedocs.io/en/latest/getting_started.html

Nextjs


전 세계 로그인 상태의 관리캣노스 씨.에 관하여 나는 이 글을 완전히 참고하였다.
https://zenn.dev/catnose99/articles/2169dae14b58b6

구조


로그인 처리


사용자 이름과 비밀번호로 로그인한 후 jwt의access token과refresh token을 쿠키로 설정합니다.
부합catnose의 보도fetch 현재 User의 처리는 다음과 같다.
최초 DOM 구축 시 쿠키의access token을 이용하여 사용자의 정보를 얻고state에 넣습니다.
access token이 끊어진 상태에서refresh token을 던져access token을 획득합니다.access token을 통해 사용자 정보를 다시 시도합니다.

이루어지다


쿠키에 jwt 처리 설정


단순 jwt의views.TokenObtainPairView를 살짝 맞춤형으로 만들어볼게요.
Token Obtain PairView는 POST의 사용자 이름과 비밀번호가 일치하면access token과refresh token을 반환합니다.이번에는 쿠키에 설치된 처리를 추가했습니다.secure 속성은middleware를 분리해서 제작하여 임의로 secure하는 것입니다.
# Django3

from rest_framework_simplejwt import views as jwt_views
from rest_framework_simplejwt import exceptions as jwt_exp

class TokenObtainView(jwt_views.TokenObtainPairView):
    def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
	try:
	    serializer.is_valid(raise_exception=True)
	except jwt_exp.TokenError as e:
	    raise jwt_exp.InvalidToken(e.args[0])
	
	res = response.Response(serializer.validated_data, status=status.HTTP_200_OK)
	try:
	    res.delete_cookie("user_token")
	except Exception as e:
	    print(e)  # ここら辺適当すぎる
	
	# httpOnlyなのでtokenの操作は全てdjangoで行う
	res.set_cookie(
	    "user_token",
	    serializer.validated_data["access"],
	    max_age=60 * 60 * 24,
	    httponly=True,
	)
	res.set_cookie(
            "refresh_token",
            serializer.validated_data["refresh"],
            max_age=60 * 60 * 24 * 30,
            httponly=True,
        )
	return res
	

access token에서user 정보 가져오기


쿠키에서access token을 주우고python의 jwt decode를 사용하는 곳이 바로 맛입니다
# Django3

import jwt

class UserAPIView(views.APIView):
    def get_object(self, JWT):

        try:
            payload = jwt.decode(
                jwt=JWT, key=settings.SECRET_KEY, algorithms=["HS256"]
            )
	    # DBにアクセスせずuser_idだけの方がjwtの強みが生きるかも
	    # その場合 return payload["user_id"]
            return User.objects.get(id=payload["user_id"])

        except jwt.ExpiredSignatureError:
	    # access tokenの期限切れ
            return "Activations link expired"
        except jwt.exceptions.DecodeError:
            return "Invalid Token"
        except User.DoesNotExist:
            return "user does not exists"

    def get(self, request, format=None):
        JWT = request.COOKIES.get("user_token")
        if not JWT:
            return response.Response(
                {"error": "No token"}, status=status.HTTP_400_BAD_REQUEST
            )
        user = self.get_object(JWT)

        # エラーならstringで帰ってくるので、型で判定
	# ここイケてないな
        if type(user) == str:
            return response.Response(
                {"error": user}, status=status.HTTP_400_BAD_REQUEST
            )

        if user.is_active:
            serializer = UserSerializer(user)
            return response.Response(serializer.data)
        return response.Response(
            {"error": "user is not active"}, status=status.HTTP_400_BAD_REQUEST
        )
	

Nextjs 측 처리


access token을 통해 사용자 가져오기 시도
만약accesstoken이 만료된 오류라면refreshtoken으로accesstoken을 리셋하고 위의 처리를 다시 실행할 수 있습니다.
// Nextjs

export const fetchCurrentUser = async () => {
  try {
    const user = await tokenToUser();
    return user;
  } catch (e) {
    // tokenの有効期限が切れていたら refreshを試みる
    if (e["error"] === "Activations link expired") {
      const refresh = await refreshToken();
      const refreshRet = await newToken(refresh);
      
      if (refreshRet["access"]) {
        // refresh に成功したら再度 access tokenでのユーザー取得を試みる
        const user = await tokenToUser();
        return user;
      }
    }
  }
};

// tokenからuser情報を取得
const tokenToUser = async () => {
  const res = await fetch(`${baseUrl}/api/user/`, {
    credentials: "include",
  });
  const ret = await res.json();
  if (res.status === 400) {
    throw ret;
  }
  return ret;
};

// refresh tokenをもらう
export const getRefreshToken = async () => {
  const res = await fetch(`${baseUrl}/api/user/refresh/`, {
    credentials: "include",
  });
  const ret = await res.json();
  return ret;
};

// refresh tokenから 新しい access tokenを生成
const newToken = async (refresh: any) => {
  const res = await fetch(`${baseUrl}/api/user/refresh/token/`, {
    method: "POST",
    credentials: "include",
    headers: {
      "Content-Type": "application/json; charset=utf-8",
      "X-CSRFToken": csrf["token"],
    },
    body: JSON.stringify(refresh),
  });
  const ret = await res.json();
  destroyCookie(null, "csrftoken");
  return ret;
};

refresh 처리


refresh_get으로 refresh token을 취득하고 단순 jwt의 Token RefreshView에서 POST를 진행한다.
이렇게 하면 새로운 access token이 쿠키에 들어가요.
# Django3

def refresh_get(request):
    try:
        RT = request.COOKIES["refresh_token"]
        return JsonResponse({"refresh": RT}, safe=False)
    except Exception as e:
        print(e)
        return None
	
class TokenRefresh(jwt_views.TokenRefreshView):
    def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)

        try:
            serializer.is_valid(raise_exception=True)
        except jwt_exp.TokenError as e:
            raise jwt_exp.InvalidToken(e.args[0])

        res = response.Response(serializer.validated_data, status=status.HTTP_200_OK)
        res.delete_cookie("user_token")
        res.set_cookie(
            "user_token",
            serializer.validated_data["access"],
            max_age=60 * 24 * 24 * 30,
            httponly=True,
        )
        return res
nginx의cors를 설정한 후refreshget에서 Token Refresh requests로 직접 이동합니다.포스터도 있고.
이상은 실제 처리입니다.
csrf 대책과 쿠키를 넣기 위한 middleware 등의 제작도 이번 실상에서 진행됐지만, 주제에서 벗어나 사랑을 끊었다.

잡담


당신의 질문과 의견을 기다리겠습니다.
핑계가 되지만 실제 업무가 1.5개월도 안 되는 엔지니어, 그리고 직장에서python과django를 사용하지 않아 난폭한 부분이 있으니 의견이 있으면 마음대로 논평해 주세요.
문제도 마음대로 논평해 주세요.
이 사이트에서 이 처리를 사용합니다.
관심 있으신 분들은 꼭 가보세요.
https://iwana.link

좋은 웹페이지 즐겨찾기