[TIL] Day23_Authentication & Authorization

오진선·2024년 2월 27일
0

TIL

목록 보기
14/29
post-thumbnail

Today I Learned

1. Authentication & Authorization

1) 인증(Authentication)

  • 해당 유저가 실제 유저인지 인증하는 절차
  • 예) 지문인식, 로그인

2) 인가(Authorization)

  • 해당 유저가 특정 리소스에 접근이 가능한지 허가를 확인하는 개념 (권한을 받는 것)
  • 예) 관리자 페이지-관리자 권한, 회원-비회원 권한

3) 웹 애플리케이션 인증

  • 서버-클라이언트 구조
  • Http 라는 프로토콜을 이용
  • 비연결성(Connectionless) 무상태(Stateless) 통신

    비연결성(Connectionless)
    서버와 클라이언트가 연결되어 있지 않다
    리소스 절약이 목적
    서버는 하나의 요청에 하나의 응답만 -> 이후 연결을 끊음

    무상태(Stateless)
    서버가 클라이언트의 상태를 저장하지 않는다
    서버의 비용과 부담 감소를 위함
    서버는 클라이언트가 직전에, 혹은 그 전에 어떠한 요청을 보냈는지 알지 못함

4) 인증의 방식

(1) 쿠키-세션 방식 인증

  • 서버가 ‘특정 유저가 로그인 되었다’는 상태를 저장하는 방식
  • 인증과 관련된 최소한의 정보는 저장해서 로그인을 유지

    쿠키-세션 방식 인증 절차

    a. 사용자가 로그인 요청
    b. 서버에서 DB의 아이디 비밀번호 대조
    c. 실제 유저테이블의 정보와 일치 -> 인증을 통과한 것
    -> “세션 저장소”에 해당 유저가 로그인 되었다는 정보를 넣음
    d. 세션 저장소에서는 유저의 정보와는 관련 없는 난수인 session-id를 발급
    e. 서버는 로그인 요청의 응답으로 session-id를 내어줌
    f. 클라이언트는 session-id를 쿠키라는 저장소에 보관 후 앞으로의 요청마다 세션아이디를 같이 보냄 (주로 HTTP header에 담음)
    g. 클라이언트의 요청에서 쿠키를 발견했다면 서버는 세션 저장소에서 검증
    h. 유저정보를 받아왔다면 이후 로그인 된 유저에 따른 응답을 내어줌

(2) JWT 기반 인증

  • JWT(JSON Web Token) : 인증에 필요한 정보들을 암호화시킨 토큰
  • JWT Access Token을 HTTP 헤더에 실어 서버가 클라이언트를 식별

    JWT 기반 인증 절차

    a. 사용자가 로그인 요청
    b. 서버에서 DB의 아이디 비밀번호 대조
    c. 실제 유저테이블의 정보와 일치 -> 인증을 통과한 것
    -> 유저의 정보를 JWT로 암호화 해서 내보냄
    d. 서버는 로그인 요청의 응답으로 jwt 토큰을 내어줌
    e. 클라이언트는 jwt 토큰을 저장소에 보관, 앞으로의 요청마다 토큰을 같이 보냄
    f. 클라이언트의 요청에서 토큰 발견시 서버가 토큰을 검증
    g. 이후 로그인 된 유저에 따른 응답을 내어줌

2. 쿠키와 세션

  • HTTP 에 상태 정보를 유지(Stateful)하기 위해 사용
  • 클라이언트 별로 인증 및 인가 가능

  • 클라이언트에 저장될 목적으로 생성한 작은 정보를 담은 파일
  • Application - Storage - Cookies 에서 도메인 별로 저장되어 있는 쿠키 확인 가능
  • 구성요소
    - Name (이름): 쿠키를 구별하는 데 사용되는 키 (중복❌)
    - Value (값): 쿠키의 값
    - Domain (도메인): 쿠키가 저장된 도메인
    - Path (경로): 쿠키가 사용되는 경로
    - Expires (만료기한): 쿠키의 만료기한 (만료기한 지나면 삭제)

