Django follow,following , OAuth2

Haks.·2025년 2월 5일
0

How to use

목록 보기
19/32

📝 팔로워 팔로잉

  • followers, following 누르면 개별 창이 떠서 목록 뜨게
  • html 코드에서 follower, following 이 동작하도록 코드 수정 bootstarp modal 사용 -> 5.3version 으로 접속 -> Live demo 부분 modal 부분 복사 , 붙여넣기 , 버튼부분 data 부터해서 수정
<!-- profile/detail.html -->
<div class="row">
    <div class="col-4 text-center">{{ object.post_set.count | intcomma }} posts</div>
    <div class="col-4 text-center">
        <button class="border-0 bg-transparent" data-bs-toggle="modal" data-bs-target="#followers-modal">
            {{ object.followers.count | intcomma }} followers
        </button>
    </div>
    <div class="col-4 text-center">
        <button class="border-0 bg-transparent" data-bs-toggle="modal" data-bs-target="#following-modal">
            {{ object.following.count | intcomma }} following
        </button>
    </div>
</div>

<!-- following modal, follower modal id 수정 -->
<div class="modal fade" id="followers-modal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <h1 class="modal-title fs-5" id="exampleModalLabel">Followers</h1>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <div class="modal-body">
          <ul>
            {% for follower in object.followers.all %}
                <li>{{ follower.nickname }}</li>
            {% endfor %}
          </ul>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
      </div>
    </div>
  </div>
</div>

<div class="modal fade" id="following-modal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <h1 class="modal-title fs-5" id="exampleModalLabel">Following</h1>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <div class="modal-body">
        <ul>
            {% for follower in object.following.all %}
                <li>{{ follower.nickname }}</li>
            {% endfor %}
          </ul>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
      </div>
    </div>
  </div>
</div>
{% endblock %}

📝 유저 검색 기능

  • 네비게이션 검색 버튼, 검색페이지, 유저를, 본문을, 무엇을 검색할지 정해서 검색하도록 만들어 보기
  1. font-awesome 사용 -> search 검색
<!-- base.html 닉네임 위쪽 -->
<div>
    <a href="">
        <i class="fa-solid fa-magnifying-glass text-white"></i>
    </a>
</div>
  1. 검색페이지 디자인 따로 앱 빼도 되지만, post/views.py 로 진행, 기본 뼈대 작성
# post/views.py
def search(request):
    search_type = request.GET.get('type') # user인지, tag인지
    if search_type in ['user', 'tag'] :
        ... # redirect

    return render(request, template_name='search.html')
# config/urls.py
    path('search/', post_views.search, name='search'),
<!-- base.html -->
<div>
    <a href="{% url 'search' %}">
        <i class="fa-solid fa-magnifying-glass text-white"></i>
    </a>
</div>
  1. 화면단 보면서 html 완성
{% extends 'base.html' %}
{% block content %}
    <form action="" method="get">
        <div class="row">
            <div class="col-3">
                <select name="type" id="" class="form-control">
                    <option value="user">User</option>
                    <option value="tag">Tag</option>
                </select>
            </div>
            <div class="col-7">
                <input type="text" name="q" placeholder="검색어" class="form-control">
            </div>
            <div class="col-2">
                <button class="btn btn-primary">
                    검색
                </button>
            </div>
        </div>
    </form>
{% endblock %}
  1. def search 수정
def search(request):
    search_type = request.GET.get('type') # user인지, tag인지
    q=request.GET.get('q')

    # type 을 정하고 그리고 html에서 name=q 로 정해서 보내준것 즉 검색어에 해당하는 것을 공통으로 해당하는 것을 가져고 오기위해
    if search_type in ['user', 'tag'] and q:
        return render(request, f'search/search_{search_type}.html')

    return render(request, template_name='search/search.html')
  1. 다양한 search html 생성 공통부분 include 에 넣어서 생성하기
<!-- include/search_form.html -->
<div>
    <form action="" method="get">
        <div class="row">
            <div class="col-3">
                <select name="type" id="" class="form-control">
                    <option value="user" {% if request.GET.type == "user" %} selected {% endif %}>User</option>
                    <option value="tag" {% if request.GET.type == "tag" %} selected {% endif %}>Tag</option>
                </select>
            </div>
            <div class="col-7">
                <input type="text" name="q" placeholder="검색어" class="form-control" value="{% if request.GET.q %}{{ request.GET.q }}{% endif %}">
            </div>
            <div class="col-2">
                <button class="btn btn-primary">
                    검색
                </button>
            </div>
        </div>
    </form>
