→ 쿠키, 세션
→ java의 HttpSession ⇒ JSESSIONID
→ 오픈소스 외에는, 직접 ID/PW를 활용하여 각 User의 역할 기준으로 서비스(페이지)를 이용할 수 있는 접근법을 어떻게 하는가는 공부해본 적이 없었음
사용자가 회원가입을 하고 나서, 로그인을 통해 id / pw를 기입하고 로그인 처리가 됐다고 해서, 애플리케이션에서 제공하는 모든 서비스를 다 이용할 수 있다???(권한 이런 거 생각하지 말고, 순수히) ⇒ x
왜 x냐? HTTP의 특성 때문이다. HTTP의 특성은 크게 2가지가 있다.

위와 같이 HTTP의 특성으로 비연결지향과 상태없음이 있기 때문에, User가 로그인 화면을 통해서 로그인 처리가 OK 됐다고 해서, 어느 화면이나 어느 서비스나 다 자연스럽게 사용할 수 있다고 생각하면 안된다.
그런데, 애플리케이션에서는 로그인 이후에도 User가 로그인 상태를 유지한 채로 서비스를 이용할 수 있게 하는 것일까? 그것은 바로 ‘세션’ 또는 ‘쿠키’를 이용하기 때문이다.
위에서 말했든, 로그인 이후에도 User가 로그인 상태를 유지한 채로 서비스를 이용할 수 있게 하기 위해 ‘세션’ 또는 ‘쿠키’를 이용한다고 말했다. 좀 더 개발자스럽게 이야기를 하면, 아래와 같다.
그렇다. 위와 같이, 애플리케이션은 클라이언트의 로그인 상태(로그아웃 전까지)를 지속적으로 알아야 하기 때문에 쿠키와 세션을 사용하는 것이다.
여기서는, 쿠키와 세션 자체가 무엇이냐를 알아가는 포스팅이 아니기 때문에, 쿠키와 세션 자체는 다른 블로그나 책을 참고하도록 하자.
대신, 여기서는 쿠키를 활용한 로그인 구현 방식과, 세션을 활용한 로그인 구현 방식을 살펴보도록 하자.
→ 로그인

memberId)를 담아 클라이언트에 전달한다.memberId 쿠키를 저장한다.→ 로그인 상태

memberId라는 쿠키를 전달한다.memberId 값으로 member를 조회하여 응답한다.→ 쿠키의 단점
쿠키도 좋아 보이기는 하지만, 여러 단점이 존재한다.
위에 기재된 단점처럼 브라우저에 의존적이고, 용량도 한계가 있고, 결정적으로 보안에 취약하다.
그렇다면… 쿠키가 아닌 세션을 이용한 로그인은 어떻게 처리되는지 한번 보도록 하자.
→ 로그인

mySessionId 쿠키를 생성한다.mySessionId 값으로 추정 불가능한 랜덤 값을 할당한다.mySessionId와 member를 묶어 보관하고 클라이언트에는 mySessionId만 전달한다.mySessionId를 저장한다.→ 로그인 상태

