이전 내용에 이어서 Cookie, Session, Token, OAuth에 대한 정리를 시작한다.
쿠키의 존재로 HTTP는 Stateless(무상태성)이어도 정보가 유지된다.
쿠키란, 어떤 웹사이트에 들어갔을 때, 서버가 일방적으로 클라이언트에 전달하는 작은 데이터이다.
다시 정리해보면, 서버에서 클라이언트에 데이터를 저장하는 방법의 하나이며, 서버가 원한다면 서버는 클라이언트에서 쿠키를 이용해 데이터를 가져올 수 있다.
쿠키를 이용하는 것은 단순히 서버에서 클라이언트에 쿠키를 전송하는 것만 의미하지 않고, 클라이언트에서 서버로 쿠키를 전송하는 것도 포함된다.
특징
HttpSession
이라는 클래스를 사용한다.public static HttpSession session;
session = request.getSession();
HttpServletRequest
객체이며 클라이언트가 보내는 요청에 해당된다. getSession()
을 통해서 세션을 불러온다.session.setAttribute("LoginMember", user);
setAttribute
를 통해 해당 키에 정보를 저장한다.
세션의 키 값으로 값 불러오기
Userdata user = (Userdata) session.getAttribute("LoginMember");
session.removeAttribute("LoginMember"); // "LoginMember"라는 키와 함께 대응하는 값 삭제 session.invalidate(); // 세션 초기화, 세션을 완전 초기화 시킨다.
토큰 기반 인증은 Spring에서 주로 JWT(JSON Web Token)을 사용한다.
토큰 기반 인증을 사용하는 이유
클라이언트에서 인증 정보를 보관한다는 것은 토큰은 유저 정보를 암호화한 상태로 담을 수 있고, 암호화했기 때문에 클라이언트에 담을 수 있다는 말이다.
aaaaaaa.bbbbbbb.ccccccc //(header).(payload).(signature)
무상태성 & 확장성
안정성
어디서나 생성가능하다.
권한 부여에 용이
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();
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에는 오류가 있기 때문에 유효시간 오류라던지 그 외 오류를 처리를 해주면된다.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를 사용하면 중요한 서비스들(ex. google, github, facebook 등)의 ID와 PW만 기억해 놓는다면 해당 서비스를 통해 소셜 로그인을 할 수 있다.
보안상의 이점도 존재한다. 검증되지 않은 APP에서 OAuth를 사용하여 로그인한다면, 직접 유저의 민감한 정보가 APP에 노출될 일이 없고, 인증 권한에 대한 허가를 미리 유저에게 구해야되기 때문에 더 안전하게 사용할 수 있다.
클라이언트가 액세스 토큰을 얻는 방법
@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이며, 같이 보낼 데이터가 두번째 인자가 되며, 어떤 클래스 형태로 받아올 것인지가 세번째 인자가 된다.
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}` }
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); }); }
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] }); }