티스토리 뷰
여기서는 안드로이드와 서버(스프링 프레임워크)사이에 유저의 세션을 유지하는 방법에 대해서 알아보겠다.
기본적으로 Http 프로토콜은 무상태성이다.(stateless) 사용자의 정보를 서버측에서 관리하지 않는다. 한번의 요청과 응답이 지나면 사용자와의 연결을 끊어버리는 프로토콜이다. 하지만, 웹 사이트를 만들다 보면 사용자의 정보를 계속 유지해야 하는 일이 빈번하게 발생한다. 그리하여 http의 무상태성을 극복하기 위해 세션과 쿠키라는 방법을 사용하게 된다.
세션이라는것은, 사용자의 상태를 서버측에 저장하는 기술이다. 사용자가 어떤 웹 사이트에 요청을 보내게 되면, 그 웹 사이트를 관리하는 서버에서는 현재 요청에 걸린 세션의 아이디를 얻게 된다.(임의의 문자열) 그 아이디를 Http 응답 헤더에 Set-Cookie 항목에 실어서 보내게 되면, 그것을 받은 클라이언트(안드로이드)는 응답 헤더를 파싱하여 서버로 부터 받은 세션 아이디를 공유 프리퍼런스와 같은곳에 저장 해놓은뒤 그 다음 부터의 모든 요청에 그 세션 아이디를 요청 헤더에 포함해서 보내야 한다.
서버가 응답 헤더에 Set-Cookie 필드에 세션 아이디를 담는다는것 자체가 세션이라는 기술도 결국엔 쿠키를 활용하는 방법이라는것이다. 쿠키에 세션의 아이디만 들어가느냐, 아니면 클라이언트의 정보 자체가 쿠키에 들어가느냐가 쿠키와 세션 기술의 차이점이라고 할 수 있다.
이렇듯, 클라이언트는 응답 헤더에 Set-Cookie가 있다면 이것을 적절히 활용 할 수 있다. 브라우저의 경우에는 Set-Cookie헤더가 있을때 그 쿠키의 세션 아이디를 자동으로 관리를 해주는것 같다.(정확하지는 않다.) 그래서, 그 다음 요청 헤더에 세션의 아이디를 포함해서 자동으로 전송하는것 같다. 하지만 안드로이드에서는 HttpUrlConnection이라는 객체를 통해서 직접 Http 패킷을 만들어서 서버에 전송하기 때문에 그런 쿠키 관리를 직접 해주어야 한다.
서버로 부터 받은 쿠키는 다음과 같은 형식으로 들어가 있다.
[JSESSIONID=D3F829CE262BC65853F851F6549C7F3E; Path=/smartudy; HttpOnly] -> []가 쿠키1개임.
직접 로그를 찍어서 확인해본 결과 서버로 부터 위와 같은 쿠키가 응답 헤더에 Set-Cookie 필드에 설정되어 왔다는것을 확인 할 수 있었다.
JSESSIONID라는것은 자바 웹 서버의 세션 아이디의 Value가 D3F~~~~라는것이고, PATH속성은 /smartudy 아래의 모든 하위 폴더에 위의 쿠키를 적용 하여 요청 헤더에 포함시켜 전송할 수 있다는 의미이다. 예를들면 다음과 같다.
www.~~~.com/smartudy (이 url에도 쿠키를 적용 할 수 있고)
www.~~~.com/smartudy/hello (여기에도 적용 가능하다.)
www.~~~.com/sm/ (여기에는 적용 불가능 하다.)
서버에서 안드로이드로 세션 아이디를 보내주는 방법
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | @RequestMapping(value = "/login") public @ResponseBody String login(AccountDto account,HttpSession session,BindingResult result) { boolean ismember = member_service.isMember(account.getPhone()); if(!ismember) { session.invalidate(); return null; //존재하지 않는 회원 } AccountDto dto = member_service.login(account); if(dto != null) { //로그인 성공시 session.setAttribute("login", dto); return makeJSON(dto,true); }else {//로그인 실패시 session.invalidate(); //로그인 성공시에만 세션 아이디가 안드로이드로 넘어감. return makeJSON(null,false); } } | cs |
사용자가 /smartudy/login 경로로 요청을 보낼때 요청을 처리하는 컨트롤러 메소드이다.
우선 첫번째로 메소드 매개변수로 HttpSession을 넣어야 한다. 이걸 넣을 경우 현재 세션이 없으면 세션을 새로 만들고 세션이 있다면(사용자가 요청 헤더에 세션 아이디를 포함해서 보냈다면) 그 세션을 사용하겠다는 의미이다.
그리고 나서, 서비스 객체의 isMember메소드를 통해 현재 입력된값에 해당하는 회원이 있는지 조회를 한다.
HttpSession을 메소드 매개변수에 선언하는것만으로도 세션의 아이디를 할당받기 때문에 없는 회원인 경우에는 session.invalidate()메소드를 호출하여 세션을 없애버렸다. 이렇게 세션을 없애면 서버의 응답 헤더에 Set-Cookie 필드가 세팅 되지 않을 것이다. 그리고 나서, 로그인을 수행한다. 로그인에 성공 하지 못했을 경우 마찬가지로 세션을 없애버린다.
로그인에 성공했을때 세션에 dto 객체를 담아서 유저의 정보를 갖고 있게끔 하였다.
makeJSON이라는 메소드는 그냥 유저의 정보를 안드로이드 측으로 전송하기 위해서 객체->JSON 변환 함수라고 간단하게 생각하면 된다. 안드로이드의 경우에 HTTP 응답을 받을때 json형태로 받으면 간편하기 때문에 위와 같이 구성하였다.
물론 안드로이드 -> 서버로 보낼때는 정상적으로 Http 요청 패킷을 만들어서 전송한다.
안드 -> 서버 : Http GET,POST 활용
서버 -> 안드 : JSON형태
이렇게 되면 HTTP 응답 헤더에 Set-Cookie 항목에 다음과 같은 세션아이디가 세팅 되어 있는 쿠키가 전송 되게 된다.
[JSESSIONID=D3F829CE262BC65853F851F6549C7F3E; Path=/smartudy; HttpOnly] -> []가 쿠키1개임.
이걸 안드로이드에서 받은다음, 프리퍼런스에 저장하도록 하자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | private String request(){ //key&value로 전송하고 json으로 받기. String params = makeParameter(map); try{ URL Url = new URL(url); con = (HttpURLConnection) Url.openConnection(); if(Method == HttpUtils.POST) con.setRequestMethod("POST"); else if(Method == HttpUtils.GET) con.setRequestMethod("GET"); con.setRequestProperty("Accept","application/json"); con.setDefaultUseCaches(false); con.setUseCaches(false); con.setDoInput(true); con.setDoOutput(true); setCookieHeader(); //사용자가 로그인해서 세션 쿠키를 서버로부터 발급받은적 있다면 그 다음 요청 헤더 부터는 그 세션 쿠키를 포함해서 전송해야 함. OutputStream os = con.getOutputStream(); os.write(params.getBytes("UTF-8")); os.flush(); os.close(); Log.d("LOG",url+"로 HTTP 요청 전송"); if (con.getResponseCode() != HttpURLConnection.HTTP_OK) { //이때 요청이 보내짐. Log.d("LOG", "HTTP_OK를 받지 못했습니다."); return null; } BufferedReader reader = new BufferedReader(new InputStreamReader(con.getInputStream(), "UTF-8")); String line; String page = ""; while ((line = reader.readLine()) != null){ page += line; } getCookieHeader(); return page; | cs |
안드로이드에서 HttpUrlConnection을 활용하여 서버로 Http 요청을 보내고 응답을 받는 코드는 위와 같다.
여기서 쿠키,세션과 관련된 가장 중요한 메서드는 setCookieHeader(); getCookieHeader(); 메소드이다. 내가 직접 정의한 메소드이다.
getCookieHedaer()먼저 살펴보자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | private void getCookieHeader(){//Set-Cookie에 배열로 돼있는 쿠키들을 스트링 한줄로 변환 List<String> cookies = con.getHeaderFields().get("Set-Cookie"); //cookies -> [JSESSIONID=D3F829CE262BC65853F851F6549C7F3E; Path=/smartudy; HttpOnly] -> []가 쿠키1개임. //Path -> 쿠키가 유효한 경로 ,/smartudy의 하위 경로에 위의 쿠키를 사용 가능. if (cookies != null) { for (String cookie : cookies) { String sessionid = cookie.split(";\\s*")[0]; //JSESSIONID=FB42C80FC3428ABBEF185C24DBBF6C40를 얻음. //세션아이디가 포함된 쿠키를 얻었음. setSessionIdInSharedPref(sessionid); } } return null; } | cs |
HttpUrlConnection(con) 객체에서 Set-Cookie헤더에 해당하는 것을 얻어 온다. 쿠키는 서버로 부터 여러개가 올 수 있기 때문에 리스트 형식으로 받은것이다. 지금은 서버로부터 하나의 쿠키를 받았기 때문에 []로 감싸진 쿠키 1개를 받았다.
cookie.split() 안에 들어가는 ;\\s*란 정규 표현식을 의미한다. 즉, 쿠키 내부를 보면 ; 또는 공백으로 나눠져 있다. 그것을 기준으로 스트링을 쪼개서 배열로 만드는 String의 함수이다. 0번째 인덱스를 얻게 되면 다음과 같은 서버로 부터 받은 세션 아이디를 추출 해 낼 수 있다.
JSESSIONID=FB42C80FC3428ABBEF185C24DBBF6C40
그리고 나서 이 세션의 아이디를 공유 프리퍼런스에 저장해야 한다.
setSessionIdInSharedPref(sessionid); 를 살펴보자.
1 2 3 4 5 6 7 8 9 10 11 12 | private void setSessionIdInSharedPref(String sessionid){ SharedPreferences pref = context.getSharedPreferences("sessionCookie",Context.MODE_PRIVATE); SharedPreferences.Editor edit = pref.edit(); if(pref.getString("sessionid",null) == null){ //처음 로그인하여 세션아이디를 받은 경우 Log.d("LOG","처음 로그인하여 세션 아이디를 pref에 넣었습니다."+sessionid); }else if(!pref.getString("sessionid",null).equals(sessionid)){ //서버의 세션 아이디 만료 후 갱신된 아이디가 수신된경우 Log.d("LOG","기존의 세션 아이디"+pref.getString("sessionid",null)+"가 만료 되어서 " +"서버의 세션 아이디 "+sessionid+" 로 교체 되었습니다."); } edit.putString("sessionid",sessionid); edit.apply(); //비동기 처리 } | cs |
그냥 단순히 세션의 아이디를 공유 프리퍼런스에 넣는것이다. 마지막에 commit()이 아닌 apply()를 함으로써 비동기적으로 처리하게끔하여 성능을 높였다.
if문과 else if문에서 상황에따라 로그를 다르게 찍도록 하였다. 서버에 처음 로그인하여 처음 세션 아이디를 발급 받은 경우와 안드로이드에서 갖고 있는 세션아이디가 서버에서 만료되어서 서버가 다른 세션의 아이디를 넘겨준경우, 2가지 경우에 위의 함수가 호출 될 수 있기 때문이다.
물론 두가지 경우 모두 그냥 공유 프리퍼런스에 세션 아이디를 저장하는것은 똑같다.
여기까지 했으면 서버로 부터 받은 세션의 아이디가 공유 프리퍼런스에 저장되어 앱을 삭제 하기 전까지 그 세션 아이디가 존재 하게 될 것이다.
그다음은 setCookieHeader를 보자.
1 2 3 4 5 6 7 8 9 10 | private void setCookieHeader(){ SharedPreferences pref = context.getSharedPreferences("sessionCookie",Context.MODE_PRIVATE); String sessionid = pref.getString("sessionid",null); if(sessionid!=null) { Log.d("LOG","세션 아이디"+sessionid+"가 요청 헤더에 포함 되었습니다."); con.setRequestProperty("Cookie", sessionid); } } | cs |
공유 프리퍼런스로부터 세션 아이디를 꺼내서 요청 헤더에 Cookie라는 항목으로 추가해서 전송하는것 밖에 없다.
모든 Http 요청을 보내기 전에 공유 프리퍼런스에 sessionid가 있는지 없는지 확인해서 있으면 요청 헤더에 포함해서 보낸다. 그 세션 아이디가 여전히 서버에서 유효하다면 그 세션아이디에 보관 되어 있는 사용자의 dto에 접근 할 수 있을것이고, 서버의 세션 아이디가 만료 되었다면 서버는 다른 새로운 세션 아이디를 발급하여 안드로이드에 넘겨 줄 것이다. 그럼 안드로이드에서는 공유 프리퍼런스에 다른 세션 아이디를 다시 새롭게 덮어 씌울 것이다.
서버에서 세션의 만료 시간은 web.xml에 다음과 같이 설정하면 된다.
<session-config>
<session-timeout>1</session-timeout> <!-- 분단위 -> (60*24*365)1년간 세션 유지-->
</session-config>
1분단위로 세션 유지 설정 시켜놓고 로그를 한번 찍어 보았다. 실제로 클라이언트가 세션을 할당 받은 다음, 공유 프리퍼런스에 저장하고 그 공유 프리퍼런스에서 세션 아이디를 꺼내서 요청 헤더에 포함해서 보냈을때 서버에서도 제대로 그 세션 아이디에 해당하는 객체를 꺼내올 수 있는지, 또한, 1분뒤에 세션이 만료 되었을때 새로운 세션 아이디를 안드로이드에 전송해주는지를 로그를통해서 확인해 보았다.
71D5A541700A1D4029D1296681803F4A
세션 생성 시간 : 1533398675505
세션 마지막 요청 시간 : 1533398675525
세션 유효 시간 : 60
세션이 존재 합니다.
simsimjaejae님이 로그인 상태 입니다.
EEAAC21940F5D7163C88CF9B63395555
세션 생성 시간 : 1533398765511
세션 마지막 요청 시간 : 1533398765511
세션 유효 시간 : 60
세션이 존재 하지 않습니다.
로그는 위와 같은데, 세션 아이디가 달라졌다는 점을 주의 깊게 보면 될것 같다. 세션 생성 시간과 세션 마지막 요청 시간은 session객체의 메소드를 통해서 얻을 수 있는데, 1970 1월을 기준으로 얼마나 지났는지를 체크하는거라 저렇게 큰 숫자가 나온다고하는데 아직 이부분은 정확히 파악하지 못했다.
세션이 존재 하다가 1분뒤에 세션이 만료되어 세션이 존재 하지 않다고, 로그가 찍힌것을 확인 할 수 있다.
이런식으로 서버와 안드로이드 간의 세션 유지 방법을 알아 보았다.
이렇게 정리해보니까 별거 아닌거 같은데, 책에도 잘 안나와 있어서 구글링하여 힘들게 알아냈다. 그래도 실력이 조금은 늘은것 같아서 기분은 좋다.
'컴퓨터 공학과 졸업 > 안드 개발 기록' 카테고리의 다른 글
[비밀] 멀티파트 요청 (0) | 2018.08.08 |
---|---|
[퍼옴] 서버와 세션 유지 (0) | 2018.08.04 |
싱글터치 멀티 터치 MotionEvent (0) | 2018.07.25 |
Canvas.save() Canvas.restore() (1) | 2018.07.25 |
외장메모리공간 (0) | 2018.07.22 |
- Total
- Today
- Yesterday
- type alias
- Babel
- atomic design
- Action
- useEffect
- state
- hydrate
- es6
- javascript
- rendering scope
- server side rendering
- Next.js
- reducer
- props
- mobx
- react hooks
- react
- reflow
- design system
- storybook
- webpack
- return type
- promise
- async
- typescript
- await
- reactdom
- useRef
- computed
- Polyfill
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |