[TIL] 240227 - 유저 인증방식

황지원·2024년 2월 27일

TIL

목록 보기
9/15

개요

  1. 로그인 방식
    • 무상태(stateless : 비 연결적인 특성)는 연결이 해제되면 동시에 server 및 clinet는 clinet가 이전에 요청한 결과에 대해서 잊기 때문에 다른 요청 시 서버와의 연결이 필요하다.
    • Http의 이러한 특성 때문에  stateless protocol 이라고 불리기도 하며 웹 사이트는 매 페이지에서 로그인이 되어 있는지 상태를 확인하는 인증 방식이 필요하게 된다.
  2. Authentication vs Authorization
    • Authentication (인증) : 웹 사이트에 로그인을 하는 것
    • Authorization (인가) : 한번 로그인을 하고 나면 로그인 상태가 유지되는 것
    • 권한 부여인 인가를 받아야지만 웹 서비스의 기능을 사용할 수 있게 된다.
    • 매 요청마다 로그인을 할 수는 없기 때문에 로그인 상태를 유지시켜주는 기능이 필요하다.
    • 이때 사용되는 것이  Session(세션)JWT 이다.

내용

Session 이란

  • 방문자가 웹서버에 접속해 있는 상태를 하나의 단위로 보며 이를 session 이라고함.
    • 즉, 브라우저에 웹 서버가 연결되어서 해당 브라우저의 종료시점 까지를 의미
  • 웹서버는 각 단위에 session id를 부여하고 같은 브라우저인지 구별한다.
    • 사용자는 Session ID를 담은 cookie를 이후의 요청부터 계속 함께 전달하여 server가 clinet를 식별할 수 있게 하는 인증방식
    • 브라우저를 닫거나(1) 서버에서 session id가 들어있는 cookie 를 찾아 삭제했을 경우(2) 제거
    • 쿠키에 중요 정보를 넣지 않아 탈취를 당해도 의미없는 Session ID 만 가지고 있음
  • 세션을 사용한다고 하여 쿠키를 사용하지 않는 건 아니지만 Session은 대부분 브라우저 종료 시 만료
  • 쿠키는 클라이언트에 저장이 되고, 세션은 서버측에 저장된다.
  • 쿠키에 로그인 정보를 담는다고 하면 노출 위험이 있기 때문에 서버에 저장하는 방식인 세션을 많이 사용한다.

session의 장단점

  • 장점
    • 쿠키 방식과 동일하게 로그인 상태를 유지할 수 있다.
    • 탈취되더라도 해석할 수 없기 때문에 보안성이 상대적으로 높다.
  • 단점
    • 해커가 중간에 가로채서 훔친 쿠키로 http요청을 보낼 수 있는 하이재킹 공격을 당할 수 있다.
      • 이를 해결하기 위해서는 HTTPS를 사용하거나(1) 세션에 유효시간을 넣는 등(2)의 보안 조치를 취해야 한다.
    • 서버에 세션 객체를 저장하기 때문에 사용자가 다수일 경우 부하가 높아질 수 있다.
      • 이를 해결하기 위해서는 세션 캐시를 사용하거나 중앙 세션 관리 시스템을 사용하는 등의 방법을 고려할 수 있다.
    • 확장성이 좋지 않다. 여러 대의 서버 컴퓨터를 추가할 경우 각 서버마다 세션 정보가 저장되기 때문에 확장성이 떨어진다.
      • 이를 해결하기 위해서는 중앙 세션 관리 시스템을 사용하는 등의 방법을 고려할 수 있다.
    • CORS(Cross-Origin Resource Sharing) 제약으로 인해 여러 도메인에서 세션을 공유하기 어렵다.

토큰 기반 인증(JWT)

  • 토큰을 사용한다는 것은 요청과 응답에 토큰을 함께 보내 이 사용자가 유효한 사용자인지를 검색하는 방법이다.

  • 이때, JWT(Json Web Token) 토큰을 사용해서 전달

  • 우리가 로그인을 성공하면 Authentication(인증)이 일어나고 서버는 jwt를 제공합니다. 그리고 우리는 매 요청마다 jwt와 함께 서버에 요청을 보내면 서버는 jwt만 확인하여 authorization(인가) 하게 됩니다.

  • JWT 인증 과정

    1. 클라이언트가 아이디와 비밀번호를 서버에게 전달하며 인증을 요청
    2. 서버는 아이디와 비밀번호를 통해 유효한 사용자인지 검증하고, 유효한 사용자인 경우 토큰을 생성해서 응답
    3. 클라이언트는 토큰을 저장해두었다가, 인증이 필요한 api에 요청할 때 토큰 정보와 함께 요청
    4. 서버는 토큰이 유효한지 검증하고, 유효한 경우에는 응답

