SEB[Spring Security : Cookie, Session, Token, OAuth]

Jogi's 코딩 일기장·2021년 9월 10일
1

이전 내용에 이어서 Cookie, Session, Token, OAuth에 대한 정리를 시작한다.

  • 쿠키의 존재로 HTTP는 Stateless(무상태성)이어도 정보가 유지된다.

  • 쿠키란, 어떤 웹사이트에 들어갔을 때, 서버가 일방적으로 클라이언트에 전달하는 작은 데이터이다.

    • 서버가 웹 브라우저에 정보를 저장하고 불러올 수 있는 수단
    • 해당 도메인에 대해 쿠키가 존재하면, 웹 브라우저는 도메인에게 http 요청시 쿠키를 함께 전달.
  • 다시 정리해보면, 서버에서 클라이언트에 데이터를 저장하는 방법의 하나이며, 서버가 원한다면 서버는 클라이언트에서 쿠키를 이용해 데이터를 가져올 수 있다.

  • 쿠키를 이용하는 것은 단순히 서버에서 클라이언트에 쿠키를 전송하는 것만 의미하지 않고, 클라이언트에서 서버로 쿠키를 전송하는 것도 포함된다.

  • 특징

    • 데이터를 저장한 이 후 아무 때나 데이터를 가져올 수 없다. 데이터를 저장한 이 후 특정 조건들이 만족하는 경우에만 다시 가져올 수 있다.
    • 특정 조건들이 옵션으로 포함될 수 있다.
  • Domain : 서버와 요청의 도메인이 일치하는 경우 쿠키를 전송한다.
  • Path : 서버와 요청의 세부경로가 일치하는 경우 쿠키를 전송한다.
  • maxAge or Expires : 쿠키의 유효기간을 설정한다. 이는 Date를 지정하는 것이다.
  • HttpOnly : 스크립트의 쿠키 접근 가능 여부를 결정하며, boolean값을 갖는다.
  • Secure : Https 프로토콜에서만 쿠키 전송 여부를 결정하며, boolean값을 갖는다.
  • SameSite : CORS 요청의 경우 옵션 및 메서드에 따라 쿠키 전송 여부를 결정한다.
    • SameSite의 옵션
      • Lax : Cross-origin 요청이면 GET 메소드에 대해서만 쿠키 전송이 가능하다.
      • Strict : Cross-origin이 아닌 same-site인 경우에만 쿠키 전송이 가능하다.
      • None : 항상 쿠키 전송이 가능하다. 하지만 Secure 옵션이 항상 필요하다 이는 Https 프로토콜에서만 가능하다는 말이다.
      • 옵션들이 서버에서 클라이언트로 쿠리를 처음 전송하게 되면, 헤더에 set_cookie라는 프로퍼티에 담아 쿠키를 전송하게 된다. 이 후 클라이언트 혹은 서버에서 쿠키를 전송해야 한다면 클라이언트 헤더에 Cookie라는 프로퍼티에 쿠키를 담아 서버에 쿠키를 전송하게 된다.

쿠키를 이용한 상태유지

  • 서버는 클라이언트에 인증 정보를 담은 쿠키를 전송하고, 클라이언트는 전달받은 쿠키를 요청과 같이 전송하여 Stateless한 인터넷 연결을 Stateful하게 유지할 수 있다.
  • 하지만 쿠키는 기본적으로 오랜 시간 유지될 수 있고, 자바스크립트를 통한 쿠키 접근이 가능하기 때문에 쿠키에 민감한 정보를 담는 것은 위험하다.

Session

  • 서버가 클라이언트에 유일하고 암호화된 ID를 부여한다.
  • 중요데이터는 서버에서 관리한다.

설명

  • Cookie : 쿠키는 그저 Http의 Stateless한 것을 보완해주는 도구
  • Session : 접속 상태를 서버가 가짐(stateful), 접속상태와 권한부여를 위해 세션아이디를 쿠키로 전송

접속상태 저장경로

  • Cookie : 클라이언트
  • Session : 서버

장점

  • Cookie : 서버의 부담을 덜어준다.
  • Session : 신뢰할 수 있는 유저인지 서버에서 추가로 확인이 가능하다.