</div>

<!-- search/search_tag.html -->
{% extends 'base.html' %}
{% block content %}
    <h1>Tag</h1>
    {% include 'include/search_form.html' %}
{% endblock %}

<!-- search/search_user.html -->
{% extends 'base.html' %}
{% block content %}
    <h1>User</h1>
    {% include 'include/search_form.html' %}
{% endblock %}

<!-- search/search.html -->
{% extends 'base.html' %}
{% block content %}
    {% include 'include/search_form.html' %}
{% endblock %}
  1. views.py에서 해당검색어에 맞는 것만 출력되게 마지막으로 수정
User = get_user_model()

def search(request):
    search_type = request.GET.get('type') # user인지, tag인지
    q=request.GET.get('q')

    # type 을 정하고 그리고 html에서 name=q 로 정해서 보내준것 즉 검색어에 해당하는 것을 공통으로 해당하는 것을 가져고 오기위해
    if search_type in ['user', 'tag'] and q:
        if search_type == 'user':
            object_list = User.objects.filter(nickname__icontains=q)
        else:
            object_list = Post.objects.filter(tags__tag=q) # 완벽히 일치하는 것만

        context = {
            'object_list' : object_list,
        }

        return render(request, f'search/search_{search_type}.html', context)

    return render(request, template_name='search/search.html')
<!-- search/search_tag.html -->
{% extends 'base.html' %}
{% block content %}
    <h1>Tag</h1>
    {% include 'include/search_form.html' %}
    <div>
        <ul>
            {% for post in object_list %}
                {{ post.id }}
            {% endfor %}
        </ul>
    </div>
{% endblock %}

<!-- search/search_user.html -->
{% extends 'base.html' %}
{% block content %}
    <h1>User</h1>
    {% include 'include/search_form.html' %}
    <div>
        <ul>
            {% for user in object_list %}
                <li>{{ user.nickname }}</li>
            {% endfor %}
        </ul>
    </div>
{% endblock %}
  1. html 업그레이드
<!-- search/search_user.html -->
{% extends 'base.html' %}
{% block content %}
    <h1>User</h1>
    {% include 'include/search_form.html' %}
    <div>
        {% for user in object_list %}
            <div class="my-4">
                <a href="{% url 'profile:detail' user.nickname %}" class="text-decoration-none text-black">
                    <span class="p-2 border rounded-circle me-2">
                        <i class="fa-solid fa-user" style="width: 16px; padding-left: 3px;"></i>
                    </span>
                    {{ user.nickname }}
                </a>
            </div>
        {% endfor %}
    </div>
{% endblock %}

<!-- search/search_tag.html -->
{% extends 'base.html' %}
{% block style %}
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css"/>
{% endblock %}
{% block content %}
    <h1>Tag</h1>
    {% include 'include/search_form.html' %}
            <div class="row mt-2">
                {% for post in object_list %}
                    <div class="col-4 my-2">
                    <!-- swiper 로 만들어 줄거임 post/list.html 부분 가져오자-->
                        <div class="swiper"> <!-- 메인 swiper -->
                            <div class="border-1 swiper-wrapper"> <!-- 이미지가 여러개 있을거니까 여기에 wrapper -->
                                {% for image in post.images.all %}
                                    <div class="swiper-slide">
                                        <img class="img-fluid post-image" src="{{ image.image.url }}" alt="">
                                    </div>
                                    {% endfor %}
                            </div>
                            <div class="swiper-pagination"></div>
                        </div>
                    </div>
                {% endfor %}
            </div>
{% endblock %}
{% block js %}
    <script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
    <script>
            const swiper = new Swiper('.swiper', {
          // Optional parameters
          direction: 'horizontal',
          loop: false,

          // If we need pagination
          pagination: {
            el: '.swiper-pagination',
          },
        });
    </script>
{% endblock %}