(2) Session

  • 서버에서 일정시간 동안 클라이언트 상태를 유지하기 위해 사용
  • 서버에서 클라이언트 별로 유일무이한 '세션 ID' 를 부여한 후 클라이언트 별 필요한 정보를 서버에 저장
  • 서버에서 생성한 '세션 ID' 는 클라이언트의 쿠키값('세션 쿠키' 라고 부름)으로 저장되어 클라이언트 식별에 사용

    Session 동작 방식

    클라이언트가 서버에 1번 요청
    👉 서버가 세션ID 생성, 쿠키에 담아 응답 헤더에 전달
    (세션 ID 형태: "SESSIONID = 12A345")
    👉 클라이언트가 쿠키에 세션ID를 저장 ('세션쿠키')
    👉 클라이언트가 서버에 2번 요청 (쿠키값 (세션 ID) 포함하여 요청)
    👉 서버가 세션ID 를 확인하고, 1번 요청과 같은 클라이언트임을 인지

(3) Cookie와 Session 비교

2) 쿠키 다루기

(1) 쿠키 생성

public static void addCookie(String cookieValue, HttpServletResponse res) {
    try {
        cookieValue = URLEncoder.encode(cookieValue, "utf-8").replaceAll("\\+", "%20"); // Cookie Value 에는 공백이 불가능해서 encoding 진행

        Cookie cookie = new Cookie(AUTHORIZATION_HEADER, cookieValue); // Name-Value
        cookie.setPath("/");
        cookie.setMaxAge(30 * 60);

        // Response 객체에 Cookie 추가
        res.addCookie(cookie);
    } catch (UnsupportedEncodingException e) {
        throw new RuntimeException(e.getMessage());
    }
}
  • new Cookie(AUTHORIZATION_HEADER, cookieValue);
    • Cookie에 저장될 Name과 Value를 생성자로 받는 Cookie 객체를 생성
  • setPath("/"), setMaxAge(30 * 60)
    • Path와 만료시간을 지정
  • HttpServletResponse 객체에 생성한 Cookie 객체를 추가하여 브라우저로 반환
    • 반환된 Cookie는 브라우저의 Cookie 저장소에 저장
  • Cookie 생성은 범용적으로 사용될 수 있기 때문에 static 메서드로 선언

(2) 쿠키 읽기

@GetMapping("/get-cookie")
public String getCookie(@CookieValue(AUTHORIZATION_HEADER) String value) {
    System.out.println("value = " + value);

    return "getCookie : " + value;
}
  • @CookieValue("Cookie의 Name")
    • Cookie의 Name 정보 전달 -> 해당 정보를 토대로 Cookie의 Value를 가져옴

3) Session 다루기

(1) Session 생성

@GetMapping("/create-session")
public String createSession(HttpServletRequest req) {
    // 세션이 존재할 경우 세션 반환, 없을 경우 새로운 세션을 생성한 후 반환
    HttpSession session = req.getSession(true);

    // 세션에 저장될 정보 Name - Value 를 추가합니다.
    session.setAttribute(AUTHORIZATION_HEADER, "Robbie Auth");

    return "createSession";
}
  • HttpServletRequest를 사용하여 세션을 생성 및 반환 가능
  • req.getSession(true)
    • 세션이 존재할 경우 -> 세션을 반환
    • 없을 경우 -> 새로운 세션을 생성
  • 세션에 저장할 정보를 Name-Value 형식으로 추가
  • 반환된 세션은 브라우저 Cookie 저장소에 ‘JSESSIONID’라는 Name으로 Value에 저장

(2) Session 읽기

@GetMapping("/get-session")
public String getSession(HttpServletRequest req) {
    // 세션이 존재할 경우 세션 반환, 없을 경우 null 반환
    HttpSession session = req.getSession(false);

    String value = (String) session.getAttribute(AUTHORIZATION_HEADER); // 가져온 세션에 저장된 Value 를 Name 을 사용하여 가져옵니다.
    System.out.println("value = " + value);

    return "getSession : " + value;
}
  • req.getSession(false)
    • 세션이 존재 -> 세션을 반환
    • 없을 경우 -> null을 반환
  • session.getAttribute(”세션에 저장된 정보 Name”)
    • Name을 사용하여 세션에 저장된 Value 가져옴