단점

  • Cookie : 쿠키 자체는 인증이 아니다.
  • Session
    • 하나의 서버에서만 접속상태를 가지므로 분산에 불리하다.
    • 서버의 메모리에 세션 정보를 저장하고 있다. 이용자가 많아진다면 가용메모리가 줄어들어 성능이 저하된다.
    • 쿠키를 여전히 사용하고 있기 때문에 세션 큐키의 탈취 가능성이 있다.

Session 사용법

  • HttpSession이라는 클래스를 사용한다.
public static HttpSession session;
  • 세션 불러오기
session = request.getSession();
  • request는 HttpServletRequest 객체이며 클라이언트가 보내는 요청에 해당된다. getSession()을 통해서 세션을 불러온다.
  • 세션의 키에 값 설정
session.setAttribute("LoginMember", user);
  • setAttribute를 통해 해당 키에 정보를 저장한다.

  • 세션의 키 값으로 값 불러오기

Userdata user = (Userdata) session.getAttribute("LoginMember");
  • 키 값을 통해서 세션에 있는 값을 가져올 수 있다.
  • 세션 삭제
session.removeAttribute("LoginMember"); // "LoginMember"라는 키와 함께 대응하는 값 삭제
session.invalidate(); // 세션 초기화, 세션을 완전 초기화 시킨다.

Token

  • 토큰 기반 인증은 Spring에서 주로 JWT(JSON Web Token)을 사용한다.

  • 토큰 기반 인증을 사용하는 이유

    • 서버기반 인증은 서버(혹은 DB)에 유저 정보를 담는 방식이다. 서버가 부담을 많이 가기 때문에 이 부담을 클라이언트에게 넘겨줄 수 없을까라는 것에서 고안됐다.
    • 대표적인 토큰기반 인증이 JWT이다.
  • 클라이언트에서 인증 정보를 보관한다는 것은 토큰은 유저 정보를 암호화한 상태로 담을 수 있고, 암호화했기 때문에 클라이언트에 담을 수 있다는 말이다.

JWT (JSON Web Token)

종류

  1. Access Token : 보호된 정보들에 접근할 수 있는 권한 부여에 사용한다. 권한을 부여받는 역할이다.
  2. Refresh Token : Access Token은 비교적 짧은 유효기간을 주어, 오랫동안 사용할 수 없도록 한다. 유효기간이 만료되면 Refresh Token을 사용하여 새로운 Access Token을 발급받으며, 유저는 로그아웃할 필요가 없다.

JWT 구조

  aaaaaaa.bbbbbbb.ccccccc
//(header).(payload).(signature)
  • Header
    • 어떤 종류의 토큰인지, 어떤 알고리즘으로 sign(암호화)할 지 적혀있다. JWT는 JSON의 형태로 볼 수 있다.
  • Payload
    • 정보가 담겨있다. 어떤 정보에 접근 가능한지에 대한 권한을 담을 수 있고, 사용자의 이름 등 필요한 데이터는 이 곳에 담아 암호화시킨다. 암호화가 될 정보라도, 민감한 정보는 되도록 담지 않는 것을 권장한다.
  • signature
    • header와 payload가 완성됐다면, 원하는 비밀키(암호화에 추가할 salt)를 사용하여 암호화한다.

토큰기반 인증 절차

  1. 클라이언트가 서버에 ID/PW를 담아 로그인 요청을 보낸다
  2. ID/PW가 일치하는지 확인하고, 클라이언트에게 보낼 암호화 토큰을 생성한다.(Access, Refresh 모두)
  3. 토큰을 클라이언트에 보내주면, 클라이언트는 토큰을 저장한다. 저장하는 위치는 local storage, cookie, react state 등 다양하다.
  4. 클라이언트가 HTTP 헤더(authorization 헤더)에 토큰을 담아 보낸다.(보통 barer authentication을 이용한다.)
  5. 서버는 토큰을 해독 후 올바르다 판단되면, 클라이언트 요청을 처리한 후 응답을 보내준다.