📝 OAuth2

  • obtaining Authorization
  • OAuth2는 사용자의 자격 증명을 직접 노출하지 않고, 서드파티 애플리케이션이 사용자 리소스에 접근할 수 있도록 하는 인증 프로토콜이다.
  • 동작과정
    1. 유저는 OAuth Server에서 로그인 하게됨 (facebook,naver,..)
    2. 설정해 놓은 Redirect Url로 OAuth 서버에서 장고 서버로 던져준다. token과 함께(get 요청)
    3. 다시한번 OAuth server에 token 을 보내며 확인 요청을 한다.
    4. 장고서버에서 다시 받아온 응답 확인후 장고서버에서 OAuth 에서 받아온 정보로 로그인을 시킴
    5. 유저 페이지를 처리해줌

📌 네이버를 통해 Open API 신청, 장고 설정

  1. 네이버 개발자 센터
  2. 네이버 로그인
  3. 오픈 API 이용 신청 - 설정
  4. 이름, 사용 API 네이버 로그인 선택
  5. 연락처 email이 필요하기에 이메일 체크
  6. 환경 PC 웹
  7. 서비스 url :http://localhost:8000
  8. Callback URL : http://localhost:8000/naver/callback/
  9. 나오는 정보 .config_secret 에 보관
# secret.json
  "naver": {
    "client_id": "클라이언트 id",
    "secret": "클라이언트 시크릿"
  }
}
  1. settings.py에 설정
# OAuth
NAVER_CLIENT_ID = SECRET['naver']['client_id']
NAVER_SECRET = SECRET['naver']['secret']
  1. 요청을 하고 다시 받아와야 해서 request 패키지 필요 , poetry add requests
  2. member/oauth.views.py 생성
  3. 네이버 개발자 센터에서 Documents에 로그인의 API 명세를 보면 방법이 나와있음, 필수항목은 꼭 넣어야 하는것들임, 콜백 되는거 한번 해보자
https://nid.naver.com/oauth2.0/authorize?response_type=cdoe&client_id=ygTj6IyW3ZFhzQ5OeDHq&redirect_url=http://localhost:8000/naver/callback/&state=abc
  1. 이 페이지로 가는 동작 제작, url 연결
class NaverLoginRedirectView(RedirectView):
    # 네이버 페이지로 로그인 할 수 있게 처리
    def get_redirect_url(self, *args, **kwargs):
        # 실행 시킨 도메인 찾는 방법
        domain = self.request.scheme + '://' + self.request.META.get('HTTP_HOST', '')

        callback_url = domain + NAVER_CALLBACK_URL

        # STATE 를 암호화 시키기 위해 signing 사용
        state = signing.dumps(NAVER_STATE)

        params = {
            'response_type': 'code',
            'client_id': settings.NAVER_CLIENT_ID,
            'redirect_uri': callback_url,
            'state' : state,
        }
        return f'{NAVER_LOGIN_URL}?{urlencode(params)}' # 네이버로 보내줌 -> callback url 로 다시 보내질것임 네이버에서
    

# config/urls.py
    path('oauth/', include('member.oauth_urls')),
  1. auth/login.html 수정
{% extends 'base.html' %}
{% block content %}
    <h1 class="title">로그인</h1>
    <form method="post">
        {% csrf_token %}
        {% include 'include/form.html' %}
        <button class="btn btn-primary">로그인</button>
    </form>
    <a href="{% url 'oauth:naver_login' %}" class="btn btn-info">
        네이버 로그인
    </a>
{% endblock %}
  1. callback url 잘확인하기
  2. callback view 제작