3. JWT

1) JWT란?

(1) JWT(JSON Web Token)

  • JWT(Json Web Token) : JSON 포맷을 이용하여 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token
  • 일반적으로 쿠키 저장소를 사용하여 JWT를 저장

(2) 사용 이유

  • 동시 접속자가 많을 때 서버 측 부하 낮춤
  • Client, Sever 가 다른 도메인을 사용할 때 ex) 카카오 OAuth2 로그인
  • 모든 서버에서 동일한 Secret Key 소유
  • Secret Key 통한 암호화 / 위조 검증 👇👇

  • 단점
    • 구현의 복잡도 증가
    • JWT 에 담는 내용이 커질 수록 네트워크 비용 증가 (클라이언트 → 서버)
    • 기 생성된 JWT 를 일부만 만료시킬 방법이 없음
    • Secret key 유출 시 JWT 조작 가능

(3) JWT 사용 흐름

Client 가 username, password 로 로그인 성공 시

  • 서버에서 "로그인 정보" → JWT 로 암호화 (Secret Key 사용)
  • 서버에서 직접 쿠키를 생성해 JWT를 담아 Client 응답에 전달
  • 브라우저 쿠키 저장소에 자동으로 JWT 저장됨

Client 에서 JWT 통해 인증방법

  • 서버에서 API 요청 시마다 쿠키에 포함된 JWT를 찾아서 사용
  • Client 가 전달한 JWT 위조 여부 검증 (Secret Key 사용)
  • JWT 유효기간이 지나지 않았는지 검증
  • 검증 성공시, JWT → 에서 사용자 정보를 가져와 확인

(4) JWT 구조

  • JWT 는 누구나 평문으로 복호화 가능
  • Secret Key 가 없으면 JWT 수정 불가 -> Read only 데이터
  • Payload에 실제 유저의 정보가 들어있고, HEADER와 VERIFY SIGNATURE부분은 암호화 관련된 정보 양식

2) JWT 다루기

(1) 설정

  • JWT dependency
// JWT
compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
  • application.properties에 추가
jwt.secret.key=7Iqk7YyM66W07YOA7L2U65Sp7YG065+9U3ByaW5n6rCV7J2Y7Yqc7YSw7LWc7JuQ67mI7J6F64uI64ukLg==

(2) JwtUtil 만들기

토큰 생성에 필요한 데이터

// Header KEY 값
public static final String AUTHORIZATION_HEADER = "Authorization";
// 사용자 권한 값의 KEY
public static final String AUTHORIZATION_KEY = "auth";
// Token 식별자
public static final String BEARER_PREFIX = "Bearer ";
// 토큰 만료시간
private final long TOKEN_TIME = 60 * 60 * 1000L; // 60분

@Value("${jwt.secret.key}") // Base64 Encode 한 SecretKey
private String secretKey;
private Key key;
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

// 로그 설정
public static final Logger logger = LoggerFactory.getLogger("JWT 관련 로그");

@PostConstruct
public void init() {
    byte[] bytes = Base64.getDecoder().decode(secretKey);
    key = Keys.hmacShaKeyFor(bytes);
}

JWT 관련 기능

  • JWT 생성
// 토큰 생성
public String createToken(String username, UserRoleEnum role) {
    Date date = new Date();

    return BEARER_PREFIX +
            Jwts.builder()
                    .setSubject(username) // 사용자 식별자값(ID)
                    .claim(AUTHORIZATION_KEY, role) // 사용자 권한
                    .setExpiration(new Date(date.getTime() + TOKEN_TIME)) // 만료 시간
                    .setIssuedAt(date) // 발급일
                    .signWith(key, signatureAlgorithm) // 암호화 알고리즘
                    .compact();
}
  • 생성된 JWT를 Cookie에 저장