토큰 사용 방식의 특징

1) 무상태성

사용자의 인증 정보가 담겨있는 토큰을 클라이언트에 저장하기 때문에 서버에서 별도의 저장소가 필요 없어, 완전한 무상태(stateless)를 가질 수 있습니다. 그리고 이로 인해 서버를 확장할 때 용이합니다.

2) 확장성

토큰 기반 인증을 사용하는 다른 시스템에 접근이 가능

3) 무결성

HMAC(Hash-based Message Authentication) 기법이라고도 불리며, 발급 후의 토큰의 정보를 변경하는 행위가 불가능합니다. 즉, 토큰이 변조되면 바로 알아차릴 수 있습니다.

4) 보안성

클라이언트가 서버에 요청을 보낼 때, 쿠키를 전달하지 않기 때문에 쿠키의 취약점은 사라집니다.


JWT?

  • 토큰 기반 인증 시스템의 대표적인 구현체
  • Java를 포함한 많은 프로그래밍 언어에서 이를 지원하며, 보통 회원 인증을 할 때에 사용
  • JWT는 .을 기준으로 헤더(header) - 내용(payload) - 서명(signature)으로 이루어져 있다.

1) 헤더(header)

헤더는 토큰의 타입과 해싱 알고리즘을 지정하는 정보를 포함합니다.

  • typ : 토큰의 타입을 지정합니다. JWT라는 문자열이 들어가게 됩니다.
  • alg: 해상 알고리즘을 지정합니다.
{
    "typ": "JWT",
    "alg": "HS256"    // HS256: 해상 알고리즘
}

2) 정보(payload)

토큰에 담을 정보가 들어갑니다. 정보의 한 덩어리를 클레임(claim)이라고 부르며, 클레임은 key-value의 한 쌍으로 이루어져 있습니다. 클레임의 종류는 세 종류로 나눌 수 있습니다.

  • 등록된(registered) 클레임
    • 토큰에 대한 정보를 담기 위한 클레임들이며, 이미 이름이 등록되어 있는 클레임
    • iss : 토큰 발급자(issuer)
    • sub : 토큰 제목(subject)
    • aud : 토큰 대상자(audience)
    • exp : 토큰의 만료시간(expiraton). 시간은 NumericDate 형식으로 되어있어야 하며, (예: 1480849147370) 항상 현재 시간보다 이후로 설정되어있어야 한다.
    • nbf : Not Before를 의미하며, 토큰의 활성 날짜와 비슷한 개념. NumericDate 형식으로 날짜를 지정하며, 이 날짜가 지나기 전까지는 토큰이 처리되지 않는다.
    • iat : 토큰이 발급된 시간 (issued at)
    • jti : JWT의 고유 식별자로서, 주로 일회용 토큰에 사용한다.
  • 공개(public) 클레임
    • 말 그대로 공개된 클레임, 충돌을 방지할 수 있는 이름을 가져야 하며, 보통 클레임 이름을 URI로 짓는다.
  • 비공개(private) 클레임
    • 클라이언트 - 서버 간에 통신을 위해 사용되는 클레임
  • 예제 Payload
    {
        "iss": "***",        // 등록된(registered) 클레임
        "iat": -,        // 등록된(registered) 클레임
        "exp": -,        // 등록된(registered) 클레임
        "https://**.com/jwt_claims/is_admin": true,        // 공개(public) 클레임
        "email": "***",        // 비공개(private) 클레임
        "hello": "안녕하세요"        // 비공개(private) 클레임
    }

3) 서명(signature)

해당 토큰이 조작되었거나 변경되지 않았음을 확인하는 용도로 사용하며, 헤더(header)의 인코딩 값과 정보(payload)의 인코딩값을 합친 후에 주어진 비밀키를 통해 해쉬값을 생성합니다.

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

session vs jwt 무엇을 사용해야 하는가?

session

  • 세션 방식에서 서버는 로그인 된 유저 정보를 모두 저장하고 있고, 유저의 통제가 JWT보다 비교적 쉬워 집니다.
  • 만약에 PC와 모바일 기기에서 동시 접근하는 것을 막고 싶을 경우 강제 로그아웃을 시키는 기능을 세션을 통해서 구현가능합니다.

JWT

  • JWT는 서버가 발급 후에 JWT를 관리하지 않습니다.
  • 오직 JWT를 받았을 때, JWT가 유효한 것인지 확인하여 서버 자원과 비용을 절감 할 수 있습니다.
  • 그러나 JWT가 수명이 길어서 제 3자에게 탈취당할 경우가 발생할 수 있는데 이를 보안하는 방법이 Access Token 과 Refresh Token의 방식이 있습니다.
  • 또한 토큰을 검증하는 서명(signature)이 정말 중요하며 서명에 사용되는 secret_key를 잘 관리해야 합니다.

