Ssul's Blog

[Django] username대신, email 사용하기, jwt로그인 2 본문

dev/기능구현

[Django] username대신, email 사용하기, jwt로그인 2

Ssul 2023. 12. 19. 17:02

jwt토큰으로 로그인 전략.

 

1. 첫 로그인시 아이디/패스워드 입력

"use client";

import { useState } from "react";

export function useLoginForm() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  return {
    email,
    setEmail,
    password,
    setPassword,
  };
}

export default function LoginForm({
  email,
  password,
  setEmail,
  setPassword,
}: ReturnType<typeof useLoginForm>) {
  return (
    <>
      <form className="space-y-6">
        <div>
          <label
            htmlFor="email"
            className="block text-sm font-medium leading-6 text-gray-900"
          >
            Email address
          </label>
          <div className="mt-2">
            <input
              id="email"
              name="email"
              type="email"
              autoComplete="email"
              required
              className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
              onChange={(e) => setEmail(e.currentTarget.value)}
            />
          </div>
        </div>

        <div>
          <div className="flex items-center justify-between">
            <label
              htmlFor="password"
              className="block text-sm font-medium leading-6 text-gray-900"
            >
              Password
            </label>
            <div className="text-sm">
              <a
                href="#"
                className="font-semibold text-indigo-600 hover:text-indigo-500"
              >
                Forgot password?
              </a>
            </div>
          </div>
          <div className="mt-2">
            <input
              id="password"
              name="password"
              type="password"
              autoComplete="current-password"
              required
              className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
              onChange={(e) => setPassword(e.currentTarget.value)}
            />
          </div>
        </div>
      </form>
    </>
  );
}

- id/pw입력받아, api로 호출하는 코드

 

2. 장고 authenticate로 인증확인 후

3. token(access,refresh)을 생성해서, json으로 user정보와 함께 client에 전달

#views.py
class LoginView(APIView):
    permission_classes = [permissions.AllowAny]
    serializer_class = CustomTokenObtainPairSerializer

    def post(self, request):
        serializer = self.serializer_class(data=request.data)
        if serializer.is_valid():
            # serializer의 validate함수 호출
            # user = serializer.validated_data
            # login(request, user)
            # return Response({"detail": "Logged in successfully!"}, status=status.HTTP_200_OK)

            # validate 함수에서 생성된 토큰을 반환합니다.
            data = serializer.validated_data
            return Response(data, status=status.HTTP_200_OK)

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
        
        
#serializers.py
# 고객에게 email통해서 로그인 > username기반 토큰 검색
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
    username_field = get_user_model().USERNAME_FIELD

    email = serializers.EmailField()
    password = serializers.CharField(write_only=True)


    @classmethod
    def get_token(cls, user):
        token = super().get_token(user)

        # 필요한 경우 추가 사용자 정보를 토큰에 포함시킬 수 있습니다.
        # 예: token['username'] = user.username
        return token

    def validate(self, attrs):
        # attrs는 유효성 검증된 데이터, initial_data는 원래 데이터

        # email을 사용해 사용자 인증을 시도합니다.
        user = authenticate(email=attrs['email'], password=attrs['password'])

        if user and user.is_active:
            data = super().validate(attrs)
            #user객체로 토큰 발급
            refresh = self.get_token(user)
			
            #토큰을 client에 전달 준비
            data['refresh'] = str(refresh)
            data['access'] = str(refresh.access_token)

            # 여기에 사용자 정보 추가해서 함께 전달
            data['user'] = {
                'email': user.email,
                'nickname': user.nickname,
                'status': user.status,
                # 필요한 경우 여기에 다른 필드 추가
            }

            return data

        raise serializers.ValidationError("Incorrect Credentials")

 

4. 클라이언트는 로컬스토리지에 토큰저장, 리덕스에도 저장