// JWT Cookie 에 저장
public void addJwtToCookie(String token, HttpServletResponse res) {
    try {
        token = URLEncoder.encode(token, "utf-8").replaceAll("\\+", "%20"); // Cookie Value 에는 공백이 불가능해서 encoding 진행

        Cookie cookie = new Cookie(AUTHORIZATION_HEADER, token); // Name-Value
        cookie.setPath("/");

        // Response 객체에 Cookie 추가
        res.addCookie(cookie);
    } catch (UnsupportedEncodingException e) {
        logger.error(e.getMessage());
    }
}
  • Cookie에 들어있던 JWT 토큰을 Substring
// JWT 토큰 substring
public String substringToken(String tokenValue) {
    if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
        return tokenValue.substring(7);
    }
    logger.error("Not Found Token");
    throw new NullPointerException("Not Found Token");
}
  • JWT 검증
// 토큰 검증
public boolean validateToken(String token) {
    try {
        Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
        return true;
    } catch (SecurityException | MalformedJwtException | SignatureException e) {
        logger.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
    } catch (ExpiredJwtException e) {
        logger.error("Expired JWT token, 만료된 JWT token 입니다.");
    } catch (UnsupportedJwtException e) {
        logger.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
    } catch (IllegalArgumentException e) {
        logger.error("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
    }
    return false;
}
  • JWT에서 사용자 정보 가져오기
// 토큰에서 사용자 정보 가져오기
public Claims getUserInfoFromToken(String token) {
    return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}

완성

package com.sparta.springauth.jwt;

import com.sparta.springauth.entity.UserRoleEnum;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.Key;
import java.util.Base64;
import java.util.Date;

@Component
public class JwtUtil {
    // Header KEY 값
    public static final String AUTHORIZATION_HEADER = "Authorization";
    // 사용자 권한 값의 KEY
    public static final String AUTHORIZATION_KEY = "auth";
    // Token 식별자
    public static final String BEARER_PREFIX = "Bearer ";
    // 토큰 만료시간
    private final long TOKEN_TIME = 60 * 60 * 1000L; // 60분

    @Value("${jwt.secret.key}") // Base64 Encode 한 SecretKey
    private String secretKey;
    private Key key;
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    // 로그 설정
    public static final Logger logger = LoggerFactory.getLogger("JWT 관련 로그");

    @PostConstruct
    public void init() {
        byte[] bytes = Base64.getDecoder().decode(secretKey);
        key = Keys.hmacShaKeyFor(bytes);
    }

    // 토큰 생성
    public String createToken(String username, UserRoleEnum role) {
        Date date = new Date();

        return BEARER_PREFIX +
                Jwts.builder()
                        .setSubject(username) // 사용자 식별자값(ID)
                        .claim(AUTHORIZATION_KEY, role) // 사용자 권한
                        .setExpiration(new Date(date.getTime() + TOKEN_TIME)) // 만료 시간
                        .setIssuedAt(date) // 발급일
                        .signWith(key, signatureAlgorithm) // 암호화 알고리즘
                        .compact();
    }

    // JWT Cookie 에 저장
    public void addJwtToCookie(String token, HttpServletResponse res) {
        try {
            token = URLEncoder.encode(token, "utf-8").replaceAll("\\+", "%20"); // Cookie Value 에는 공백이 불가능해서 encoding 진행

            Cookie cookie = new Cookie(AUTHORIZATION_HEADER, token); // Name-Value
            cookie.setPath("/");

            // Response 객체에 Cookie 추가
            res.addCookie(cookie);
        } catch (UnsupportedEncodingException e) {
            logger.error(e.getMessage());
        }
    }

    // JWT 토큰 substring
    public String substringToken(String tokenValue) {
        if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
            return tokenValue.substring(7);
        }
        logger.error("Not Found Token");
        throw new NullPointerException("Not Found Token");
    }

    // 토큰 검증
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (SecurityException | MalformedJwtException | SignatureException e) {
            logger.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
        } catch (ExpiredJwtException e) {
            logger.error("Expired JWT token, 만료된 JWT token 입니다.");
        } catch (UnsupportedJwtException e) {
            logger.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
        } catch (IllegalArgumentException e) {
            logger.error("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
        }
        return false;
    }

    // 토큰에서 사용자 정보 가져오기
    public Claims getUserInfoFromToken(String token) {
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
    }
}
profile
₍ ᐢ. ̫ .ᐢ ₎

0개의 댓글