728x90

나는 개발을 할 때 MacOS를 사용해왔었고, iterm2을 설치하고 oh my zsh로 커스텀하여 편리한 기능을 많이 활용할 수 있었다.

하지만 회사의 보안 정책으로 본인의 전자장비를 반입할 수 없었고 설상가상으로 회사 컴퓨터는 맥북이 아닌 윈도우11을 사용중이었다.

 

그렇다고 명령프롬프트나 git bash 등을 사용하기에는 기존에 사용하던 편리한 기능들을 사용할 수 없었고

능률이 떨어질것이라고 판단하여 윈도우에서 터미널을 사용할 수 있는 방법에 대해 연구하여 적용시켜보자 마음먹게 되었다.

 

검색 결과, WSL을 사용하면 윈도우에서 리눅스를 일종의 가상환경처럼 사용할 수 있다는 것을 알게되었고, 이를 적용시키면 윈도우에서도 충분히 기존 작업환경과 유사한 개발환경을 세팅할 수 있겠다고 판단하였다.

 

 

✅ WSL 설치

1. WSL 활성화

이 방법은 Window11 이상 or Windows 10 특정 버전 이상에서 권장된다.

PowerShell 이나 cmd 창을 관리자 권한으로 실행하여 아래 명령어를 입력한다.

 

wsl --install

 

✅ 우분투 설치 

우분투 설치가 자동으로 되는 경우도 있지만, 나의 경우는 직접 설치를 해줘야 했다.

아래 명령어를 입력하여 우분투 설치를 진행한다.

 

wsl.exe --install --no-distribution

 

이후 변경사항을 적용하기 위해 컴퓨터를 종료후 다시 실행한다. (이 과정에서 윈도우 11로 업데이트도 동시에 진행해주었다.)

재부팅 이후, 아래 명령어를 입력하여 우분투를 최초 실행하게 되면 우분투 사용자 이름과 비밀번호를 설정하게 된다.

본인이 원하는대로 설정하면 초기 설정이 완료되고, 우분투 터미널을 사용할 수 있게 된다.

 

여기까지 완료되었다면 터미널 디자인을 커스텀할 차례이다.

커스텀은 oh my zsh를 통해 진행할 것이다. 아래 동영상을 참고하여 자신이 원하는대로 커스텀을 진행한다.

 

 

✅ 1단계: zsh 설치 및 기본 셸로 설정

 
 
apt 업데이트를 진행하기 위해 명령어를 입력한다.
 
sudo apt update && sudo apt upgrade -y
 
sudo 설정이 되어있으므로 앞서 설정한 자신의 비밀번호를 입력하고 Enter를 눌러 업데이트를 진행한다.

 

oh my zsh를 사용하기 위해 zsh를 설치하고, 기본 셀로 지정한다.

 

sudo apt install -y zsh

zsh --version  # 설치 확인

 

 

정상적으로 설치가 되었다면 아래 명령어를 입력하여 기본 셸로 zsh을 설정한다.

 

chsh -s $(which zsh)

 

이후 zsh를 입력하면 아래와 같이 안내문자가 나오는데 2번을 눌러주면 된다.

 

 2번 누르기

 

✅ Homebrew 설치

 
아래 명령어를 입력하여 homebrew 설치를 진행한다.
 
 
 

 

설치가 완료되었다면, 설치된 homebrew에 대한 환경변수를 zsh에 등록해야한다. 아래 명령어를 입력하여 환경 변수를 등록한다.

 

echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"' >> ~/.zprofile
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"

 

잘 등록되었다면 brew --version을 입력하였을 때, 아래와 같이 버전이 출력되어야한다.

 

✅ oh-my-zsh 설치

이어서 oh-my-zsh를 설치해보자.
명령어를 입력하고, Y와 비밀번호를 누르면 oh-my-zsh 의 문구가 출력되고 기본 설정이 oh-my-zsh로 변경된다.

 

sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

 

 

 

초기설정이 완료되었으니 터미널을 꾸밀 차례이다.

 

나는 아래 블로그를 참고하였다.

https://kdohyeon.tistory.com/122

 

터미널 꾸미기: Oh-my-zsh + iTerm2

이번 시간에는 맥북을 처음 세팅할 때 터미널 환경을 커스텀하게 설정하는 방법에 대해서 정리합니다. iTerm2 설치 MacOS 에서 기본으로 제공하는 터미널은 다소 아쉬운 부분이 있어 보통 iTerm2 어

kdohyeon.tistory.com

 

윈도우에서는 iTerm2를 통한 터미널 꾸미기가 불가능하여 다소 단조로운 형태의 터미널이 완성되었다.

 