토큰기반 인증의 장점

  • 무상태성 & 확장성

    • 서버는 클라이언트에 대한 정보를 저장할 필요가 없다. => 토큰 해독이 되는지만 판단한다.
    • 클라이언트는 새로운 요청을 보낼 때마다 토큰을 헤더에 포함시키면 된다. => 서버를 여러개 가진 서비스라면 같은 토큰으로 여러 서버에서 인증 가능하므로 효율적이다.
  • 안정성

    • 암호화된 토큰을 사용하고, 암호화 키를 노출할 필요가 없기 때문에 안전하다.
  • 어디서나 생성가능하다.

    • 토큰을 확인하는 서버가 토큰을 만들지 않아도 된다. => 토큰 생성용 서버를 만들거나, 다른 회사에서 토큰관련 작업을 맡기는 것 등 다양한 활용이 가능하다.
  • 권한 부여에 용이

    • 토큰의 payload 안에 어떤 정보에 접근 가능한지 정할 수 있다.
      ex) 서비스의 사진과 연락처 사용권한만 부여할 수 있다.

Token 사용법

토큰 생성하기

	Jwts.builder()
        .setHeaderParam(Header.TYPE, Header.JWT_TYPE)
        .setIssuer("fresh")
        .setExpiration(new Date(now.getTime() + Duration.ofSeconds(time).toMillis()))
        .claim("userId", userList.getUserId())
        .claim("password", userList.getPassword())
        .signWith(SignatureAlgorithm.HS256, SIGN_KEY)
        .compact();
  • 토큰을 생성하는데에는 다양한 방법이 있다. 여기에서는 Jwts라는 객체를 이용해 토큰을 생성한다. setHeaderParam은 헤더를 정해주는 것이다. setExpiration은 토큰에는 유효시간이 있기 때문에 토큰의 유효시간을 정해주는 것이다. 그 다음에는 payload에 들어가는 내용은 claim을 통해 추가가 가능하다. 이는 키-값 쌍으로 추가를 해주면된다. 그리고 signWith는 어떤 알고리즘을 가지고 암호화 키를 가지고 암호화하는 것이다. 그리고 마지막에 compact()를 해주면 String의 형식으로 반환을 하게된다.

토큰 검증하기

Claims claims = Jwts.parser()
                    .setSigningKey(SIGN_KEY)
                    .parseClaimsJws(key)
                    .getBody();
// 토큰을 체크 후 "userId 값을 리턴합니다."
String userid = (String) claims.get("userId");
  • 이 역시 Jwts를 사용하며 암호화 키를 통해 복호화를 한다. 그리고 여기에서 보이는 key는 Access Token 혹은 Refresh Token이다. 그 후에는 getBody()로 처리를 해주면 Claims 객체로 반환받는다. 그 후에는 Claims객체도 키-값의 형태이기 때문에 get("키값")으로 받는다면 원하는 정보를 얻을 수 있다. 그리고 Jwt에는 오류가 있기 때문에 유효시간 오류라던지 그 외 오류를 처리를 해주면된다.

실습하면서 알아두면 좋을것 같은 것들

  • refresh token은 쿠키에 담아 보내기 때문에 쿠키를 만들어주고 쿠키에 담아두는 것이 좋다. 응답에 추가를 해주는 것이다.
String accessToken = tokenService.CreateJwtToken(userList, ACCESS_TIME);
String refreshToken = tokenService.CreateJwtToken(userList, REFRESH_TIME);
Cookie setCookie = new Cookie("refreshToken", refreshToken);
response.addCookie(setCookie);
  • 쿠키에도 키-값의 형태로 값을 넣어주면 된다.

  • 요청을 받을 때는 request의 쿠키를 받아서 또 확인이 가능하다.

String cookiesResult = "";
Cookie[] cookies = request.getCookies();
for(int i = 0; i < cookies.length; ++i) {
	if(cookies[i].getName().equals("refreshToken")) {
    	cookiesResult = cookies[i].getValue();
        break;
    }
}
  • 요청에서 getCookies()를 이용하면 쿠키들을 배열의 형태로 받을 수 있다. 우리는 getName()을 통해서 값을 찾을 수 있고 우리가 찾고자 하는 토큰을 찾을 수 있다.

  • body에 종종 JSON의 형태로 내용을 넣어 응답을 보내는 경우가 있다. 그 때에도 쓰기 좋은 방법이 HashMap을 이용하는 것이다.

