xxleyi / learning_list

聚集自己的学习笔记
11 stars 3 forks source link

Cookie, Session and JWT Token in HTTP #74

Open xxleyi opened 5 years ago

xxleyi commented 5 years ago

HTTP cookies - HTTP | MDN

Typically, it's used to tell if two requests came from the same browser — keeping a user logged-in, for example. It remembers stateful information for the stateless HTTP protocol.

Cookies are mainly used for three purposes:

Session management Logins, shopping carts, game scores, or anything else the server should remember

Personalization User preferences, themes, and other settings

Tracking Recording and analyzing user behavior

所以说,最根本的立足点是基于 HTTP 协议了解和掌握 cookies 。开头那篇 MDN 的文档很棒,基于 HTTP 介绍 cookies 的方方面面。下面对重要内容做一些摘抄。

The Set-Cookie and Cookie headers

The Set-Cookie HTTP response header sends cookies from the server to the user agent. A simple cookie is set like this:

Set-Cookie: <cookie-name>=<cookie-value>

This header from the server tells the client to store a cookie.

HTTP/2.0 200 OK
Content-type: text/html
Set-Cookie: yummy_cookie=choco
Set-Cookie: tasty_cookie=strawberry

page content

Now, with every new request to the server, the browser will send back all previously stored cookies to the server using the Cookie header.

GET /sample_page.html HTTP/2.0
Host: www.example.org
Cookie: yummy_cookie=choco; tasty_cookie=strawberry

接下来是除了 key value 对之外一些配置项,不再详述。

重点讲一个 HttpOnly:

JavaScript access using Document.cookieSection

New cookies can also be created via JavaScript using the Document.cookie property, and if the HttpOnly flag is not set, existing cookies can be accessed from JavaScript as well.

document.cookie = "yummy_cookie=choco"; 
document.cookie = "tasty_cookie=strawberry"; 
console.log(document.cookie); 
// logs "yummy_cookie=choco; tasty_cookie=strawberry"

Cookies created via JavaScript cannot include the HttpOnly flag.

Please note the security issues in the Security section below. Cookies available to JavaScript can be stolen through XSS.

Session hijacking and XSS

The HttpOnly cookie attribute can help to mitigate this attack by preventing access to cookie value through JavaScript.

Cross-site request forgery (CSRF)

xxleyi commented 5 years ago

set cookie util and Django response cookie method

import datetime
def set_cookie(response, key, value, days_expire=None):
    if days_expire is None:
        max_age = 365 * 24 * 60 * 60  # one year
    else:
        max_age = days_expire * 24 * 60 * 60
    expires = datetime.datetime.strftime(
        datetime.datetime.utcnow() + datetime.timedelta(seconds=max_age),
        "%a, %d-%b-%Y %H:%M:%S GMT",
    )
    response.set_cookie(
        key,
        value,
        max_age=max_age,
        expires=expires,
        domain=settings.SESSION_COOKIE_DOMAIN,
        secure=settings.SESSION_COOKIE_SECURE or None,
    )

虽然很棒,但是并不需要,因为 Django 默认的支持就挺好。

Django HttpResponseBase 中已经实现了一套完善的 set 和 delete cookie 方法

def set_cookie(self, key, value='', max_age=None, expires=None, path='/',
                   domain=None, secure=False, httponly=False, samesite=None):
        """
        Set a cookie.

        ``expires`` can be:
        - a string in the correct format,
        - a naive ``datetime.datetime`` object in UTC,
        - an aware ``datetime.datetime`` object in any time zone.
        If it is a ``datetime.datetime`` object then calculate ``max_age``.
        """
        self.cookies[key] = value
        if expires is not None:
            if isinstance(expires, datetime.datetime):
                if timezone.is_aware(expires):
                    expires = timezone.make_naive(expires, timezone.utc)
                delta = expires - expires.utcnow()
                # Add one second so the date matches exactly (a fraction of
                # time gets lost between converting to a timedelta and
                # then the date string).
                delta = delta + datetime.timedelta(seconds=1)
                # Just set max_age - the max_age logic will set expires.
                expires = None
                max_age = max(0, delta.days * 86400 + delta.seconds)
            else:
                self.cookies[key]['expires'] = expires
        else:
            self.cookies[key]['expires'] = ''
        if max_age is not None:
            self.cookies[key]['max-age'] = max_age
            # IE requires expires, so set it if hasn't been already.
            if not expires:
                self.cookies[key]['expires'] = http_date(time.time() + max_age)
        if path is not None:
            self.cookies[key]['path'] = path
        if domain is not None:
            self.cookies[key]['domain'] = domain
        if secure:
            self.cookies[key]['secure'] = True
        if httponly:
            self.cookies[key]['httponly'] = True
        if samesite:
            if samesite.lower() not in ('lax', 'strict'):
                raise ValueError('samesite must be "lax" or "strict".')
            self.cookies[key]['samesite'] = samesite

def delete_cookie(self, key, path='/', domain=None):
        # Most browsers ignore the Set-Cookie header if the cookie name starts
        # with __Host- or __Secure- and the cookie doesn't use the secure flag.
        secure = key.startswith(('__Secure-', '__Host-'))
        self.set_cookie(
            key, max_age=0, path=path, domain=domain, secure=secure,
            expires='Thu, 01 Jan 1970 00:00:00 GMT',
        )