✅ 커스텀 명령어 추가

WSL은 리눅스 터미널을 사용할 수 있기 때문에, 맥에서 사용하던 대부분의 명령어를 사용할 수 있지만 'open .' 처럼 mac에서 제공하는 명령어는 사용이 불가능하다.

WSL의 기본 쉘인 Bash 또는 Zsh 설정 파일(~/.bashrc 또는 ~/.zshrc)에 명령어를 추가함으로서 커스텀 명령어를 추가하고 사용할 수 있다.

 

open . 은 현재 디렉토리를 파일 탐색기로 실행시키는 명령어이다.

아래와 같은 과정을 통해 커스텀 명령어를 생성할 수 있다.

 

1. zsh 설정 파일 실행

nano ~/.zshrc

 

2. 명령어 추가

alias open="explorer.exe"

 

3. 설정 반영

source ~/.zshrc

 

 

이후 원하는 디렉토리에서 테스트하여 원하는대로 동작하는지 확인한다.

이밖에 필요한 명령어가 있다면 zsh 설정파일 문법에 대해 공부하거나, GPT 등에 명령구문 작성을 요청하여 옮겨 적으면 된다.

 

✅ 빠른 실행 도구 추가

마지막으로 완성한 커스텀 터미널을 기존 명령 프롬프트처럼 빠른 실행 도구를 통해 접속해보자.

윈도우 + R 키를 누르면 실행 도구가 켜지는데, 여기서 cmd를 입력하면 명령 프롬프트가 실행된다는 것은 많은 사람들이 알고 있는 사실이다.

 

이 실행도구에 커스텀 터미널을 등록시켜 간편하게 사용하는 것이 최종 목표이다.

 

'실행' 도구는 윈도우 환경에 따라 다르나, 주로 아래 경로에 있는 프로그램의 이름을 입력할 경우 실행되는 구조를 갖고 있다.

 

C:\Windows\System32

%USERPROFILE%\AppData\Local\Microsoft\WindowsApps

 

나의 경우 System32 폴더에서 cmd.exe 프로그램을 찾을 수 있었다.(못 찾겠다면 우측 상단의 검색 도구를 사용하여 cmd를 검색해보자)

 

이 디렉토리에 원하는 프로그램을 저장시켜두면, cmd나 notepad 처럼 원하는 프로그램을 명령어를 통해 실행시킬 수 있다.

기존의 커스텀 터미널은 파워쉘을 열고,wsl 명령어를 입력하여 실행시켜야되는 번거로움이 있으므로 커스텀 프로그램을 빠른 실행 명령어에 추가하도록 하겠다.

 

먼저 커스텀 터미널에 접속할 수 있는 batch 프로그램을 만든다.

 

아래 내용을 메모장에 붙여넣고, 원하는 이름.bat 로 간단한 배치 프로그램을 생성한다.

 

@echo off
rem 사용자 이름 가져오기
set "USERNAME=%USERNAME%"
rem WSL에서 데스크탑으로 이동
wsl.exe ~ -e bash -c "cd /mnt/c/Users/%USERNAME%/Desktop && exec bash"

 

 

 

작성한 배치 프로그램을 실행하여 wsl 터미널이 실행되는지 확인하고, 앞서 확인한 경로에 붙여 넣는다.(관리자 권한 필요)

 

이 과정까지 수행하였다면 파일 이름(term)을 입력하여 cmd나 notepad 처럼 빠르게 프로그램을 실행할 수 있다.

 

 

728x90

 

VPC 접속 후 기본 VPC 및 서브넷 삭제

 

 

새로운 VPC 생성 - VPC 등 선택

 

서브넷 CIDR 블록 사용자 지정 클릭 - 세부 정보 수정, NAT 게이트웨이, VPC 엔드포인트 없음으로 설정

 

VPC 생성 후 - 기본 라우팅 테이블로 이동

 

라우팅 테이블 - 라우팅 편집 선택

 

라우팅 추가 - 인터넷 게이트웨이 deploy-igw 선택

 

서브넷에 아래와 같이 연결되었는지 확인

 

보안 그룹 - 보안 그룹 생성 선택

 

ec2용 보안 그룹 생성 

보안 그룹 이름과 설명에는 나중에 알아보기 쉽도록 적절한 이름 설정, VPC는 앞서 만든 deploy-vpc 선택

 

인바운드 규칙 추가

HTTP, SSH, 사용자 지정 TCP(서버 포트 설정용)

 

DB용 보안 그룹 생성

DB용으로 생성했다는 사실을 기억하기 쉬운 이름 설정 후, EC2 보안그룹과 동일하게 VPC 선택

인바운드 규칙에 MYSQL 관련 유형 선택 후 생성

 

 

