스프링 Session 쇼핑몰 방금 본 상품&권한
지난 시간에는 스프링 session으로 회원가입을 하고, 자동로그인을 코드로 구현해 보았다. 이전 포스팅과 이어지는 포스팅이라서 이전 포스팅을 참고하고 오면 좋을것 같다. 이번 시간에는 회원가입 후 가입한 내용으로 로그인 로직을 구현해보고, 쇼핑몰에서 방금 내가 봤던 상품을 띄워주는 로직과 사장님만 들어갈 수 있는 admin 페이지 비슷한 내용을 만들어 볼려고한다.
loginPage 생성
먼저 아래와 같이 로그인 페이지를 생성해준다.
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page contentType="text/html; charset=UTF-8" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>login</title>
</head>
<body>
<h1>login</h1>
<hr/>
<!-- 시큐리티는 x-www-form-url-encoded 타입만 인식 -->
<form action="/loginProc" method="post">
<input type="text" name="username" />
<input type="password" name="password" />
<button>login</button>
</form>
<a href="/joinPage">회원가입을 아직 안하셨나요?</a>
<P>${msg}</P>
</body>
</html>
UserRepository 유저 조회
UserRepository 클래스에서 유저정보를 찾는 로직을 작성한다. 없으면 없다는 메시지를 전달하기 위해서 Map을 이용해서 오류문구를 로그인 화면의 loginPage.html 의 ${msg}로 전달해준다. 로그인 성공을 하면 로그인한 유저 정보를 Map에 담아서 Controller로 넘겨준다. 그럼 Controller를 보자.
//유저 조회
public Map<String, String> selectUser(Map<String, String> userInfoMap) throws Exception{
//결과 문구
Map<String, String> resultMap = new HashMap<>();
//전달 받은 유저 정보
String userName = userInfoMap.get("username");
String userPassword = userInfoMap.get("password");
//유저 정보 찾기
for (int i = 0; i < userList.size(); i++) {
Map<String, String> targetUser = userList.get(i);
//서버에 저장된 유저 정보
String targetUserName = targetUser.get("username");
String targetUserPassword = targetUser.get("password");
String targetUserRole = targetUser.get("role");
//이름체크
//db에 해당유저명이 존재한다면(참고로 유저명은 유니크하다고 가정한다.)
if(userName.equals(targetUserName)) {
//비밀번호 체크
if(userPassword.equals(targetUserPassword)) {
//로그인 성공
resultMap.put("msg", "로그인성공");
resultMap.put("rst", "S");
resultMap.put("userRole", targetUserRole);
resultMap.put("userName", targetUserName);
return resultMap;
}else {
resultMap.put("msg", "비밀번호가 틀렸습니다.");
resultMap.put("rst", "F");
}
}else {
resultMap.put("msg", "username이 존재하지 않습니다.");
resultMap.put("rst", "F");
}
}
return resultMap;
}
LoginController 작성
LoginController내용은 아래와 같다. 여기서 중요한 부분은 loginProc 메소드에서 로그인이 성공했다면, 즉 DB에 유저가 존재한다면 유저정보를 세션에 넣어주고, 로그인 실패했다면 실패 정보를 화면으로 던져주는 부분이다.
package com.session.my;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class LoginController {
@Autowired
private UserRepository userRepository;
//로그인 페이지
@RequestMapping(value = "/loginPage", method = RequestMethod.GET)
public String loginPage(Model model) {
System.out.println("loginPage - 진입");
//model.addAttribute("serverTime", formattedDate );
return "loginPage";
}
//로그인
@RequestMapping(value = "/loginProc", method = RequestMethod.POST)
public String loginProc(User user, HttpServletRequest request, HttpServletResponse response, Model model) throws Exception{
System.out.println("loginProc - 진입");
String username = user.getUsername();
String password = user.getPassword();
System.out.println("username : "+ username);
System.out.println("password : "+ password);
//회원정보 저장
Map<String, String> userInfoMap = new HashMap<String, String>();
userInfoMap.put("username", username);
userInfoMap.put("password", password);
//db에서 조회
Map<String, String> resultMap = userRepository.selectUser(userInfoMap);
//로그인 성공
if(resultMap.get("rst").equals("S")) {
//세션 : 유저명 + 권한
HttpSession session = request.getSession();
session.setAttribute("userSession", resultMap.get("userName"));
session.setAttribute("role", resultMap.get("userRole"));
return "redirect:/";
}else {
//로그인 실패
model.addAttribute("msg", resultMap.get("msg") );
}
return "loginPage";
}
//로그아웃
@RequestMapping(value = "/logout", method = RequestMethod.GET)
public String logout(HttpServletRequest request) {
System.out.println("logout - 진입");
//세션 끊기
HttpSession session = request.getSession();
session.invalidate();
return "home";
}
}
메인페이지 작성
메인페이지에서는 로그인 성공후 세션이 존재하면 유저 정보를 노출해주고, 세션이 없으면 로그인페이지와 가입페이지를 보여주도록 한다. 그리고 아래의 products 와 adminPage는 권한처리를 위해 미리 작성해 두었다.
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page contentType="text/html; charset=UTF-8" language="java"%>
<html>
<head>
<title>Home</title>
</head>
<body>
<h1>메인화면!</h1>
<%
session = request.getSession();
if(session.getAttribute("userSession") != null){
out.print("<h3>" + session.getAttribute("userSession")+ "님 반갑습니다." + "</h3>" + "<br>");
out.print("<a href='/logout'>logout</a>" +"<br>");
}else{
out.print("<a href='/loginPage'>login</a>" +"<br>");
out.print("<a href='/joinPage'>join</a>" +"<br>");
}
%>
<a href="/products">products</a>
<a href="/adminPage">admin</a>
</body>
</html>
로그인 테스트
http://localhost:8080/loginPage로 접속한 다음 이미 샘플로 넣어둔 이름 kim 비밀번호 1111을 넣어서 로그인 해보자.
스프링 세션 장바구니&방금 본 상품
스프링 세션으로 방금 본 상품을 띄워주는 로직을 구현하면서 권한 중에서 일반 user가 아닌 admin 권한만 가진 사용자만 products 화면에 들어가서 상품 상세화면을 볼 수 있도록 하는 로직을 구현해볼것이다. 여기서 의문을 가질 수 있다. 서버 컨트롤러에서 세션을 만들어 주고 화면에서 세션이 있으면 상품리스트 메뉴를 보여주고, 없으면 숨겨서 처리를 하면되지 않냐고 생각할 수도 있다. 그렇게 처리를 할 수도 있지만 보안에 너무나 취약하다. 만약에 어떤 유저가 웹페이지를 우클릭해서 페이지 소스보기를 클릭해서 url을 알게되거나, 또 다른 경로로 권한이 부여된 특정 url을 알게 된다면 브라우저에 입력하는 순간 서버로 부터 응답을 받게 된다. 그래서 메뉴(url)를 숨겨 놓더라도 더 안전하게, 컨트롤러에 진입하기 전에 인터셉터에서 url 요청을 가로채서 권한 체크를 하는 것이다. 이를 위해서 상품관련 화면 페이지를 만들고 권한을 체크해 주는 인터셉터와 컨트롤러를 작성한다.
상품 컨트롤러 작성
아래는 상품 목록 페이지로 요청이 들어왔을 경우 상품 목록 페이지로 이동 시켜주고, 특정 상품을 클릭했을 때, 세션에 상품 정보를 넣어서 화면으로 던져주는 컨트롤러다.
package com.session.my;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class ProductController {
//상품 목록
@RequestMapping(value = "/products", method = RequestMethod.GET)
public String selectProductList(HttpServletRequest request, Model model) {
System.out.println("selectProductList - 진입");
return "product";
}
//상품 상세
@RequestMapping(value = "/product", method = RequestMethod.GET)
public String selectProduct(@RequestParam String name , @RequestParam String id ,HttpServletRequest request, Model model) {
System.out.println("selectProduct - 진입");
System.out.println("prodId : " + id);
System.out.println("prodName : " + name);
//세션에 클릭한 상품 정보 넣어주기
HttpSession session = request.getSession();
session.setAttribute("prodId", id);
session.setAttribute("prodName", name);
//특정 세션 삭제
//session.removeAttribute("prodId");
//session.removeAttribute("prodName");
model.addAttribute("prodId", id);
model.addAttribute("prodName", name);
return "productDetail";
}
}
인터셉터 작성
인터셉터를 만들어서 user의 권한 중에 admin이라는 권한을 가진 사람만 상품 컨트롤러에 접근 할 수 있도록 체크하는 로직을 구현한다. 인터셉터는 아래와 같다.
package com.session.my;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
//Interceptor를 구현하는 방법은 2가지가 있는데,
//HandlerInterceptor 인터페이스를 구현하는 방법과 HandlerInterceptorAdapter 클래스를 상속 받는 방법이 있습니다.
public class MyInterceptor implements HandlerInterceptor{
// controller로 보내기 전에 처리하는 인터셉터
// 반환이 false라면 controller로 요청을 안함
// 매개변수 Object는 핸들러 정보를 의미한다. ( RequestMapping , DefaultServletHandler )
/*
1.preHandle() 메서드는 컨트롤러가 호출되기 전에 실행됩니다.
매개변수 obj는 Dispatcher의 HandlerMapping 이 찾아준 컨트롤러의 메서드를 참조할 수 있는 HandlerMethod 객체입니다.
*/
@Override
public boolean preHandle(
HttpServletRequest request, HttpServletResponse response,
Object obj) throws Exception {
System.out.println("MyInterCeptor - preHandle");
System.out.println("obj : " + obj.getClass());
HttpSession session = request.getSession();
if(session != null) {
String username = (String)session.getAttribute("userSession");
if(username != null) {
String role = (String)session.getAttribute("role");
System.out.println("role : " + role);
if(role.equals("admin")) {
return true;
}
}else {
System.out.println("username이 존재하지 않습니다.");
}
}else {
System.out.println("세션이 존재하지 않습니다.");
}
//home으로 리턴 (특정페이지로 리턴 가능)
response.sendRedirect(request.getContextPath() + "/");
//false 가 리턴되면 요청 이후로 안넘어간다.
return false;
}
// controller의 handler가 끝나면 처리됨
@Override
public void postHandle(
HttpServletRequest request, HttpServletResponse response,
Object obj, ModelAndView mav)
throws Exception {
}
// view까지 처리가 끝난 후에 처리됨
@Override
public void afterCompletion(
HttpServletRequest request, HttpServletResponse response,
Object obj, Exception e)
throws Exception {
}
}
servlet-context.xml 작성
servlet-context.xml 파일에 위에서 만든 인터셉터 클래스를 빈 객체로 등록 시켜준다. 그리고 중요한 부분은 인터셉터의 path를 /products 로 설정을 해주었다. 이렇게 설정을 해주면 인터셉터에서 /products 주소로 호출되는 url만 잡아서 세션체크를 한다.
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!--
필터의 경우 서블릿(servlet)에서 제어하기 때문에 웹 애플리케이션 내에서 동작하고,
따라서 스프링의 Context에 접근할 수 없다.
하지만 인터셉터의 경우, Spring Context내에서 존재하므로
스프링의 모든 객체를 활용 할 수 있게 된다.
따라서 Spring의 빈으로 등록된 컨트롤러나 서비스 객체들을 기존 그대로 주입받아서 사용할 수 있게 된다.
설정위치
Interceptor는 DispatcherServlet이 실행된 후,
Interceptor는 spring-servlet.xml,
Interceptor는 설정은 물론 메서드 구현이 필요.
-->
<!-- interceptor 프로그램 등록 -->
<beans:bean id="MyInterceptor" class="com.session.my.MyInterceptor" />
<interceptors>
<interceptor>
<mapping path="/products"/>
<beans:ref bean="MyInterceptor"/>
</interceptor>
</interceptors>
<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
<!-- Enables the Spring MVC @Controller programming model -->
<annotation-driven />
<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
<resources mapping="/resources/**" location="/resources/" />
<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<beans:property name="prefix" value="/WEB-INF/views/" />
<beans:property name="suffix" value=".jsp" />
</beans:bean>
<context:component-scan base-package="com.session.my" />
</beans:beans>
상품리스트 페이지 작성
상품리스트 페이지에 세션객체를 가져와서 컨트롤러에서 넣어준 내가 방금 본상품이 무엇인지를 찾는다. prodId가 있으면 내가 방금 본 상품이 있다는 뜻이므로 방금 본 상품을 보여주고, 없으면 안보여준다. 그리고 아래 아이폰12와 맥북 프로 m1 상품을 올려놓았다. 해당 상품 url을 누르면 상품 상세 페이지로 이동한다.
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page contentType="text/html; charset=UTF-8" language="java"%>
<html>
<head>
<title>Product list</title>
</head>
<body>
<h1>product list</h1>
<%
session = request.getSession();
if(session.getAttribute("userSession") != null){
out.print("userSession name : " + session.getAttribute("userSession") + "<br>");
}
if(session.getAttribute("prodId") != null){
out.print("방금본 상품 <br>");
out.print("아이디 : " + session.getAttribute("prodId") + "<br>");
out.print("상품명 : " + session.getAttribute("prodName") + "<br>");
}
%>
<br>
<a href="/product?name=ipone12&id=i1">ipone12</a>
<a href="/product?name=macbookpro&id=i2">macbookpro</a>
</body>
</html>
상품상세 페이지
컨트롤러에서 넘겨받은 상품의 id와 상품이름을 보여준다.
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page contentType="text/html; charset=UTF-8" language="java"%>
<html>
<head>
<title>Product detail</title>
</head>
<body>
<h1>product detail</h1>
<%
session = request.getSession();
if(session.getAttribute("userSession") != null){
out.print("session name : " + session.getAttribute("userSession") + "<br>");
}
%>
<br>
<h4> 상품 id : ${prodId} </h4>
<h4> 이름 : ${prodName} </h4>
</body>
</html>
권한 테스트 및 방금 본 상품 테스트
이름 kim 비밀번호 1111 계정은 admin 계정이라서 상품 상세페이지로 접근을 할 수 있다. 로그인해서 상품 목록 버튼을 눌러보자. 아래 아이폰 12와 맥북프로 m1은 상품 목록이라고 생각하자.
그리고 해당 상품을 클릭하면 상품 상세 페이지로 이동한다.
다시 상품 목록 페이지로 돌아갔을 경우 방금 본 iphone 12 상품 아이디와 이름이 화면에 노출되어 있다. 단순하게 만들어서 이름이 텍스트로 나오지 보통은 가공을 해서 화면 우측 하단에 상품 이미지로 작게 보여준다.
마지막으로 로그아웃을 해보자. 로그아웃 후에 새로 가입을 하고 가입한 유저 정보로 로그인을 하자. 그리고 상품 상세 페이지를 클릭해보자. 아무리 눌러도 상품 상세페이지로 이동이 안된다. 왜냐면 인터셉터에서 세션을 체크해서 권한이 user이기 때문에 컨트롤러로 이동 안시키고 false를 리턴해버리기 때문이다.
정리
이번 포스팅에서는 이전 포스팅에서 나열한 몽실이가 아이폰12를 아마존 사이트에서 구입할 때 발생한 문제점을 해결했다. 몽실이는 아마존에서 아이폰12를 구입할때, 마우스를 잘못눌러서 화면이 닫혀져서 다시 로그인해야했고, 조금전 발견했던 아이폰12 상품을 다시 찾아야 했다. 그래서 두번의 포스팅으로 몽실이의 문제 두가지를 스프링 세션으로 해결해보는 시간을 가졌다. 세션의 개념에 초점을 맞춘 코드라서 실무에서 그대로 사용하기는 무리가 있다. 하지만 이번 세션 구현을 통해서 세션에 대해서 이해가 조금이라도 됐을것이라고 생각한다. 단순히 코드만 나열하기 보다는 session과 관련된 핵심 부분을 잘설명을 하면서 포스팅하려고 했는데, session에 대한 그림이 조금 그려질지 모르겠다. session을 처음 접하는 사람들에게 도움이 되길 바란다. 위의 코드만 보고 도저히 모르겠다고 생각되는 사람은 아래 소스코드 전체를 올려놓았으니 다운받아서 실행해보자.
session 예제 파일
'웹개발 > 보안' 카테고리의 다른 글
카카오 로그인 OAuth2.0 (0) | 2020.12.08 |
---|---|
OAuth2 인증 - 인가 코드 그랜트 (0) | 2020.12.05 |
OAuth2 인증 - 암시적 코드 그랜트 (0) | 2020.12.05 |
스프링 Session으로 자동 로그인 구현하기 (0) | 2020.12.01 |
세션의 개념과 단점 (0) | 2020.12.01 |