mySessionId라는 쿠키를 전달한다.mySessionId의 쿠키 값으로 세션 저장소에서 member를 조회하여 응답한다.→ 세션 정리
member)과 관련된 중요한 정보를 갖고있지 않다.mySessionId)는 추정 불가능한 값이기 때문에 세션 ID를 이용해서 회원과 관련된 값을 가져올 수 없다.쿠키를 이용한 로그인과 달리, 세션을 이용한 로그인은 상대적으로 더 좋아보인다.
필자도, 쿠키가 아닌 세션을 이용한 로그인 방식을 이용해서, 로그인 이후, 각 User의 권한에 따라 서비스를 다르게 이용하게 하고자 한다.
바로, HttpSession 이라는 것을 활용한다.
서블릿 → 세션 트래킹을 위해 HttpSession이라는 인터페이스를 지원
HttpSession 객체를 생성 → JSESSIONID이라는 쿠키를 생성 && 추정 불가능한 랜덤 값을 할당JSESSIONID를 부여 ⇒ JSESSIONID를 확인하여 어떤 유저의 요청인지 구분할 수 있음HttpSession의 값은 서버의 메모리(RAM)에 보관| Method | Description |
|---|---|
public HttpSession getSession() | HttpSession 객체를 반환한다. 만약 요청에 관련된 세션이 없는 경우 새 세션을 생성한다. |
public HttpSession getSession(boolean create) | getSession()에 인자로 false를 전달하는 경우 기존 세션이 없어도 새로 생성하지 않는다. true를 전달하면 새로 생성한다. |
public String getId() | 고유한 세션 ID를 반환한다. |
public void invalidate() | 세션을 무효화한다. |
위와 같이, HttpSession을 활용해 Spring은 Session 처리를 하는 것을 알 수 있다. 단점에서 보이듯, 세션은 서버에 저장되기 때문에 서버에 문제를 초래할 수 있다.
하지만, 일단 이번 포스팅에서는 세션을 활용해서 처리를 해보고자 한다.
spring.application.name=session-auth-app
# Server Port
server.port=포트번호
# MariaDB Configuration
spring.datasource.url=jdbc:mariadb://localhost:3306/session_auth_app
spring.datasource.username=데이터베이스 아이디
spring.datasource.password=데이터베이스 비밀번호
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
# Hibernate Configuration
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect
# ViewResolver Prefix & Suffix
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
package com.example.sessionauthapp.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
private String role; // 사용자 권한 (e.g., user, admin)
// Getter and Setter
.
.
.
}
package com.example.sessionauthapp.repository;
import com.example.sessionauthapp.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
}
package com.example.sessionauthapp.controller;
import com.example.sessionauthapp.model.User;
import com.example.sessionauthapp.repository.UserRepository;
import jakarta.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.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.Optional;
@Controller
// @RestController // 이 친구는 html을 찾아주는 게 아니라 JSON이나 문자열을 반환해줌
public class AuthController {
@Autowired
private UserRepository userRepository;
// wow!
@GetMapping("/hello")
public String helloPage(HttpSession session, Model model) {
String username = (String) session.getAttribute("username");
model.addAttribute("username", username);
System.out.println("someone connects this Hello Page!!!");
return "hello"; // hello-page
}
@GetMapping("/login")
public String loginPage() {
return "login";
}
@PostMapping("/login")
public String login(@RequestParam String username,
@RequestParam String password,
HttpSession session,
Model model) {
Optional<User> user = userRepository.findByUsername(username);
if (user.isPresent() && user.get().getPassword().equals(password)) {
/*
> 로그인이 정상적으로 처리되면, JSESSIONID라는 이름으로 세션을 쿠키에 저장
> JSESSIONID의 역할
- JSESSIONID는 서버가 클라이언트를 식별하기 위해 사용하는 세션 ID입니다.
- 실제 사용자 데이터(username, role)는 *** 서버 메모리에 저장 ***되며, JSESSIONID를 통해 서버가 해당 세션 데이터를 찾습니다.
- 개발자 도구에서는 JSESSIONID 값만 보이고, username이나 role과 같은 세부 데이터는 보이지 않습니다.
*/
session.setAttribute("username", username);
session.setAttribute("role", user.get().getRole());
return "redirect:/hello";
} else {
model.addAttribute("error", "ID 또는 비밀번호가 틀렸습니다.");
return "login";
}
}
@GetMapping("/logout")
public String logout(HttpSession session) {
session.invalidate();
return "redirect:/hello";
}
@GetMapping("/register")
public String registerPage() {
return "register";
}
// 회원가입 처리
@PostMapping("/register")
public String register(@RequestParam String username,
@RequestParam String password,
Model model) {
// 1. 중복 아이디 확인
Optional<User> existingUser = userRepository.findByUsername(username);
if (existingUser.isPresent()) { // 이미 존재하는 아이디
model.addAttribute("error", "이미 존재하는 아이디입니다.");
return "register"; // 에러 메시지와 함께 회원가입 페이지로 반환
}
// 2. 사용자 정보 저장
User user = new User();
user.setUsername(username);
user.setPassword(password); // 비밀번호는 추후 암호화 필요
user.setRole("user"); // 기본 권한 설정
userRepository.save(user); // DB에 저장
// 3. 회원가입 성공 시 로그인 페이지로 리다이렉트
return "redirect:/hello";
}
@GetMapping("/serviceA")
public String serviceAPage(HttpSession session, Model model) {
String username = (String) session.getAttribute("username");
String role = (String) session.getAttribute("role");
model.addAttribute("username", username);
model.addAttribute("role", role);
System.out.println("username = " + username +
" & role" + role + " > connects this serviceA Page!!!");
return "serviceA";
}
@GetMapping("/serviceB")
public String serviceBPage(HttpSession session, Model model) {
String username = (String) session.getAttribute("username");
String role = (String) session.getAttribute("role");
model.addAttribute("username", username);
model.addAttribute("role", role);
System.out.println("username = " + username +
" & role" + role + " > connects this serviceB Page!!!");
return "serviceB";
}
@GetMapping("/serviceAdmin")
public String serviceAdminPage(HttpSession session, Model model) {
String username = (String) session.getAttribute("username");
String role = (String) session.getAttribute("role");
model.addAttribute("username", username);
model.addAttribute("role", role);
System.out.println("username = " + username +
" & role" + role + " > connects this serviceAdmin Page!!!");
return "serviceAdmin";
}
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>serviceA</title>
</head>
<body>
<h1>serviceA Page!!!</h1>
<div>
<p th:text="'환영합니다, ' + ${username} + '님! role = ' + ${role}"></p>
<a href="/logout">로그아웃</a>
<a href="/serviceB">Service B</a>
<a href="/serviceAdmin">Service Admin</a>
</div>
</body>
</html>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>serviceB</title>
</head>
<body>
<h1>serviceB Page!!!</h1>
<div>
<p th:text="'환영합니다, ' + ${username} + '님! role = ' + ${role}"></p>
<a href="/logout">로그아웃</a>
<a href="/serviceA">Service A</a>
<a href="/serviceAdmin">Service Admin</a>
</div>
</body>
</html>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>serviceAdmin</title>
</head>
<body>
<h1>serviceAdmin Page!!!</h1>
<div>
<h1>Admin이 아니시면, 해당 서비스를 이용하기 어려우십니다...</h1>
<p th:text="'환영합니다, ' + ${username} + '님! role = ' + ${role}"></p>
</div>
<div th:if="${role} == admin">
<a href="/serviceA">Service A</a>
<a href="/serviceB">Service B</a>
<a href="/serviceAdmin">Service Admin</a>
</div>
<div>
<a href="/logout">로그아웃</a>
</div>
</body>
</html>
→ 로그인 전 /hello

→ /register



→ /login

→ 로그인 상태인 /hello

→ /serviceA

→ /serviceB

→ /serviceAdmin (role이 user인 경우)

→ /serviceAdmin (role이 admin인 경우)
이렇게, 세션을 활용한 인증 처리를 통해서 회원가입 및 로그인과, 로그인 된 user의 role에 따라서 접근할 수 있는 권한을 상이하게 할 수 있다는 것을 알 수 있다.
CS 공부를 할 때, 쿠키와 세션에 대한 공부를 했었는데 실제로 활용하지는 못하고 암기로만 했었다.
하지만, 이번 기회를 통해 쿠키와 세션의 차이가 더 명확히 이해가 갔고, 더 나아가 Session을 활용해 user의 role에 따른 서비스 차등을 두어보며 재밌게 공부한 것 같다.
하지만, Session의 문제도 분명히 있고, 보안적으로 password를 평문으로 저장하는 것이 매우 위험하다는 것을 인지하고 있다.
다음으로는 password를 Spring Security를 활용해 평문이 아닌 암호화하여 저장해보고자 한다.