RDS -  서브넷 그룹 - DB 서브넷 그룹 생성 선택

 

세부 정보 입력 후 VPC 에 앞서 생성한 VPC 선택

 

서브넷 추가 - 2a, 2b 가용 영역 선택

'Backend' 카테고리의 다른 글

[SpringBoot] Spring Securtiy JWT(2)  (1) 2024.06.22
[SpringBoot] Spring Securtiy JWT(1)  (0) 2024.06.18
[SpringBoot] Spring Securtiy (2)  (1) 2024.06.14
[SpringBoot] Spring Securtiy (1)  (0) 2024.06.11
[Spring] Spring Initializr Naming Rule  (0) 2024.06.08
728x90

해당 포스트는 유튜브 '개발자 유미' 님의 스프링 시큐리티 JWT 강의 영상을 바탕으로 작성하였습니다.

https://www.youtube.com/playlist?list=PLJkjrxxiBSFCcOjy0AAVGNtIa08VLk1EJ

 

스프링 시큐리티 JWT

JWT 방식 인증을 위한 스프링 시큐리티 구현 방법 (Spring Security JWT)

www.youtube.com

 

7. JWT 발급 및 검증 클래스

JWT는 사용자 정보를 내포하는 토큰으로 인증된 사용자인지 식별하는 용도로 사용된다. 

로그인에 성공하게 되면 서버는 클라이언트에게 JWT를 발급하고, 클라이언트는 서버에 요청을 할 때마다 JWT를 함께 전송함으로서 서버는 요청을 하는 사용자가 인증된 사용자인지, 해당 요청에 대한 접근 권한이 있는지 등을 검토할 수 있게 된다.

 

이때 서버는 클라이언트가 전송한 JWT가 신뢰가능한지 검증해야 한다.(실제로 서버에서 발급해준 토큰인지, 유효기간이 지나지는 않았는지 등)

따라서 JWT를 발급하고 검증하는 JWTUtil 이라는 클래스를 생성하여 JWT를 발급하고 검증하는 메소드를 작성하도록 한다.

 

그전에 JWT가 무엇인지 이해를 하는 것이 중요하다. 

공식문서와 도움이 될만한 유튜브 영상을 첨부하니 참고하면 좋을 것 같다.

 

https://jwt.io/

 

JWT.IO

JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.

jwt.io

 

https://www.youtube.com/watch?v=XXseiON9CV0

 

JWT는 . 을 기준으로 Header, PAYLOAD, VERIFY SIGNATURE 로 분리된다.

 

HEADER

- 해당 토큰이 JWT 임을 명시한다.

- 해당 토큰을 암호화할때 사용한 암호화 알고리즘을 명시한다. ( "alg" : "HS256")

 

PAYLOAD

- 실질적인 정보가 담기는 부분이다.

- 예) 사용자 이름, 나이, 권한, 등

 

VERIFY SIGNATURE

JWT를 발행한 서버, 혹은 시크릿 키를 보유중인 서버에서만 JWT를 사용할 수 있도록 Header와 Payload, 마지막으로 시크릿 키를  Header에서 명시한 암호화 알고리즘을 바탕으로 암호화한 값

암호화 알고리즘(BASE64(Header) + BASE64(Payload) + 시크릿 키)

 

JWT는 내부 정보를 BASE64방식으로 인코딩 하기 때문에 서버가 아니더라도 외부에서 쉽게 디코딩 가능하다.

따라서 외부에서 열람해도 되는 정보(사용자 이름, 권한, 등)만을 담아야하며, 토큰의 발급처를 확인하기 위해서 사용한다.

 

 

JWT 암호화 방식

 

해당 프로젝트에서는 JWT 암호화를 위해 양방향 암호화 방식, 그 중 대칭키 방식으로 진행할 것이다.

 

- 대칭키 방식 : 동일한 키를 사용하여 암호화와 복호화를 사용한다. 예) HS256

- 비대칭키 방식 : 암호화와 복호화에 사용되는 키가 서로 다르다.

 

시크릿 키 생성을 위해 터미널에서 아래 명령어를 입력한다.

 

생성된 시크릿 키는 application.properties에 임의의 변수명을 작성하여 저장한다.

spring.application.name=new-jwt-practice

# DATABASE Connection
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/jwt-practice
spring.datasource.username=root
spring.datasource.password=1234

# JPA
spring.jpa.hibernate.ddl-auto=none
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

logging.level.org.springframework.security=DEBUG