Java 코드 구현

1) 의존성 추가

jsonwebtoken을 사용하기 위해 의존성을 추가해 줍니다.

implementation 'io.jsonwebtoken:jjwt:0.9.1'

2) JWT 토큰 만들기

public String makeJwtToken() {
    Date now = new Date();

    return Jwts.builder()
        .setHeaderParam(Header.TYPE, Header.JWT_TYPE)    // (1)
        .setIssuer("fresh")    // (2)
        .setIssuedAt(now)    // (3)
        .setExpiration(new Date(now.getTime() + Duration.ofMinutes(30).toMillis()))    // (4)
        .claim("id", "아이디")    // (5)
        .claim("email", "ajufresh@gmail.com")
        .signWith(SignatureAlgorithm.HS256, "secret")    // (6)
        .compact();
  }
  1. 헤더의 타입(typ)을 지정할 수 있습니다. jwt를 사용하기 때문에 Header.JWT_TYPE로 사용해 줍니다.
  2. 등록된 클레임 중, 토큰 발급자(iss)를 설정할 수 있습니다.
  3. 등록된 클레임 중, 발급 시간(iat)을 설정할 수 있습니다. Date 타입만 추가가 가능합니다.
  4. 등록된 클레임 중, 만료 시간(exp)을 설정할 수 있습니다. 마찬가지로 Date 타입만 추가가 가능합니다.
  5. 비공개 클레임을 설정할 수 있습니다. (key-value)
  6. 해싱 알고리즘과 시크릿 키를 설정할 수 있습니다.

더 많은 기능이 있지만, 제가 사용해 본 메서드 위주로 설명을 적어두었습니다. 모든 설정이 끝나면 compact()를 통해 JWT 토큰을 만들 수 있습니다.

그 이후에 실행하면,

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJmcmVzaCIsImlhdCI6MTYyMjkwNjg0NSwiZXhwIjoxNjIyOTA4NjQ1LCJpZCI6IuyVhOydtOuUlCIsImVtYWlsIjoiYWp1ZnJlc2hAZ21haWwuY29tIn0.ucTS9OgA7Z751a6aNzttcEXRfEhG_hsZPzZZTHhbUrA

위와 같은 토큰을 획득할 수 있습니다.

3) JWT 토큰 파싱하기

Authorization : Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJmcmVzaCIsImlhdCI6MTYyMjkwNjg0NSwiZXhwIjoxNjIyOTA4NjQ1LCJpZCI6IuyVhOydtOuUlCIsImVtYWlsIjoiYWp1ZnJlc2hAZ21haWwuY29tIn0.ucTS9OgA7Z751a6aNzttcEXRfEhG_hsZPzZZTHhbUrA

전달받은 토큰을 해석해서 유효한 토큰인지 확인이 가능합니다. 👀

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
  String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
  Claims claims = jwtTokenProvider.parseJwtToken(authorizationHeader);

  filterChain.doFilter(request, response);
}
public Claims parseJwtToken(String authorizationHeader) {
    validationAuthorizationHeader(authorizationHeader);// (1)String token = extractToken(authorizationHeader);// (2)return Jwts.parser()
        .setSigningKey("secret")// (3)
        .parseClaimsJws(token)// (4)
        .getBody();
}

private void validationAuthorizationHeader(String header) {
    if (header == null || !header.startsWith("Bearer ")) {
      throw new IllegalArgumentException();
    }
}

private String extractToken(String authorizationHeader) {
    return authorizationHeader.substring("Bearer ".length());
}
  1. 헤더가 'Bearer'로 시작하는지 검사합니다.
  2. 'Bearer'을 제외한 문자열만 반환해 주도록 처리해 줍니다.
  3. 시크릿 키를 넣어주어 토큰을 해석할 수 있습니다.
  4. 해석할 토큰을 문자열(String) 형태로 넣어줍니다.

위와 같은 정보를 넣어준 후에 getBody()를 호출하게 되면, Claim 타입의 결과 객체를 반환하게 되는데, 여기에서 저장된 클레임 정보들을 확인할 수 있습니다.

발생할 수 있는 예외는 다음과 같으며 적절한 처리를 해주는 것이 좋다

  • UnsupportedJwtException : 예상하는 형식과 다른 형식이거나 구성의 JWT일 때
  • MalformedJwtException : JWT가 올바르게 구서오디지 않았을 때
  • ExpiredJwtException : JWT를 생성할 때 지정한 유효기간이 초과되었을 때
  • SignatureException : JWT의 기존 서명을 확인하지 못했을 때
  • IllegalArgumentException
profile
함께 도전하고 성장하는 어린 꿀벌, 주니어 개발자 황지원 입니다

0개의 댓글