// Function to handle form submission
  const login = async (e) => {
    e.preventDefault();
    const { email, password } = loginForm;
    try {
      const response = await axios.post(
        `${process.env.NEXT_PUBLIC_API_URL}/accounts/login/`,
        {
          email,
          password,
        },
      );
      
      //api서버로부터 받은 토큰들
      const accessToken = response.data.access;
      const refreshToken = response.data.refresh;
      
      // user정보는 리덕스에 저장
      dispatch(
        //요 친구들이 다 action.payload에 저장
        userSlice.actions.setUser({
          user: response.data.user,
          access: response.data.access,
          refresh: response.data.refresh,
        }),
      );
      
      // 토큰은 로컬 스토리지에 저장
      localStorage.setItem("accessToken", accessToken);
      localStorage.setItem("refreshToken", refreshToken);
      alert("알림: 로그인 완료");
      // Redirect or perform other actions on successful login
    } catch (error) {
      console.log("Failed to sign in. Please check your credentials.");
    }
  };

 

5. 리덕스상태를 통해 로그인여부를 확인, 리덕스가 clear해지면, 로컬스토리지에서 refresh토큰 체크

//생략

// 훅을 통하여 로그인여부 체크하여 user객체 가져오기
const user = useAuth();

//user객체와, email이 리덕스에 있으면 로그인 한것
{user && user.userInfo.email ? ( // 로그인한 경우 마이페이지 링크를 표시합니다.
          <div className="flex items-center space-x-4">
            <div>
              <Link href="#">마이페이지</Link>
            </div>
            <Logout />
          </div>
        ) : (
          <div className="flex items-center space-x-4">
            <div>
              <Link href="/login">로그인</Link>
            </div>
            <div>회원가입</div>
          </div>
        )}

 

6. refresh토큰이 없으면 로그인화면, 있으면 api에 refresh토큰과 함께 호출

const useAuth = () => {
  const dispatch = useDispatch();
  const user = useSelector((state) => state.user);

  useEffect(() => {
    const fetchUserData = async () => {
    	// 로컬스토리지에서 토큰 가져오기
      const accessToken = localStorage.getItem("accessToken");
      const refreshToken = localStorage.getItem("refreshToken");

		//토큰이 있으면, refresh토큰으로 로그인 정보 받아오기
      if (accessToken && refreshToken) {
        try {
          // JWT 토큰을 사용하여 서버로부터 사용자 정보를 요청합니다.
          const response = await axios.post(
            `${process.env.NEXT_PUBLIC_API_URL}/api/token/refresh/`,
            {
              refresh: refreshToken,
            },
          );

          // 받아온 유저정보 업데이트
          dispatch(
            userSlice.actions.setUser({
              user: response.data.user,
              access: response.data.access,
              refresh: response.data.refresh,
            }),
          );
        } catch (error) {
          console.error("User authentication failed", error);
          // 에러 처리 로직 (예: 로그아웃 처리, 상태 업데이트 등)
        }
      }
    };

    fetchUserData();
  }, [dispatch]);

  return user;
};

export default useAuth;

 

7. 원래는 토큰만 주지만, user정보도 함께 주도록 커스텀마이징(refresh api커스터마이징)

#urls.py
path('api/token/refresh/', CustomTokenRefreshView.as_view(), name='token_refresh'),

#views.py
class CustomTokenRefreshView(TokenRefreshView):
    serializer_class = CustomTokenRefreshSerializer

#serializers.py
class CustomTokenRefreshSerializer(TokenRefreshSerializer):
    def validate(self, attrs):
        data = super().validate(attrs)

        # 여기에서 사용자 정보를 가져옵니다.
        refresh = RefreshToken(attrs['refresh'])
        user = get_user_model().objects.get(id=refresh['user_id'])

        # 사용자 정보를 응답 데이터에 추가합니다.
        data['user'] = {
            'email': user.email,
            'nickname': user.nickname,
            'status': user.status,
            # 필요한 경우 다른 필드 추가
        }

        return data

 

 

매번 구현할때마다, 다시 찾아서 구현하던 것을 정리.

이런 순서로 체크하며, 본인 서비스에 적용하면 된다.