# Secret Key 설정
# 아래 명령어를 통해 무작위 시크릿 키를 생성할 수 있다.
# openssl rand -hex 32
# spring.jpa.secret은 임의의 변수명에 해당한다. 단 jpa에서 제공하는 변수명과 겹치면 안된다.
spring.jpa.secret = 26b2386c5f8a815df8676200e24cbbd03af84150c9b2bd645db9ba4df126b704

 

이제 시크릿키를 사용하여 JWT를 발급하고,JWT가 유효한지 검증하는 등의 역할을 수행할 유틸 클래스를 작성해야한다.

 

JwtUtil.java

package com.example.new_jwt_practice.jwt.util;

import io.jsonwebtoken.Jwts;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Date;

// JWTUtil 0.12.3
// JWT를 검증할 메소드, 생성할 메소드를 구현
// username, role, 생성일, 만료일 확인용 메소드
// => 생성 : 토큰의 Payload에 username, role, 생성일, 만료일을 저장
@Component
public class JwtUtil {
    private SecretKey secretKey;

    // application.properties에 저장된 시크릿키를 대상으로 객체 키 생성
    public JwtUtil(@Value("${spring.jpa.secret}") String secret) {
        secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
    }

    // 토큰을 통해 사용자 이름 추출
    public String getUsername(String token) {

        return Jwts.parser()
                .verifyWith(secretKey) // 시크릿 키를 사용하여 복호화한다.
                .build() // 복호화에 사용할 Jwts Parser 생성
                // 생성된 Parser를 사용하여 token으로 부터 정보 추출
                .parseSignedClaims(token)
                .getPayload().get("username", String.class);
    }

    // 토큰으로부터 사용자 권한 추출
    public String getRole(String token) {

        return Jwts.parser()
                .verifyWith(secretKey)
                .build()
                .parseSignedClaims(token)
                .getPayload()
                .get("role", String.class);
    }

    // 토큰이 만료되었는지 확인, 오늘 날짜와 토큰에 저장된 만료일자를 비교한다.
    // 만료되었다면 true
    public Boolean isExpired(String token) {

        return Jwts.parser()
                .verifyWith(secretKey)
                .build()
                .parseSignedClaims(token)
                .getPayload()
                .getExpiration()
                .before(new Date());
    }

    // 토큰 생성 메소드
    // 사용자 이름, 권한, 토큰 유지 시간을 매개변수로 전달받는다.
    public String createJwt(String username, String role, Long expiredMs) {

        return Jwts.builder()
                .claim("username", username)
                .claim("role", role)
                .issuedAt(new Date(System.currentTimeMillis()))
                .expiration(new Date(System.currentTimeMillis() + expiredMs))
                .signWith(secretKey)
                .compact();
    }


}

 

@Value를 사용하여 application.properties 에 작성해둔 시크릿키를 불러와 Secret 클래스 sercetKey에 값을 할당한다.

위 과정은 @Component에 의해 컴포넌트 스캔시 생성자가 호출되며 진행된다.

 

위 클래스는 createJwt()를 사용하여 사용자의 username, role 에 대한 정보를 JWT에 담아 반환하며 매개변수로 받은 expiredMs 시간 만큼을 JWT의 유효시간으로 설정한다.

이후 isExpired()를 통해 토큰의 유효기간을 검증하며, getUsername(), getRole() 을 사용하여 JWT에 담겨진 사용자 정보를 얻어온다.

 

JWT의 Payload에 정보가 담기게 되며, 정보를 담는 기본 단위는 claim이다. 따라서 .claim() 을 사용하여 사용자가 주입하고 싶은 정보를 JWT의 Payload에 저장할 수 있으며, parseSignedClaims(token) 을 사용하여 해당 JWT에 대한 클레임 정보들을 파싱할 수 있다.

 

8. 로그인 성공시 JWT 발급 

앞서서 사용자가 로그인에 성공하였을 때 서버에서 JWT를 발급해서 반환해주고, 이후 사용자는 JWT를 매 요청마다 Header에 첨부하게 된다고 하였다. 

그렇다면 이제부터 사용자가 로그인에 성공하였을 때 JWT를 발급하는 코드를 작성해보자.

 

/login요청은 UsernamePasswordAuthenticationFilter를 대체하는 LoginFilter에 의해 수행되며, 해당 클래스를 상속받고 있기 때문에 successfulAuthentication() 과 unsuccessfulAuthentication()을 override하고 있는 상태이다.

위 두 함수들은 로그인필터에 의해 사용자가 로그인에 성공하였을 때와 실패하였을 때 실행되며 이 함수들에 JWT를 발급하는 코드와 실패시 오류코드를 반환하는 코드를 작성하면 된다.

 

LoginFilter.java

package com.example.new_jwt_practice.jwt.filter;