return ResponseEntity.ok().body(new HashMap<>(){{
                put("data", new HashMap<>(){{
                    String accessToken = tokenService.CreateJwtToken(userList, ACCESS_TIME);
                    put("userInfo", new HashMap<>(){{
                        put("createdAt", userList.getCreatedAt());
                        put("userId", userList.getUserId());
                        put("email", userList.getEmail());
                    }});
                    put("accessToken", accessToken);
                }});
                put("message", "ok");
            }});
{
    "data": {
        "userInfo": {
            "createdAt": "2021-09-06T15:08:04.000+00:00",
            "userId": "jogi",
            "email": "jogiyo@fish.com"
        }
    },
    "message": "ok"
}

이와 같은 형태로 body를 보낼 수 있다.

OAuth

  • 소셜 로그인 인증 방식은 OAuth 2 라는 기술을 바탕으로 구현된다.
  • 직접 작성한 서버에서 인증을 처리해주는 것과는 달리 OAuth는 인증을 중개해주는 매커니즘이다.
  • 보안된 리소스에 액세스하기 위해 클라이언트에게 권한을 제공(Authorization) 하는 프로세스를 단순화하는 프로토콜이다.

OAuth의 사용시기, 왜 사용하는지

  • 유저 입장에서 웹 상의 서비스를 이요하기 위해서 회원가입 절차가 대부분인데 OAuth를 사용하면 중요한 서비스들(ex. google, github, facebook 등)의 ID와 PW만 기억해 놓는다면 해당 서비스를 통해 소셜 로그인을 할 수 있다.

  • 보안상의 이점도 존재한다. 검증되지 않은 APP에서 OAuth를 사용하여 로그인한다면, 직접 유저의 민감한 정보가 APP에 노출될 일이 없고, 인증 권한에 대한 허가를 미리 유저에게 구해야되기 때문에 더 안전하게 사용할 수 있다.

주요용어

  • Resource Owner : 액세스 중인 리소스의 유저
  • Client : Resourc Owner를 대신하여 보호된 리소스에 액세스하는 응용 프로그램
  • Resource Server : Client의 요청을 수락하고 응답할 수 있는 서버
  • Authorization Server : Resource Server가 액세스 토큰을 발급받는 서버, 즉 클라이언트 및 리소스 소유자를 성공적으로 인증한 후 액세스 토큰을 발급하는 서버
  • Authorization grant : 클라이언트가 액세스 토큰을 얻을 때 사용하는 자격 증명의 유형
  • Authorization code : 액세스 토큰을 발급받기 전에 필요한 코드, Client ID로 Code를 받아온 후, Client Secret과 code를 이용해 액세스 토큰을 받아온다.
  • Access Token : 보호된 리소스에 액세스하는 데 사용되는 credentials(자격증명)이다. Authorization code와 client secret을 이용해 받아온 이 액세스 토큰으로 resource server에 접근할 수 있다.
  • Scope : 토큰의 권한을 정의한다. 주어진 액세스 토큰을 사용하여 액세스할 수 있는 리소스의 범위다.

Grant Type 종류

클라이언트가 액세스 토큰을 얻는 방법

  • Authorization
  • Implicit
  • Client Credentials
  • Resource Owner Credentials
  • Refresh Token

Authorization Code Grant Type

  • 액세스 토큰을 받아오기 위해서 먼저 Authorization code를 받아 액세스 토큰과 교환하는 방법
  • Authorization code 절차를 거치는 이유는 보안성 강화에 목적이 있다.
  • 클라이언트에서는 Authorization code만 받아오고 서버에서 액세스 토큰 요청을 진행한다.

Refresh Token Grant Type

  • 일정 기간 유효 시간이 지나서 만료된 액세스 토큰을 편리하게 다시 받아오기 위해 사용하는 방법
  • Access Token보다 Refresh Token의 유효시간이 대체로 조금 더 길게 설정하기 때문에 가능한 방법이다.
  • 서버마다 Refresh Token에 대한 정책이 다르기 때문에 Refresh Token을 사용하기 위해서는 사용하고자하는 서버의 정책을 살펴볼 필요가 있다.

실습

