82일차(2)/Android App(45) : Interceptor 활용하기
- 로그인 여부에 따라서 응답방식이 달라지는 기능은?
- jsp의 LoginFilter, spring의 Interceptor와 같은 종류
- 기존에는 LoginInterceptor 를 만들어서 Webconfig에 등록했다.
- WebMvcConfigurer 인터페이스를 구현하고 @Configuration 어노테이션 붙이기
- 특정 폴더 하위의 모든 요청이 interceptor를 거치게 했다.
LoginInterceptor
package com.sy.boot07.interceptor;
import java.net.URLEncoder;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
//로그인된 사용자인지 검사할 인터셉터
@Component
public class LoginInterceptor implements HandlerInterceptor{
//Controller 메소드 수행직전에 로그인된 사용자 인지 검증을 해서
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
//세션 객체의 참조값을 얻어와서
HttpSession session=request.getSession();
String id=(String)session.getAttribute("id");
//만일 로그인을 하지 않았다면
if(id == null) {
//로그인 페이지로 리다일렉트 이동 시키고 false 를 리턴한다.
//원래 가려던 url 정보 읽어오기
String url=request.getRequestURI();
//GET 방식 전송 파라미터를 query 문자열로 읽어오기 ( a=xxx&b=xxx&c=xxx )
String query=request.getQueryString();
//특수 문자는 인코딩을 해야한다.
String encodedUrl=null;
if(query==null) {//전송 파라미터가 없다면
encodedUrl=URLEncoder.encode(url);
}else {
// 원래 목적지가 /test/xxx.jsp 라고 가정하면 아래와 같은 형식의 문자열을 만든다.
// "/test/xxx.jsp?a=xxx&b=xxx ..."
encodedUrl=URLEncoder.encode(url+"?"+query);
}
//3. 로그인을 하지 않았다면 /users/loginform 페이지로 리다일렉트 이동 시킨다. (HttpServletResponse)
String cPath=request.getContextPath();
response.sendRedirect(cPath+"/users/loginform?url="+encodedUrl);
return false;
}
//로그인을 했다면 흐름을 이어간다.
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
// TODO Auto-generated method stub
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
// TODO Auto-generated method stub
}
}
- HandlerInterceptor 를 구현해서 만들고 @Component 어노테이션을 붙인다.
- Bean으로 만들어져서 여기에 이렇게 주입되는 것이다.
- 주입된 interceptor는 registry에서 어떤 경로 요청에 대해서 인터셉터가 동작하게 할 지 설정했다.
- 로그인된 아이디가 없다면 리다이렉트 이동을 시키는데,
안드로이드 앱에 대해서 리다이렉트는 의미가 없다.
- 웹브라우저가 아니므로 리다이렉트 응답에 대한 대비가 되어있지 않으면 동작하지 않는다.
- 따라서 안드로이드 요청에 대해서 대응할 인터셉터를 따로 만들어야 한다.
MobileLoginInterceptor
package com.sy.boot07.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
//Bean이 될 수 있도록 어노테이션 붙이기
@Component
public class MobileLoginInterceptor implements HandlerInterceptor{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
//세션 객체의 참조값을 얻어와서
HttpSession session=request.getSession();
String id=(String)session.getAttribute("id");
//만일 로그인을 하지 않았다면
if(id == null) {
//인증이 되지 않았다는 에러를 응답한다.
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
//로그인을 했다면 흐름을 이어간다.
return true;
}
}
- HandlerInterceptor 구현
- Component 어노테이션
- 인증이 되지 않았다면 에러를 응답한다.(401번 에러)
- 이 안드로이드의 responseCode=401 인 것과 같다.
- 이 코드값이 urlConnection 안에서 getResponseCode 하면 나온다.
- 로그인하지 않았다면 에러를 응답하고, login 했다면 개입하지 않는다.
WebConfig
package com.sy.boot07.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import com.sy.boot07.interceptor.LoginInterceptor;
import com.sy.boot07.interceptor.MobileLoginInterceptor;
@Configuration
public class WebConfig implements WebMvcConfigurer{
//로그인 인터셉터 주입 받기
@Autowired LoginInterceptor loginInterceptor;
@Autowired MobileLoginInterceptor mLoginInterceptor;
//인터셉터 동작하도록 등록하기
@Override
public void addInterceptors(InterceptorRegistry registry) {
//웹브라우저의 요청에 대해 개입할 인터셉터 등록
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/users/*","/gallery/*","/cafe/*","/file/*")
.excludePathPatterns("/users/signup_form", "/users/signup", "/users/loginform", "/users/login",
"/gallery/list", "/gallery/detail",
"/cafe/list","cafe/detail","/cafe/ajax_comment_list",
"/file/list","/file/download");
//모바일 요청에 대해 개입할 인터셉터 등록
registry.addInterceptor(mLoginInterceptor)
.addPathPatterns("/api/gallery/*");
}
// resources 폴더안에 있는 자원을 spring 컨트롤러를 거치지 않고 응답되도록 설정
// webapp 안에 resources 폴더를 만들어야 한다.
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
}
}
- WebConfig 에 인터셉터를 추가해줘야 한다.
- 이런식으로 모바일 요청에 대해서도 interceptor가 개입하도록 할 수 있다.
- 이제 로그인했으면 개입하지 않고, 로그인하지 않았으면 401 에러가 응답된다.
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/userInfo"
android:background="#00ff00"
android:textSize="30sp"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/login"
android:text="로그인"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/logout"
android:text="로그아웃"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/gallery"
android:text="갤러리 목록보기"/>
</LinearLayout>
- 안드로이드에서 http 요청을 받아올 때도, 로그인하지 않은 클라이언트의 임의의 요청을 막아야 할 경우가 있을 수 있다.
Android Controller
package com.sy.boot07.api;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.sy.boot07.gallery.dao.GalleryDao;
import com.sy.boot07.gallery.dto.GalleryDto;
@Controller
public class AndroidController {
/*
* JSON 문자열
* 1. @ResponseBody 어노테이션
* 2. Map 혹은 List 혹은 Dto 를 리턴하면 자동으로 JSON 문자열로 변환 되어서 응답된다.
*/
@RequestMapping("/api/send")
@ResponseBody
public Map<String, Object> send(String msg){
System.out.println(msg);
Map<String, Object> map=new HashMap<>();
map.put("isSuccess", true);
map.put("response", "hello client!");
map.put("num", 1);
return map;
}
@RequestMapping("/api/list")
@ResponseBody
public List<String> list(int pageNum){
List<String> names=new ArrayList<>();
names.add("바나나");
names.add("딸기");
names.add("복숭아");
return names;
}
//로그인 여부를 json으로 응답하는 메소드
@RequestMapping("/api/logincheck")
@ResponseBody
public Map<String, Object> logincheck(HttpSession session){
//테스트로 session의 아이디를 출력해보기
System.out.println("세션 아이디:"+session.getId());
Map<String, Object> map=new HashMap<>();
//세션 영역에 id라는 키값으로 저장된 값이 있는지 읽어와 본다.
String id=(String)session.getAttribute("id");
//만일 로그인을 하지 않았다면
if(id == null) {
map.put("isLogin", false);
System.out.println("로그인중이 아님요");
}else {
map.put("isLogin", true);
map.put("id", id);
System.out.println(id+" 로그인중...");
}
return map;
}
@RequestMapping("/api/login")
@ResponseBody
public Map<String, Object> login(String id, String pwd, HttpSession session){
//session.setMaxInactiveInterval(60*60*24); //로그인 유지시간
System.out.println(id+"|"+pwd);
Map<String, Object> map=new HashMap<>();
if(id.equals("gura") && pwd.equals("1234")) {
map.put("isSuccess", true);
map.put("id", id);
session.setAttribute("id", id);
}else {
map.put("isSuccess", false);
}
return map;
}
@RequestMapping("/api/logout")
@ResponseBody
public Map<String, Object> logout(HttpSession session){
session.invalidate();
Map<String, Object> map=new HashMap<>();
map.put("isSuccess", true);
return map;
}
@Autowired GalleryDao dao;
@RequestMapping("/api/gallery/list")
@ResponseBody
public List<GalleryDto> galleryList(){
//최신 갤러리 데이터 10개만 가져오기
GalleryDto dto=new GalleryDto();
dto.setStartRowNum(1);
dto.setEndRowNum(10);
return dao.getList(dto);
}
}
- 안드로이드에서 /api/gallery/list를 요청하면 이 dto를 담아서 JSON으로 리턴한다.
- 로그인해야만 정상적으로 응답된다.
- 해당 페이지에 직접 접속해보면 401 에러가 일어난다.
로그인하지 않았기 때문에!
- 로그인하고 동일한 경로로 들어가면 이렇게 데이터를 보내준다.
새 액티비티- BasicActivity 생성
GalleryActivity
package com.example.step18login;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import androidx.navigation.ui.AppBarConfiguration;
import androidx.navigation.ui.NavigationUI;
import com.example.step18login.databinding.ActivityGalleryBinding;
import com.google.android.material.snackbar.Snackbar;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
public class GalleryActivity extends AppCompatActivity {
private AppBarConfiguration appBarConfiguration;
private ActivityGalleryBinding binding;
SharedPreferences pref;
String sessionId;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//View 바인딩을 이용해서 화면 구성하기
binding = ActivityGalleryBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setSupportActionBar(binding.toolbar);
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_gallery);
appBarConfiguration = new AppBarConfiguration.Builder(navController.getGraph()).build();
NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
//우하단의 float action 버튼을 눌렀을 때 동작할 리스너 등록하기
binding.fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//Snackbar 에 메세지를 길게 띄우기
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
}
@Override
public boolean onSupportNavigateUp() {
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_gallery);
return NavigationUI.navigateUp(navController, appBarConfiguration)
|| super.onSupportNavigateUp();
}
@Override
protected void onStart() {
super.onStart();
//SharedPreferences 객체의 참조값 얻어와서 필드에 저장하기
pref= PreferenceManager.getDefaultSharedPreferences(this);
//저장된 session id 가 있는지 읽어와 본다.(없다면 기본값은 빈 문자열)
sessionId=pref.getString("sessionId", "");
//갤러리 1페이지의 데이터 요청 task 실행하기
new GalleryListTask().execute(1);
}
class GalleryListTask extends AsyncTask<Integer, Void, String>{
@Override
protected String doInBackground(Integer... integers) {
//갤러리 목록 요청 url
String requestUrl=AppConstants.BASE_URL+"/gallery/list?pageNum="+integers[0];
//서버가 http 요청에 대해서 응답하는 문자열을 누적할 객체
StringBuilder builder=new StringBuilder();
HttpURLConnection conn=null;
InputStreamReader isr=null;
BufferedReader br=null;
try{
//URL 객체 생성
URL url=new URL(requestUrl);
//HttpURLConnection 객체의 참조값 얻어오기
conn=(HttpURLConnection)url.openConnection();
if(conn!=null){//연결이 되었다면
conn.setConnectTimeout(20000); //응답을 기다리는 최대 대기 시간
conn.setRequestMethod("GET");//Default 설정
conn.setUseCaches(false);//케쉬 사용 여부
//App에 저장된 session id가 있다면 요청할 때 쿠키로 같이 보내기
if(!sessionId.equals("")) {
//JSESSIONID=xxx 형식의 문자열을 쿠키로 보내기
conn.setRequestProperty("Cookie", sessionId);
}
//응답 코드를 읽어온다.
int responseCode=conn.getResponseCode();
if(responseCode==200){//정상 응답이라면...
//서버가 출력하는 문자열을 읽어오기 위한 객체
isr=new InputStreamReader(conn.getInputStream());
br=new BufferedReader(isr);
//반복문 돌면서 읽어오기
while(true){
//한줄씩 읽어들인다.
String line=br.readLine();
//더이상 읽어올 문자열이 없으면 반복문 탈출
if(line==null)break;
//읽어온 문자열 누적 시키기
builder.append(line);
}
}else if(responseCode== HttpURLConnection.HTTP_UNAUTHORIZED){//로그인이 안된 상태라면
//예외를 발생시켜서 try~catch~finally 절이 정상 수행되도록 한다.
throw new RuntimeException("로그인이 필요합니다.");
}
}
//서버가 응답한 쿠키 목록을 읽어온다.
List<String> cookList=conn.getHeaderFields().get("Set-Cookie");
//만일 쿠키가 존재한다면
if(cookList != null){
//반복문 돌면서
for(String tmp : cookList){
//session id 가 들어있는 쿠키를 찾아내서
if(tmp.contains("JSESSIONID")){
//session id만 추출해서
String sessionId=tmp.split(";")[0];
//SharedPreferences 를 편집할 수 있는 객체를 활용해서
SharedPreferences.Editor editor=pref.edit();
//sessionId 라는 키값으로 session id 값을 저장한다.
editor.putString("sessionId", sessionId);
editor.apply(); //apply() 는 비동기로 저장하기 때문에 실행의 흐름이 잡혀있지 않다.
//필드에도 담아둔다.
GalleryActivity.this.sessionId=sessionId;
}
}
}
}catch(Exception e){//예외가 발생하면
Log.e("GalleryListTask", e.getMessage());
}finally {
try{
if(isr!=null)isr.close();
if(br!=null)br.close();
if(conn!=null)conn.disconnect();
}catch(Exception e){}
}
//String Builder 객체에 담긴 문자열을 리턴해준다.
return builder.toString();
}
@Override
protected void onPostExecute(String s) {
super.onPostExecute(s);
//응답된 문자열을 토스트 메세지로 띄워보기
Toast.makeText(GalleryActivity.this, s, Toast.LENGTH_LONG).show();
}
}
}
activity_gallery.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".GalleryActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/Theme.MyAndroid.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/Theme.MyAndroid.PopupOverlay" />
</com.google.android.material.appbar.AppBarLayout>
<include layout="@layout/content_gallery" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="@dimen/fab_margin"
android:layout_marginBottom="16dp"
app:srcCompat="@android:drawable/ic_dialog_email" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
- Basic Activity는 이런 모양을 가지고 있다.
- 갤러리 버튼에 GalleryActivity로 이동할 수 있도록 설정 (intent 객체 사용)
- Basic Activity 는 버튼으로 전환 가능한 2개의 프래그먼트로 되어잇고,
우하단의 이메일 모양의 아이콘을 누르면 하단에 snackbar가 잠시 떴다가 사라진다.
- GalleryActivity (Basic Activity)의 코드확인
- Viewbinding을 사용하고 있다.
- 플로팅 버튼을 누르면 실행할 리스너가 들어있다.
- 이 스낵바를 누르면 snackbar 안에 있는 리스너가 실행된다.(기본은 null로 등록되지 않은 상태)
- galleryActivity에서 http 요청 해서 json 문자열 출력해보기
(이미지뷰에 이미지 출력하는건 나중에)
- 파라미터 타입은 Integer (페이지번호 전달)
- 안에서 doInBackground, onPostExecute 메소드 오버라이드
- 각 메소드의 인자 타입은 AsyncTask의 제너릭 타입과 각각 위와 같은 연관관계를 가진다.
- onStart 메소드를 만들고, 1페이지에 해당하는 내용만 불러와 본다.
- get방식 파라미터의 pageNum=x 로 나올 수 있도록 코딩하기
- 200일 경우, 401일 경우로 나누어 if로 분기한다.
- 각 응답코드는 이렇게 상수로 저장되어 있으므로 상수를 사용하면 더 좋다.
- 갤러리 액티비티의 sessionId에 넣어주기
- 그런데 이 중간의 else if 절에서 return 하면 아래의 코드들이 실행되지 않으므로
여기서는 예외를 발생시켜 throw로 보내준다.
- throw로 보낸 내용이 catch 로 들어오고, 최종적으로 Bulilder에 담긴 문자열이 출력될 수 있도록 한다!
- 로그인하지 않으면 토스트 메시지에 아무것도 응답되지 않는다. (빈 문자열)
- 로그인하면 이렇게 토스트 메시지로 아까 JSON으로 출력된 문자열이 전달된다.
- 인증된 사용자의 요청에만 선택적으로 응답하도록 했다!
- 리다일렉트 응답은 301 또는 302이다.
- 이것을 응답하게 하려면 안드로이드에서도 이 리다일렉트 요청에 응답할 대비가 되어있어야 한다.
- 그 대비된 경로로 요청을 해주어야 한다.
- 위와 같이 세션(쿠키 기능)을 이용해서 로그인 정보를 받아오고,
응답되는 쿠키를 확인해서 새로 받은 세션 아이디를 업데이트할 수 있다.
'국비교육(22-23)' 카테고리의 다른 글
83일차(1)/Android App(46) : 전화걸기 기능 구현 (0) | 2023.02.07 |
---|---|
[Android Studio] 모듈 의존성 제거 (모듈 삭제) (0) | 2023.02.07 |
82일차(1)/Android App(44) : 로그아웃 기능 구현 (0) | 2023.02.06 |
80일차(1)/Android App(43) : 로그인 기능 구현 (0) | 2023.02.03 |
79일차(1)/Android App(42) : Http 요청으로 Oracle DB 출력하기 (0) | 2023.02.02 |