import com.example.new_jwt_practice.jwt.dto.CustomUserDetails;
import com.example.new_jwt_practice.jwt.util.JwtUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;

// 폼 로그인 방식의 경우 스프링 시큐리티 필터들 중 UsernamePasswordAuthenticationFilter에 의해 수행된다.
// 해당 필터는 AuthenticationManager를 사용하여 사용자의 인증절차를 수행하는데
// 이때 UserDetailsService가 제공하는 UserDetails를 사용한다.
// 해당 프로젝트의 경우 폼 로그인 방식을 disable하였으므로
// 사용자의 로그인 요청을 수행하기 위한 UsernamePasswordAuthenticationFilter를 커스텀하여 로그인 요청을 수행한다.
@RequiredArgsConstructor
public class LoginFilter extends UsernamePasswordAuthenticationFilter {

    private final AuthenticationManager authenticationManager; // 인증 절차를 수행할 때 사용

    // 사용자 로그인 성공시 JWT 토큰을 발급해주기 위해 JwtUtil을 주입받는다.
    private final JwtUtil jwtUtil;

    @Override
    public Authentication attemptAuthentication(
            HttpServletRequest request,
            HttpServletResponse response) throws AuthenticationException {

        // 클라이언트 요청에서 username, passowrd 추출
        String username = obtainUsername(request);
        String password = obtainPassword(request);

        // 스프링 시큐리티에서 username과 password를 검증하기 위해서는 토큰에 담아야한다.
        // username, password, authorities
        UsernamePasswordAuthenticationToken authToken
                = new UsernamePasswordAuthenticationToken(username, password,null);

        // 토큰 정보를 바탕으로 검증 수행 및 결과 반환
        // 검증은 AuthenticationManager가 UserDetails를 바탕으로 수행한다.
        // DB의 정보를 바탕으로 UserDetailsService에 의해 UserDetails가 생성되면 AuthenticationManager가 이를 토대로 검증한다.
        Authentication authentication = authenticationManager.authenticate(authToken);

        return authentication;
    }

    // 로그인 성공시 실행
    // 로그인 성공시 사용자 정보를 담고 있는 JWT 토큰을 발급 해준다.
    @Override
    protected void successfulAuthentication(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain chain, Authentication authResult) throws IOException, ServletException {

        // 로그인 성공시, 사용자에게 토큰 발급
        CustomUserDetails customUserDetails = (CustomUserDetails) authResult.getPrincipal();
        // 사용자 이름 추출
        String username = customUserDetails.getUsername();
        // 사용자 권한 추출
        Collection<? extends GrantedAuthority> authorities = customUserDetails.getAuthorities();
        Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
        GrantedAuthority auth = iterator.next();
        String role = auth.getAuthority();

        String token = jwtUtil.createJwt(username, role, 60 * 60 * 10 * 10L);

        // HTTP 반환 헤더에 "Authorization" 키 값으로 토큰 반환
        response.addHeader("Authorization","Bearer " + token);

    }

    // 로그인 실패시 실행
    @Override
    protected void unsuccessfulAuthentication(
            HttpServletRequest request,
            HttpServletResponse response,
            AuthenticationException failed) throws IOException, ServletException {
        // 실패 코드 반환
        response.setStatus(401);
    }
}

 

앞서 작성한 JwtUtil 클래스를 사용하기 위해 JwtUtil 클래스를 주입받았다.

로그인에 성공할경우 Authentication 객체로부터 사용자의 이름과 권한을 추출하여 JwtUtil의 createJwt() 를 통해 JWT를 발급받은후 response의 헤더부분에 "Authorization"이라는 키로 "Bearer {JWT토큰값}" 의 형태로 JWT를 삽입한다.

로그인에 실패할 경우 401코드를 반환하도록 설정한다.

 

포스트맨을 통해 테스트를 진행해보면 로그인 성공시 리턴 Response의 Header에 JWT가 반환되는 것을 확인할 수 있다.

 

9. JWT 검증 필터

사용자는 모든 요청에 대해서 앞으로 Header에 JWT를 넘겨줌으로서 서버는 사용자의 인증,인가를 진행하게 된다고 하였다.

그렇다면 사용자가 첨부한 JWT를 검증하는 과정이 필요할 것이다.

JWTFilter를 작성하여 위 과정을 진행한다.

package com.example.new_jwt_practice.jwt.filter;

import com.example.new_jwt_practice.entity.Member;
import com.example.new_jwt_practice.jwt.dto.CustomUserDetails;
import com.example.new_jwt_practice.jwt.util.JwtUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