xxleyi commented 5 years ago

Django login user without password

def login_user(request, user):
    """ log in a user without requiring credentials with user object"""
    if not hasattr(user, "backend"):
        for backend in settings.AUTHENTICATION_BACKENDS:
            user.backend = backend
            break
    return login(request, user)
xxleyi commented 5 years ago

Django session valid_exist

session = request.session
session_store = SessionStore()
session_store.clear_expired()
valid_exist = session_store.exists(session_key=session.session_key)
xxleyi commented 5 years ago

Django 手动设置 jwt 认证 token

from rest_framework_jwt.utils import jwt_payload_handler, jwt_encode_handler

def get_jwt_auth_token(user):
    payload = jwt_payload_handler(user)
    token = jwt_encode_handler(payload)
    return token

response = HttpResponseRedirect("/")
response.set_cookie("Admin-Token", get_jwt_auth_token(user), httponly=True)
response.set_cookie("username", username, httponly=True)
xxleyi commented 5 years ago

Django 强制退出某 session 对应的用户

@api_view(["GET"])
@permission_classes([])
def logout_by_sso(request):
    """
    sso 强制退出
    """
    session_key = request.GET.get("session_id")
    SessionStore().delete(session_key=session_key)
    return Response(dict(code=200, message="OK"))
xxleyi commented 5 years ago

Flask 强制退出某 session 对应的用户

import redis
from flask_session import RedisSessionInterface

import config

_RedisInstance = redis.Redis(
    host=config.REDIS_CONFIG.get("REDIS_HOST"),
    port=config.REDIS_CONFIG.get("REDIS_PORT"),
    db=config.REDIS_INDEX,
    socket_timeout=10,
    password=config.REDIS_CONFIG.get("REDIS_PASSWORD"),
)

_SESSION_PREFIX = config.SESSION_KEY_PREFIX
SESSION_REDIS = _RedisInstance

class SessionManager(object):
    __redis = SESSION_REDIS

    @classmethod
    def force_logout(cls, key):
        cls.__redis.delete(_SESSION_PREFIX + key)
xxleyi commented 5 years ago

Flask sso auth by login session without password

@app.route('/', methods=['GET'])
def welcome():
    """
    sso 返回数据格式
    {
        "code": 200,
        "message": "OK",
        "data": {
                "uidnumber": 1,
                "username": "*****",
                "realname": "***",
                "mail": "*******",
                "title": "",
                "department": "*****"
            }
    }
    """
    sso_token = request.args.get('sso_access_token')
    fresh = session.get('_fresh')
    if not sso_token and not fresh:
        return redirect(url_for('login'))

    if sso_token:
        url = "{}/api/decrypt".format(SSO_END_POINT)
        data = dict(sso_access_token=sso_token, session_id=session.sid)
        res = requests.post(url, json=data, timeout=10)
        if res.status_code == 200 and res.json()['code'] == 200:
            res_data = res.json()['data']
            username = res_data['username']
            session['username'] = username
            dbsession = SessionAudit()
            user = dbsession.query(User).filter(User.username == username).first()
            if not user:
                user = User(**dict(user_id=None, name=res_data['realname'], username=username, account=username + '@wecash.net'))
                dbsession.add(user)
                user_obj = format_user_obj(user)
            else:
                user_obj = format_user_obj(user)
            login_user(user_obj, remember=False, force=True, fresh=True)

    return render_template('welcome.html')
xxleyi commented 5 years ago

Django add custom auth backend

# -*- coding:utf-8 -*-

import requests

from django.conf import settings
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import User

class SSOBackend(ModelBackend):
    def authenticate(self, sso_token, session_id, **kwargs):
        url = "{}/api/decrypt".format(settings.SSO_END_POINT)
        data = dict(sso_access_token=sso_token, session_id=session_id)
        res = requests.post(url, json=data, timeout=10)
        if res.status_code == 200 and res.json()["code"] == 200:
            res_data = res.json()["data"]
            username = res_data["username"]
            try:
                user = User.objects.get(username=username)
            except User.DoesNotExist:
                user = User.objects.create_user(
                    username=username,
                    email=res_data["mail"],
                    last_name=res_data["realname"],
                )
            return user

    def get_user(self, user_id):
        try:
            return User.objects.get(pk=user_id)
        except User.DoesNotExist:
            return None

@api_view(["GET"])
@permission_classes([])
def sso_auth(request):
    """
    sso 返回数据格式
    {
        "code": 200,
        "message": "OK",
        "data": {
                "uidnumber": 109527,
                "username": "*****",
                "realname": "***",
                "mail": "*******",
                "title": "",
                "department": "*****"
            }
    }
    """
    sso_token = request.GET.get("sso_access_token")
    session = request.session
    session_store = SessionStore()
    session_store.clear_expired()
    valid_exist = session_store.exists(session_key=session.session_key)

    if not sso_token and not valid_exist:
        return redirect("/")

    if sso_token:
        user = authenticate(sso_token, session.session_key)
        if user:
            login(request, user)
            response = HttpResponseRedirect("/")
            response.set_cookie("Admin-Token", get_jwt_auth_token(user))
            response.set_cookie("username", user.username)
            return response

    return redirect("/")

# settings.py
AUTHENTICATION_BACKENDS = ["applications.ss_auth.backends.SSOBackend"]