이번 주 미션은 로그인 인증에 대해서 알아보고 Django에서 지원하는 Simple JWT로 직접 로그인을 구현해보는 것이다!
사용자가 로그인을 하면 서버에서 사용자를 확인한 후, 사용자에게 고유한 ID값을 부여하여 세션 저장소에 저장하고 Session ID
를 발급한다
서버는 HTTP response header에 발급된 Session ID
를 넣어 보낸다
클라이언트는 매 요청마다 HTTP request header에 Session ID
가 담킨 쿠키를 보낸다
서버에서는 세션 저장소에서 받은 쿠키와 일치하는 정보를 가져와 인증을 한다
사용자의 정보는 세션 저장소에 저장되기 때문에 쿠키가 노출되더라도 사용자의 정보는 알 수 없다
사용자는 고유의 Session ID
를 가지므로 회원 정보를 하나씩 대조할 필요가 없어 서버 자원 낭비를 줄일 수 있다
노출된 쿠키를 사용하여 서버로 HTTP 요청을 보내 서버가 사용자로 오인해 정보를 전달하게 만드는 세션 하이재킹 공격에 취약하다
세션 저장소를 사용하기 때문에 추가적인 저장공간을 필요로 한다
사용자가 로그인을 하면 서버에서 사용자를 확인 후, Payload
에 사용자의 정보를 넣는다
암호화된 Access Token
을 HTTP response header에 넣어 보낸다
사용자는 Access Token
을 받아 저장한 후, 인증이 필요한 요청마다 토큰을 HTTP request header에 넣어 보낸다
서버에서는 해당 토큰의 Verify Signature
를 복호화한 후, 조작 여부와 토큰 유효 기간을 확인한다
검증이 완료된다면, Payload
를 디코딩하여 사용자의 정보에 맞는 데이터를 가져온다
유저 정보를 토큰에 저장하므로 서버에 따로 추가 저장 공간이 필요 없다 -> 메모리, 비용 절감
토큰 기반으로 하는 다른 인증 시스템에 접근이 가능하기 때문에 확장성이 뛰어나다
JWT는 한 번 발급되면 유효기간이 만료될 때까지 삭제가 불가능하므로 한번 노출되면 대처할 방안이 없다
Payload
는 디코딩하면 누구나 접근할 수 있기에 중요한 정보들을 보관할 수 없다
JWT는 길기 때문에 인증 요청이 많아지면 서버에 자원낭비가 발생한다
Access Token을 이용한 인증 방식의 문제는 토큰이 노출될시 대처 방안이 없다는 것이다
토큰의 유효기간을 짧게 하면 사용자는 로그인을 자주 해야해서 번거롭기에 이를 해결하고자 나온 것이 Refresh Token
이다
Refresh Token
은 Access Token
과 같은 형태인 JWT이다
Refresh Token
은 Access Token
보다 유효기간이 길며, Access Token
이 만료됐을 때 Refresh Token
을 통해 Access Token
을 재발급 할 수 있다
사용자가 로그인을 하면 Access Token
, Refresh Token
을 발급하여 HTTP response header에 넣어 보낸다 Refresh Token은 따로 사용자 DB에 저장해놓는다
사용자는 Refresh Token
은 안전한 저장소에 저장 후, 요청을 보낼 때 Access Token
을 HTTP request header에 넣어 보낸다
서버는 받은 Access Token
을 검증한다 이때 유효기간이 지나 Access Token
이 만료됐다면 권한 없음을 HTTP 응답으로 보낸다
권한 없음 응답을 받은 사용자는 Refresh Token
과 Access Token
을 함께 HTTP request header에 보낸다
서버는 받은 Access Token
을 검증한 후, 받은 Refresh Token
과 사용자의 DB에 저장되어 있던 Refresh Token을 비교한다
Refresh Token
이 동일하고 유효기간도 지나지 않았다면 새로운 Access Token
을 발급하여 HTTP response header로 보낸다
Access Token
의 유효 기간이 짧기 때문에, 기존의 Access Token
만을 이용한 인증보다 안전하다Access Token
만 사용하는 것 보다 구현이 복잡하다
Access Token
이 만료될 때마다 새롭게 발급하는 과정에서 서버의 자원 낭비가 생긴다
OAuth는 외부서비스의 인증 및 권한부여를 관리하는 범용적인 프로토콜이다
현재 범용적으로 사용되고 있는 것은 OAuth 2.0이다
JSON Web Token(JWT)
는 인증에 필요한 정보들을 암호화시킨 토큰으로, 당사자 간에 정보를 JSON 형태로 안전하게 전송할 수 있다
이는 Access Token
으로 사용된다
JWT를 생성하기 위해서는 Header
, Payload
, Verify Signature
객체가 필요하다
<Header>
Header
는 alg
와 typ
로 구성된다
alg
: 암호화 방식(해싱 알고리즘)typ
: 토큰의 타입{
'alg': 'HS256',
'typ': 'JWT'
}
<Payload>
Payload
는 토큰에 담을 정보를 나타낸다
하나의 정보 조각을 클레임(Claims)으로 부르는데, 클레임의 종류로는 Registered
, Public
, Private
로 3가지가 존재한다
Registered Claims
(필수는 아닌 이름이 지정되어 있는 클레임들):
Public Claims
: 키와 값을 마음대로 정의 가능(충돌이 발생하지 않을 이름으로 설정해야함)
Private Claims
: 통신 간에 상호 합의되고 등록된 클레임과 공개된 클레임이 아닌 클레임
{
'sub': 'ceos payload',
'name': 'haensu',
'admin': true,
'iat': 1516239022
}
<Verify Signature>
Payload
가 위변조되지 않았다는 사실을 증명하는 문자열이다
Base64Url
방식으로 인코딩한 Header
, Payload
그리고 secret key
를 더한 후 암호화된다
완성된 토큰은 <Header\>.<Payload\>.<Signature\>
의 형식을 가진다
Header
와 Payload
는 인코딩될 뿐, 따로 암호화되지 않기에 누구나 디코딩하여 확인할 수 있기에 정보가 쉽게 노출될 수 있다
하지만 Verify Signature는 secret key
를 알지 못하면 복호화할 수 없다
만약에 해커가 사용자의 토큰을 훔쳐 Payload의 데이터를 변경하여 토큰을 서버로 보낸다면, 서버에서 Verify Signature를 검사하여 토큰의 유효성을 판단한다
이제 직접 JWT를 이용한 인증을 구현해보겠다!
Django에서는 JWT 인증을 위해 Simple JWT
라는 라이브러리를 제공한다
pip install djangorestframework-simplejwt
위 명령어를 통해 라이브러리를 설치해주고
INSTALLED_APPS = [
...
'rest_framework_simplejwt',
...
]
...
REST_FRAMEWORK = {
...
'DEFAULT_AUTHENTICATION_CLASSES': (
...
'rest_framework_simplejwt.authentication.JWTAuthentication',
)
...
}
...
# 추가적인 JWT_AUTH 설정
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30),
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
'ROTATE_REFRESH_TOKENS': False,
'BLACKLIST_AFTER_ROTATION': False,
'UPDATE_LAST_LOGIN': False,
'ALGORITHM': 'HS256',
'SIGNING_KEY': SECRET_KEY,
'VERIFYING_KEY': None,
'AUDIENCE': None,
'ISSUER': None,
'JWK_URL': None,
'LEEWAY': 0,
'AUTH_HEADER_TYPES': ('Bearer',),
'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id',
'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule',
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
'TOKEN_TYPE_CLAIM': 'token_type',
'TOKEN_USER_CLASS': 'rest_framework_simplejwt.models.TokenUser',
'JTI_CLAIM': 'jti',
'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp',
'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5),
'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),
}
AUTH_USER_MODEL = 'accounts.User'
setting.py
에 다음과 같이 추가한다
SIMPLE_JWT
에서는 토큰의 유효시간 등을 설정할 수 있다
freeze
명령어로 requirements.txt
에 라이브러리 추가하는 거 잊지 말자!
JWT 인증을 사용하기 위해서는 AbstractBaseUser
모델을 상속하여 기본 유저 모델을 만들어야 한다
이전에는 AbstractUser
모델을 상속했었기에
DB랑 마이그레이션 모두 날리고 다시 만들었다 ㅠ
class UserManager(BaseUserManager):
def create_user(self, username, email, nickname, password=None):
if not username:
raise ValueError(_('Users must have an ID'))
if not email:
raise ValueError(_('Users must have an email address'))
user = self.model(
username=username,
email=self.normalize_email(email),
nickname=nickname
)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, username, email, nickname, password):
user = self.create_user(
username=username,
email=email,
password=password,
nickname=nickname,
)
user.is_superuser = True
user.save(using=self._db)
return user
class User(AbstractBaseUser, BaseTimeModel, PermissionsMixin):
username = models.CharField(max_length=15, unique=True)
school = models.ForeignKey(School, on_delete=models.CASCADE, related_name='users', null=True)
nickname = models.CharField(max_length=10, unique=True)
email = models.EmailField(max_length=255, unique=True)
name = models.CharField(max_length=10)
objects = UserManager()
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = ['nickname', 'email']
@property
def is_staff(self):
return self.is_superuser
def __str__(self):
return self.username
회원 가입 후 로그인을 성공하면 Access Token과 Refresh Token을 모두 발급해주는 로직으로 구현하였다
회원가입시 password가 암호화되어 DB에 저장된다
class SignIn(APIView):
permission_classes = [AllowAny]
def post(self, request):
username = request.data['username']
password = request.data['password']
user = User.objects.filter(username=username).first()
# ID가 존재하지 않는 경우
if user is None:
return Response(
{"message": "회원정보가 일치하지 않습니다"}, status=status.HTTP_400_BAD_REQUEST
)
# # 비밀번호가 틀린 경우
if not check_password(password, user.password):
return Response(
{"message": "비밀번호가 틀렸습니다."}, status=status.HTTP_400_BAD_REQUEST
)
# user가 맞다면,
if user is not None:
token = TokenObtainPairSerializer.get_token(user) # refresh 토큰 생성
refresh_token = str(token) # refresh 토큰 문자열화
access_token = str(token.access_token) # access 토큰 문자열화
response = Response(
{
"user": UserSerializer(user).data,
"message": "login success",
"jwt_token": {
"access_token": access_token,
"refresh_token": refresh_token
},
},
status=status.HTTP_200_OK
)
response.set_cookie("access_token", access_token, httponly=True)
response.set_cookie("refresh_token", refresh_token, httponly=True)
return response
else:
return Response(
{"message": "로그인에 실패하였습니다."}, status=status.HTTP_400_BAD_REQUEST
)
TokenObtainPairSerializer.get_token(user)
은 Simple JWT의 내장 Serializer이다 이를 통해 Refresh Token
을 생성한다
check_password(current_password,user.password)
를 통해 hash 암호화 되어 저장된 password와 클라이언트로부터 받은 password를 비교하여 boolean값을 return해준다
response.set_cookie()'
를 통해 Access Token
과 Refresh Token
을 쿠키에 저장한다
로그인 성공시 Access Token
과 Refresh Token
이 쿠키에 저장되는 것을 볼 수 있다
로그아웃시 쿠키에서 token이 삭제된다
simple-jwt
라이브러리에서 Refresh Token
으로 Access Token
을 재발급 해주는 뷰를 제공해준다
from django.urls import path, include
from rest_framework_simplejwt.views import TokenRefreshView
from accounts.views import SignUp, SignIn, Logout
urlpatterns = [
path("sign-up/", SignUp.as_view()),
path("sign-in/", SignIn.as_view()),
path("log-out/", Logout.as_view()),
path("refresh/", TokenRefreshView.as_view())
]
위와 같이 제대로 발급되는 것을 볼 수 있다!
이제 권한 설정을 구현해보겠다
에브리타임을 이용하기 위해서는 로그인이 필수이다
즉, 로그인 하지 않은 사용자가 회원가입, 로그인 이외의 url에 접근하는 것을 막아야한다
로그인 했다는 것을 판별하기 위해 HTTP request header에 Access Token
을 함께 보낸다
Access Token
이 존재하지 않으면 해당 url에 접근할 수 없다
그럼 구현해보자!
pip install dj-rest-auth
우선 위의 명령어를 통해 라이브러리를 설치해준다
INSTALLED_APPS = [
...
'dj_rest_auth',
'rest_framework.authtoken',
...
]
REST_FRAMEWORK = {
# 'DEFAULT_AUTHENTICATION_CLASSES': (
# 'rest_framework_simplejwt.authentication.JWTAuthentication',
# )
'DEFAULT_AUTHENTICATION_CLASSES': [
'dj_rest_auth.jwt_auth.JWTCookieAuthentication'
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
]
}
DEFAULT_PERMISSION_CLASSES
를 IsAuthenticated
로 설정했다
그리고 토큰 인증이 필요없는 회원가입, 로그인 뷰에는
permission_classes = [AllowAny]
접근 권한을 AllowAny로 설정해주었다
Postman에서 새로운 environments를 추가했고
로그인 API 테스트에서 로그인시 발급받은 Access Token
이 자동으로 환경변수에 등록되도록 했다
마지막으로 Authorization에서 Bearer Token을 환경 변수로 설정해주면 끝!
로그인 하지 않았을 때 접근할 수 없다
로그인 하면 이렇게 접근 허용~~
지금까지 simple-jwt를 이용한 인증 및 permission을 구현해보았다!