// 사용자가 JWT를 첨부하여 요청시, JWT를 바탕으로 사용자 정보를 확인하여 인가처리를 수행한다.
// 이때 JWT를 검증(올바르게 생성된 토큰인지, 올바른 권한을 보유중인지, 등)하고, 한번만 사용할 임시 세션을 만들어 요청을 수행한다.
// STATELESS하게 관리되므로 한번의 요청이후 해당 토큰은 사라진다.
@RequiredArgsConstructor
public class JWTFilter extends OncePerRequestFilter {

    // JWT 검증용
    private final JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {
        // 토큰 추출
        String authorization = request.getHeader("Authorization");
        // 토큰 유효성 검증
        // - Bearer 접두사가 있는지, 토큰이 실제로 존재하는지, 등
        if (authorization == null || !authorization.startsWith("Bearer ")) {
            System.out.println("token null");
            filterChain.doFilter(request, response);
            return;
        }
        // Bearer 접두사 제거
        String token = authorization.split(" ")[1];

        // 토큰 소멸시간 검증
        if (jwtUtil.isExpired(token)) {
            System.out.println("token expired");
            filterChain.doFilter(request,response);
            return;
        }

        // 토큰에서 username, role 획득
        String username = jwtUtil.getUsername(token);
        String role = jwtUtil.getRole(token);

        // 사용자 객체 생성 및 값 주입
        Member member = Member.builder()
                .username(username)
                .role(role)
                .build();

        // UserDetails에 회원 정보 객체 담기
        CustomUserDetails customUserDetails = new CustomUserDetails(member);

        // 스프링 시큐리티 인증 토큰 생성 => 해당 토큰을 바탕으로 AuthenticationManager에서 사용자 권한 검증 수행
        Authentication authToken
                = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());
        // 세션에 사용자 등록
        SecurityContextHolder.getContext().setAuthentication(authToken);

        filterChain.doFilter(request,response);
    }
}

 

JWTFilter는 OncePerRequestFilter를 상속받음으로서 Security Filter로 등록가능하다.

OncePerRequestFilter의 doFilterInternal을 override 하여 토큰의 유효성에 대해 검증하며 매개변수로 전달받은 HttpServletRequest에서 "Authorization" 헤더를 추출하여 토큰을 추출한뒤 JwtUitl을 주입받아, JwtUtil에 구현해둔 함수를 사용하여 토큰에 대한 유효성을 검증한다.

만약 토큰에 문제가 발생할 경우 filterChain.doFilter()를 호출하여 필터체인의 다른 필터들로 요청을 넘기고 함수를 종료한다.

만약 토큰에 문제가 없다면 jwtUtil을 사용하여 토큰의 클레임에서 사용자 정보를 추출하고 UserDetails 객체를 만들어 AuthenticationManager에서 사용할수 있도록 스프링 시큐리티 인증 토큰을 생성하여 반환한다.

 

기본적으로 JWT를 사용한 인가처리 방식의 경우 사용자 정보를 세션에 저장하지 않는 STATELESS 방식이지만 사용자의 요청을 처리하기 위해서는 결국 하나의 세션이 필요하다. 그렇기 때문에 JWT정보를 바탕으로 사용자의 요청이 끝날때 까지 유효한 임시 세션을 만들어 사용자 정보를 등록시켜 사용하게 되는데, 이를 위해 SecruityContextHolder에 setAuthentication을 호출하여 사용자 정보를 세션으로 만들어 등록한다. 추후 SecurityContextHolder를 통해 등록해둔 사용자 정보를 추출하여 사용할 수 있게 된다.

해당 세션은 사용자 요청이 종료되는 순간 삭제되어 STATELESS한 상태가 된다.

 

추가로 작성한 필터를 등록시키는 과정이 필요한데, SecurityConfig의 addFilterBefore()를 통해 로그인 필터 이전에 위치시키도록 한다.

 

SecurityConfig.class

package com.example.new_jwt_practice.jwt.config;

import com.example.new_jwt_practice.jwt.filter.JWTFilter;
import com.example.new_jwt_practice.jwt.filter.LoginFilter;
import com.example.new_jwt_practice.jwt.util.JwtUtil;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;

import java.util.Collections;

@Configuration
@EnableWebSecurity // 시큐리티를 위한 구성파일임을 알림
@RequiredArgsConstructor
public class SecurityConfig {

    // AuthenticationManager의 AuthenticationConfiguration에서 사용하기 위함
    private final AuthenticationConfiguration authenticationConfiguration;

    // LoginFilter의 RequireArgsConstructor에 의해 생성자 주입이 발생,
    // 따라서 SecurityConfig에서 LoginFilter 생성자 호출시 JwtUtil 주입 필요
    private final JwtUtil jwtUtil;