def naver_callback(request):
    # 두가지 인자 받아오고
    code = request.GET.get('code') # callback에서 온건 code와 state가 있음
    state = request.GET.get('state')

    if NAVER_STATE != signing.loads(state): # 다시한번 로드해서 맞는지 확인
        raise Http404

    params = {
        'grant_type': 'authorization_code',
        'client_id': settings.NAVER_CLIENT_ID,
        'client_secret': settings.NAVER_SECRET,
        'code': code,
        'state': state,
    }
    
    # 다시한번 access_token 이라는 login 토큰을 받아올 수 있음
    response = requests.get(NAVER_TOKEN_URL, params=params)
    result = response.json()
    access_token = result.get('access_token')

    print('token request', result)
    # access_token 으로 profile url 을 신청하는 거임
    headers = {
        'Authorization': f'Bearer {access_token}'
    }
    
    response = requests.get(NAVER_PROFILE_URL, headers=headers)
    print('profile response', response.json())  # 네이버 프로필 API 응답 확인

    if response.status_code != 200:
        raise Http404

    # 다시 요청해서 받아오는 것
    result = response.json()
    print('profile response', result)
    email = result.get('response').get('email')

    print(email)

    user = User.objects.filter(email=email).first()

    if user:
        if not user.is_active:
            user.is_active = True
            user.save()

        login(request, user)
        return redirect(reverse('main'))
  1. nickname 생성을위한 폼제작
# member/forms.py
class NicknameForm(BootstrapModelForm):
    class Meta:
        model = User
        fields = ('nickname',)
  • 동작과정
    1. user가 해당서버에서 로그인
    2. redirect url 로 장고 서버로 보내줌
    3. 유저가 직접 보내는것을 방지하기 위해, 네이버 서버로 요청해서 응답을 받아옴
    4. 토큰을 보고 확인가능
    5. 네이버에서 access_token 을 통해 프로필을 받아와서 eamil을 받아옴
    6. 닉네임이 없어서 넣으라고 유저에게 페이지를 보여주고
    7. 회원가입 완료 후 메인페이지로 돌려줌

🧑‍💻 네이버 로그인

class NaverLoginRedirectView(RedirectView):
    # 네이버 페이지로 로그인 할 수 있게 처리
    def get_redirect_url(self, *args, **kwargs):
        # 실행 시킨 도메인 찾는 방법
        domain = self.request.scheme + '://' + self.request.META.get('HTTP_HOST', '')

        callback_url = domain + NAVER_CALLBACK_URL

        # STATE 를 암호화 시키기 위해 signing 사용
        state = signing.dumps(NAVER_STATE)

        params = {
            'response_type': 'code',
            'client_id': settings.NAVER_CLIENT_ID,
            'redirect_uri': callback_url,
            'state' : state,
        }
        return f'{NAVER_LOGIN_URL}?{urlencode(params)}' # 네이버로 보내줌 -> callback url 로 다시 보내질것임 네이버에서

def naver_callback(request):
    # 두가지 인자 받아오고
    code = request.GET.get('code') # callback에서 온건 code와 state가 있음
    state = request.GET.get('state')

    if NAVER_STATE != signing.loads(state): # 다시한번 로드해서 맞는지 확인
        raise Http404

    params = {
        'grant_type': 'authorization_code',
        'client_id': settings.NAVER_CLIENT_ID,
        'client_secret': settings.NAVER_SECRET,
        'code': code,
        'state': state,
    }

    # 다시한번 access_token 이라는 login 토큰을 받아올 수 있음
    response = requests.get(NAVER_TOKEN_URL, params=params)
    result = response.json()
    access_token = result.get('access_token')

    print('token request', result)
    # access_token 으로 profile url 을 신청하는 거임
    profile_response = get_naver_profile(access_token)
    print('profile response', result)
    email = profile_response.get('email')

    print(email)

    user = User.objects.filter(email=email).first()

    if user:
        if not user.is_active:
            user.is_active = True
            user.save()

        login(request, user)
        return redirect('main')
    return redirect(
        reverse('oauth:nickname') + f'?access_token={access_token}'
    )
    # View에서 User가 없으면 nickname을 입력하는 페이지로 가져야함, 로그인 시 nickname은 입력하지 않으니까 unique 필드인 닉네임도 얻어야함


def oauth_nickname(request):
    access_token = request.GET.get('access_token')
    if not access_token:
        return redirect('login')
    # 닉네임을 입력해야하니까 form 에 닉네임 추가
    form = NicknameForm(request.POST or None)

    if form.is_valid():
        user = form.save(commit=False)
        profile = get_naver_profile(access_token)
        email = profile.get('email')

        if User.objects.filter(email=email).exists():
            raise Http404
        user.email = profile.get('email')

        user.ia_active = True
        random_password = get_random_string(12)  # 12자리 랜덤 문자열 생성
        user.set_password(random_password)
        user.save()

        login(request, user)
        return redirect('main')

    return render(request, 'auth/nickname.html', {'form': form})