토큰 받아오기

 @PostMapping(value = "/callback")
    public ResponseEntity<?> PostCallBack(@RequestBody(required = true)CallBackAuthorization authorization){
        try{
            CallBackToken callBackToken = new CallBackToken();
            // Authorization Code와 DB에 있는 User 데이터를 사용하여 토큰을 받아옵니다.
            // [post] Git URL : https://github.com/login/oauth/access_token
            OAuthCode oac = oAuthRepository.FindUserOAuthCode();
            UserData userData = new UserData(oac.getClientId(), oac.getClientSecret(), authorization.getAuthorizationCode()); // Git URL에 전달할 데이터 객체를 생성합니다.
            Token token = restTemplate.postForObject("https://github.com/login/oauth/access_token", userData, Token.class); //restTemplate을 사용하여 Git URL에 post 요청을 보냅니다.
            if(token != null){
                callBackToken.setAccessToken(token.getAccess_token());
            }
            return ResponseEntity.ok().body(callBackToken);
        }catch (Exception error){
            return ResponseEntity.badRequest().body("Not found!");
        }
    }

Token을 받아와야 하기 때문에 java의 Token객체를 사용하며 또한 RestTemplate객체를 사용하여 post요청을 보내 토큰을 받아온다. 첫인자는 요청을 보낼 url이며, 같이 보낼 데이터가 두번째 인자가 되며, 어떤 클래스 형태로 받아올 것인지가 세번째 인자가 된다.

Login.js (컴포넌트)

constructor(props) {  // Login.js의 컴포넌트 생성자에서 처리한다.
    super(props)
    this.socialLoginHandler = this.socialLoginHandler.bind(this)
    // GitHub로부터 사용자 인증을 위해 GitHub로 이동해야 한다.
    // OAuth 인증이 완료되면 authorization code와 함께 callback url로 리디렉션 한다.
    let uri = 'http://localhost:3000'   // callback url 
    this.GITHUB_LOGIN_URL = `https://github.com/login/oauth/authorize?client_id={클라이언트id}&redirect_uri=${uri}`
  }

토큰 받아오기, getAccessToken

async getAccessToken(authorizationCode) {
    let callbackURL = 'http://localhost:8080/callback';
    await axios.post(callbackURL, {authorizationCode})
          .then(res => {
            this.setState({
              isLogin: true,
              accessToken: res.data.accessToken
            });
          })
          .catch(err => {
            console.log(err);
          });
  }
  • 받아온 authorization code로 다시 OAuth App에 요청해서 access token을 받을 수 있다.
  • access token은 보안 유지가 필요하기 때문에 클라이언트에서 직접 OAuth App에 요청을 하는 방법은 보안에 취약할 수 있다.
  • 서버의 /callback 엔드포인트로 authorization code를 보내주고 access token을 받아온다.
  • 여기에서 등장하는 것이 axios 모듈인데, 이를 이용해 http 요청을 보낼 수 있다. get, post와 같은 요청이 가능하다.

받아온 토큰을 가지고 정도 가져오기

async getImages() {
    const {accessToken} = this.props;
    let res = await axios.get('http://localhost:8080/images', {
      headers: {
        authorization: `token ${accessToken}`
      }
    });
    this.setState({
      images: [...res.data.images]
    });
  }
  • 부모 컴포넌트로부터 토큰을 props로 전달받기 때문에 구조분해 할당을 통해서 토큰을 받는다. 여기에서도 axios모듈을 이용해 get요청을 보낸다. 비슷한 사용법이지만 get요청을 보낼 때에는 header에 토큰을 같이 보내주어야 한다.

느낀점

  • 이번에는 거의 개념만 주어지고 완전 구글링, 블로깅을 통해서 사용방법을 알게 됐다. 그러는 과정이 너무나도 고통스러웠다. 나중에는 분명 혼자 찾아보고 알아서 적용을 해야할텐데 얼른 이런 과정들도 나에게 익숙해졌으면 좋겠다. 아직 인증에 대해서 제대로 파악하지는 못했지만 이번시간으로 흐름 정도는 이해했으면 한다. 다음 주도 힘내보자.

Reference

  • 코드스테이츠 강의자료
profile
프로그래머로서의 한걸음

0개의 댓글