    // 사용자 비밀번호를 관리하기 위한 암호화 클래스
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 커스텀한 UsernamePasswordAuthentication Filter에서 사용하기 위해 Authentication 빈 등록
    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }


    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        // csrf disable
        http
                .csrf(auth -> auth.disable()); // JWT 방식은 세션을 Stateless로 관리하기 때문에 csrf 공격에 비교적 안전하다.

        // JWT 방식을 사용할 것이므로 이외의 방식은 모두 비활성화 한다.
        // Form 로그인 방식 disable
        http
                .formLogin(auth -> auth.disable());

        // http basic 인증 방식 disable
        http
                .httpBasic(auth -> auth.disable());

        // 경로별 인가 설정
        http
                .authorizeHttpRequests(auth -> auth
                        // login, root, join 경로의 요청에 대해서는 모두 허용
                        .requestMatchers("/login", "/", "/join").permitAll()
                        // admin 경로 요청에 대해서는 ADMIN 권한을 가진 경우에만 허용
                        .requestMatchers("/admin").hasRole("ADMIN")
                        // 이외의 요청에 대해서는 인증된 사용자만 허용
                        .anyRequest().authenticated()
                );

        // 커스텀한 UsernamePasswordAuthenticationFilter를 사용하도록 알림
        // addFilterAt : 해당 필터를 대체한다.(그 자리를 대체한다.)
        // addFilterAt의 2번째 인자는 어떤 위치를 대체할것인지 해당 필터를 작성한다.(이 경우 Username.. Filter)
        http
                .addFilterAt(new LoginFilter
                                (authenticationManager(authenticationConfiguration),jwtUtil),
                        UsernamePasswordAuthenticationFilter.class);

        // 로그인 필터 앞에 JWT 검증용 필터 추가
        http
                .addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class);

        // 가장 중요
        // JWT 방식에서는 세션을 Stateless 상태로 관리하게 됨(서버 세션에 저장하지 않음)
        // 따라서 세션을 STATELESS 상태로 설정 해주어야 한다.
        http
                .sessionManagement(session -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        return http.build();
    }
}

 

 

10. 세션 정보

앞서 설명한대로, JWT를 바탕으로 임시 세션을 만들어서 사용자 요청이 진행되는 동안 세션에 사용자 정보를 보관한다.

MainController에서 사용자 정보를 추출하여 반환하는 코드를 작성해보자.

 

MainController.java

package com.example.new_jwt_practice.controller;

import lombok.Builder;
import lombok.Data;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collection;
import java.util.Iterator;

@RestController
public class MainController {

    @Data
    @Builder
    public static class MemberInfo {
        private String username; // 사용자 이름
        private String role; // 권한
    }


    @GetMapping("/")
    public MemberInfo mainP() {
        // JWT는 STATELESS한 방식으로 구동되긴 하지만 일시적으로 세션을 만들어 사용한다.
        // 따라서 세션을 통해 JWT에 내포된 사용자 정보를 추출할 수 있다.
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String name = authentication.getName();
        // 권한 추출
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
        GrantedAuthority auth = iterator.next();
        String authority = auth.getAuthority();

        MemberInfo memberInfo = MemberInfo.builder()
                .username(name)
                .role(authority)
                .build();

        return memberInfo;
    }
}

 

SecurityContextHolder.getContext() 에서 getAuthentication()을 호출하여 Authentication 객체를 얻어와 사용자의 이름과 권한을 추출한다. 이 API 에서는 사용자의 이름과 권한을 반환하도록 작성해보았다.

 

포스트맨을 통해 직접 테스트 해보면 Header에 첨부한 토큰 내용을 바탕으로 사용자 정보를 추출하여 반환하는 것을 확인할 수 있다.

 

11. CORS 설정

서버 API를 호출할 때, 웹 브라우저의 경우 교차 출저 리소스 정책을 준수하지 않을 경우 CORS 문제가 발생한다.

https://ko.wikipedia.org/wiki/%EA%B5%90%EC%B0%A8_%EC%B6%9C%EC%B2%98_%EB%A6%AC%EC%86%8C%EC%8A%A4_%EA%B3%B5%EC%9C%A0

 

교차 출처 리소스 공유 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 교차 출처 리소스 공유(Cross-origin resource sharing, CORS), 교차 출처 자원 공유는 웹 페이지 상의 제한된 리소스를 최초 자원이 서비스된 도메인 밖의 다른 도메인으

ko.wikipedia.org

 

이 문제를 방지하기 위해 서버에서 특정 출저에 대해 허용해주도록 코드를 작성해주어야 한다.

우리의 프로젝트의 경우 스프링 시큐리티 필터를 거치고, Controller 단을 거쳐서 들어가기 때문에 해당 출저에 대해 아래 코드들을 작성해주어야 한다.

 