def get_naver_profile(access_token):
    headers = {
        'Authorization': f'Bearer {access_token}'
    }

    response = requests.get(NAVER_PROFILE_URL, headers=headers)
    print('profile response', response.json())  # 네이버 프로필 API 응답 확인

    if response.status_code != 200:
        raise Http404

    # 다시 요청해서 받아오는 것
    result = response.json()
    return result.get('response')

✅ 깃허브 로그인 구현

📌 1. 깃허브 OAuth2 로그인 동작 과정

  1. 사용자가 Django 애플리케이션에서 "깃허브 로그인" 버튼을 클릭
  2. 깃허브 OAuth 서버로 리디렉트되어 로그인 진행
  3. 로그인 성공 후 설정한 Redirect URL로 인증 코드 전송 (code, state 포함)
  4. Django 서버가 깃허브 OAuth 서버에 access_token 요청
  5. 받은 access_token을 이용하여 사용자 정보를 가져옴
  6. 이메일을 기반으로 로그인 처리 또는 회원가입 후 로그인

📌 2. Django 설정

1️⃣ 깃허브 개발자 설정 페이지에서 OAuth App 등록

2️⃣ secret.json 설정

{
  "github": {
    "client_id": "깃허브 클라이언트 ID",
    "secret": "깃허브 클라이언트 Secret"
  }
}

3️⃣ settings.py 등록

GITHUB_CLIENT_ID = SECRET['github']['client_id']
GITHUB_SECRET = SECRET['github']['secret']
GITHUB_CALLBACK_URL = "http://localhost:8000/oauth/github/callback/"

📌 3. 깃허브 로그인 뷰 (views.py)

1️⃣ 깃허브 로그인 페이지로 리디렉트

class GithubLoginRedirectView(RedirectView):
    def get_redirect_url(self, *args, **kwargs):
        domain = self.request.scheme + '://' + self.request.META.get('HTTP_HOST', '')
        callback_url = domain + GITHUB_CALLBACK_URL
        state = signing.dumps("github_login")

        params = {
            'client_id': settings.GITHUB_CLIENT_ID,
            'redirect_uri': callback_url,
            'state': state,
            'scope': 'user:email',
        }
        return f"https://github.com/login/oauth/authorize?{urlencode(params)}"

2️⃣ 깃허브 콜백 처리 (views.py)

def github_callback(request):
    code = request.GET.get('code')
    state = request.GET.get('state')

    if signing.loads(state) != "github_login":
        raise Http404

    params = {
        'client_id': settings.GITHUB_CLIENT_ID,
        'client_secret': settings.GITHUB_SECRET,
        'code': code,
        'state': state,
    }

    headers = {'Accept': 'application/json'}
    response = requests.post("https://github.com/login/oauth/access_token", data=params, headers=headers)
    access_token = response.json().get('access_token')

    profile_response = get_github_profile(access_token)
    email = profile_response.get('email')

    user = User.objects.filter(email=email).first()
    if user:
        login(request, user)
        return redirect('main')

    return redirect(f"{reverse('oauth:nickname')}?access_token={access_token}")

3️⃣ 깃허브 프로필 조회 함수

def get_github_profile(access_token):
    headers = {'Authorization': f'Bearer {access_token}'}
    response = requests.get("https://api.github.com/user", headers=headers)
    
    if response.status_code != 200:
        raise Http404

    return response.json()

4️⃣ URL 패턴 (urls.py)

urlpatterns = [
    path('github/login/', GithubLoginRedirectView.as_view(), name='github_login'),
    path('github/callback/', github_callback, name='github_callback'),
]

📌 Tip

  • option + enter : naver같은 이문자는 맞춤법이 틀린걸 아니다라고 딕셔너리에 저장
  • @receiver
    • @receiver 데코레이터는 Django의 시그널 프레임워크와 관련된 기능으로, 시그널을 수신하여 특정 작업을 수행하는 함수(시그널 핸들러)를 정의하는 데 사용됩니다. 시그널은 Django에서 특정 이벤트가 발생했을 때 자동으로 호출될 수 있는 기능을 제공합니다.

0개의 댓글

관련 채용 정보