CorsMvcConfig.java

@Configuration
public class CorsMvcConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry corsRegistry) {

        corsRegistry.addMapping("/**")
                .allowedOrigins("http://localhost:3000");
    }
}

 

SecurityConfig.java

package com.example.new_jwt_practice.jwt.config;

import com.example.new_jwt_practice.jwt.filter.JWTFilter;
import com.example.new_jwt_practice.jwt.filter.LoginFilter;
import com.example.new_jwt_practice.jwt.util.JwtUtil;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;

import java.util.Collections;

@Configuration
@EnableWebSecurity // 시큐리티를 위한 구성파일임을 알림
@RequiredArgsConstructor
public class SecurityConfig {

    // AuthenticationManager의 AuthenticationConfiguration에서 사용하기 위함
    private final AuthenticationConfiguration authenticationConfiguration;

    // LoginFilter의 RequireArgsConstructor에 의해 생성자 주입이 발생,
    // 따라서 SecurityConfig에서 LoginFilter 생성자 호출시 JwtUtil 주입 필요
    private final JwtUtil jwtUtil;

    // 사용자 비밀번호를 관리하기 위한 암호화 클래스
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 커스텀한 UsernamePasswordAuthentication Filter에서 사용하기 위해 Authentication 빈 등록
    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }


    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{

        // CORS 동일출저 정책 방지용 설정 작성
        http
                .cors((corsCustomizer -> corsCustomizer.configurationSource(new CorsConfigurationSource() {

                    @Override
                    public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {

                        CorsConfiguration configuration = new CorsConfiguration();
                        // 로컬호스트 3000번 포트의 요청에 대해서
                        configuration.setAllowedOrigins(Collections.singletonList("http://localhost:3000"));
                        // GET, POST, 등 모든 요청 허용
                        configuration.setAllowedMethods(Collections.singletonList("*"));
                        configuration.setAllowCredentials(true);
                        configuration.setAllowedHeaders(Collections.singletonList("*"));
                        configuration.setMaxAge(3600L);
                        // JWT가 Authorization 헤더에 담기므로
                        configuration.setExposedHeaders(Collections.singletonList("Authorization"));

                        return configuration;
                    }
                })));

        // csrf disable
        http
                .csrf(auth -> auth.disable()); // JWT 방식은 세션을 Stateless로 관리하기 때문에 csrf 공격에 비교적 안전하다.

        // JWT 방식을 사용할 것이므로 이외의 방식은 모두 비활성화 한다.
        // Form 로그인 방식 disable
        http
                .formLogin(auth -> auth.disable());

        // http basic 인증 방식 disable
        http
                .httpBasic(auth -> auth.disable());

        // 경로별 인가 설정
        http
                .authorizeHttpRequests(auth -> auth
                        // login, root, join 경로의 요청에 대해서는 모두 허용
                        .requestMatchers("/login", "/", "/join").permitAll()
                        // admin 경로 요청에 대해서는 ADMIN 권한을 가진 경우에만 허용
                        .requestMatchers("/admin").hasRole("ADMIN")
                        // 이외의 요청에 대해서는 인증된 사용자만 허용
                        .anyRequest().authenticated()
                );

        // 커스텀한 UsernamePasswordAuthenticationFilter를 사용하도록 알림
        // addFilterAt : 해당 필터를 대체한다.(그 자리를 대체한다.)
        // addFilterAt의 2번째 인자는 어떤 위치를 대체할것인지 해당 필터를 작성한다.(이 경우 Username.. Filter)
        http
                .addFilterAt(new LoginFilter
                                (authenticationManager(authenticationConfiguration),jwtUtil),
                        UsernamePasswordAuthenticationFilter.class);

        // 로그인 필터 앞에 JWT 검증용 필터 추가
        http
                .addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class);

        // 가장 중요
        // JWT 방식에서는 세션을 Stateless 상태로 관리하게 됨(서버 세션에 저장하지 않음)
        // 따라서 세션을 STATELESS 상태로 설정 해주어야 한다.
        http
                .sessionManagement(session -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        return http.build();
    }
}

 

이렇게 작성하게 되면 CORS 문제를 방지할 수 있다.

'Backend' 카테고리의 다른 글

[AWS] 네트워크 구축  (0) 2024.12.08
[SpringBoot] Spring Securtiy JWT(1)  (0) 2024.06.18
[SpringBoot] Spring Securtiy (2)  (1) 2024.06.14
[SpringBoot] Spring Securtiy (1)  (0) 2024.06.11
[Spring] Spring Initializr Naming Rule  (0) 2024.06.08

+ Recent posts