[스프링부트] 익명 블로그 V5 - 인증 블로그 V1 - 로그인(인증) 기능

익명 블로그 V5(인증 블로그 V1) - 2
이나겸's avatar
Nov 29, 2024
[스프링부트] 익명 블로그 V5 - 인증 블로그 V1 - 로그인(인증) 기능

이론 참고

[Spring 동작 원리]

  1. Apach Tomcat은 HTTP 요청(헤더, 바디)을 받아 HttpServletRequest 객체를 생성해서 헤더와 바디를 담고, HttpServletResponse 객체를 생성해 Spring DispatcherServlet, Session으로 전달
    1. Session은 싱글턴으로 관리됨 (한 번 생성된걸로 재사용)
  1. Spring IoC 컨테이너는 싱글턴으로 관리되는 Bean과 의존성을 주입(DI)하며 애플리케이션의 객체를 관리
    1. 어노테이션이 붙은 클래스, @Bean 메소드, Session 담고 있음
    2. HttpServletRequest, HttpServletResponse 객체는 Bean으로 관리하지 않아서 의존성 주입 받을 수 없지만, 메소드 파라미터에서 사용할 수 있도록 Spring이 지원
  1. DispatcherServlet은 HTTP 요청을 처리하고 적절한 컨트롤러와 뷰를 호출
    1. 컨트롤러에서 반환된 데이터 기반으로 View 랜더링하거나 JSON 응답 생성
  1. BufferedReader/BufferedWriter는 요청 및 응답 데이터를 효율적으로 처리
    1. BufferedReader : Buffer(문자열 담음) 만들어 놓고 요청 헤더와 바디 담아서(읽어서) Tomcat 통해서 요청으로 들어감
    2. BufferedWriter : Buffer에 응답 담아서(작성해서) Tomcat 통해서 응답으로 나감
  1. 세션과 요청 객체는 특정 컨텍스트에서 주입받아 사용 가능
    1. Session : 클라이언트와 서버 간 상태 정보 유지
    2. Spring은 Session 관리를 위해 HttpSession 객체 제공
    3. Tomcat이 관리, 필요하면 Spring에서 주입 받아 사용 가능
 

[프로토콜 - 쿠키와 세션]

💡

프로토콜

  • 컴퓨터 간 통신을 가능하게 하기 위해 정해진 약속
  • 요청과 응답 간의 상태를 유지하지 않는 무상태성을 보완하기 위해 Session과 Cookie 사용

Cookie

  • 서버가 클라이언트(브라우저)에게 전달하고 이후 요청 시 이 데이터를 다시 서버에 보냄
  • 사용자 식별, 세션 유지, 개인화된 설정 저장에 사용
  • 만료 시간 지정 가능하고 만료 시간이 없으면 브라우저 종료 시 삭제

Session

  • 사용자가 서버와 상호작용 하는 동안 상태 유지
  • 서버에서 일정 시간 동안 유지되고 클라이언트가 활동하지 않으면 만료됨
  • 클라이언트 식별을 위한 고유한 세션 ID 생성
  • 세션 ID를 클라이언트에게 전달, 이를 쿠키에 저장하여 이후 요청 시 서버가 세션 식별 가능 ex) JSESSION이라는 이름의 쿠키에 세션 ID 저장

Cookie와 Session의 상관관계 - 로그인 기능 예시

  • 사용자가 로그인하면 서버가 세션을 생성하고 세션 ID 발급
    • 클라이언트(브라우저) → 서버 : Post 요청 /login
    • 세션 ID xyz123 생성, 세션에 사용자 데이터 저장
session.setAttribute("user", "john");
  • 서버는 Set-Cookie 헤더를 통해 클라이언트(브라우저)에게 세션 ID 전달
    • 서버가 세션 ID를 쿠키로 설정
Set-Cookie: JSESSIONID=xyz123; Path=/; HttpOnly
  • 클라이언트는 세션 ID가 저장된 쿠키를 포함해 서버에 요청 보냄
Cookie: JSESSIONID=xyz123
  • 서버는 세션 ID를 기반으로 해당 사용자의 세션 데이터를 가져옴
    • 세션 데이터 : user = john
 

[인증(Authentication)과 인가(Authorization)]

  • 인증
    • 세션에 값만 있으면 됨
    • 로그인이 되면 세션에 로그인 정보 담김
  • 인가 (권한 부여)
    • 세션에 들어있는 값과 정보 다 필요
    • 로그인(인증) 했다고 다 권한이 생기는 건 아님
notion image
 

Spring 인증 블로그 로그인 구현

[UserController 클래스]

💡
로그인은 민감한 정보를 담기 때문에 예외적으로 post 요청
💡
컨트롤러는 항상 로그인에 정상적으로 성공했을 때만 객체를 return 받음
⇒ 전달받은 데이터의 유저가 없거나, 아이디 또는 비밀번호가 틀렸을 때
UserService 클래스 또는 UserRepository 클래스에서 예외를 throw 하도록 작성하였기 때문
  • @PostMapping("/login")
    • Post 요청을 /login 경로로 매핑
    • 로그인 요청 처리하는 엔드 포인트
  • LoginDTO 객체를 통해(매개변수) 사용자가 로그인할 때 입력한 데이터를 받아 처리
  • userService.로그인(loginDTO) 호출
    • 로그인 비즈니스 로직 처리하고 인증된 사용자 객체 반환받음
  • session.setAttribute("sessionUser", sessionUser) 호출
    • 로그인된 사용자 정보 세션 저장
    • 세션 유지되는 동안 사용자는 인증된 상태 유지 가능
  • return "redirect:/"
    • 로그인 성공하면 리다이렉트하여 메인 페이지 이동
@RequiredArgsConstructor @Controller public class UserController { private final UserService userService; // IOC 컨테이너에서 세션 꺼내옴 (세션은 싱글턴이라서 하나만 있으니까 IOC가 관리해줌) private final HttpSession session; @PostMapping("/login") public String login(UserRequest.LoginDTO loginDTO) { User sessionUser = userService.로그인(loginDTO); session.setAttribute("sessionUser", sessionUser); return "redirect:/"; // 로그인 성공하면 메인 페이지 리다이렉트 } }
 

[UserService 클래스]

  • LoginDTO 객체를 통해(매개변수) 사용자가 로그인할 때 입력한 입력 데이터를 받아 처리
  • userRepository.findByUsername(loginDTO.getUsername())
    • 데이터베이스에서 username 기준으로 사용자 정보 조회
  • if (!userPS.getPassword().equals(loginDTO.getPassword()))
    • 비밀번호 검증 - 조회된 사용자의 비밀번호와 입력된 비밀번호가 일치하는지 확인
    • 비밀번호가 일치하지 않으면 RuntimeException 발생시켜서 로그인 실패 처리
    • 비밀번호가 일치하면 조회된 사용자 객체 반환
@RequiredArgsConstructor @Service public class UserService implements UserDetailsService { private final UserRepository userRepository; public User 로그인(UserRequest.LoginDTO loginDTO) { User userPS = userRepository.findByUsername(loginDTO.getUsername()); if (!userPS.getPassword().equals(loginDTO.getPassword())) { throw new RuntimeException("아이디 혹은 패스워드가 일치하지 않습니다."); // 실제로는 로그를 남겨서 서버에 패스워드가 실패 카운팅해야 함 // 일단은 alert 보이도록 해놓음 } return userPS; } // post 요청 // /login 일때 호출됨 // key 값 -> username, password // Content-Type -> x-www-form-urlencoded @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username); return user; } }
 

[UserRepository 클래스]

💡

JPQL(JPA Query Language)

  • JPA의 일부로 정의된 플랫폼 독립적인 객체 지향 쿼리 언어
  • 데이터베이스에 저장된 엔티티를 데이터베이스 종속적인 SQL 구문이 아니라 추상적이고 객체 지향적인 방식으로 조회
💡

Optional로 null 처리하지 않고 try-catch로 예외 처리한 이유

  • q.getSingleResult() 메서드에서 쿼리문으로 조회했을 때 만약 유저가 없다면 null이 반환되는 것이 아니라, NoResultException 예외를 발생
  • 해당 예외가 발생하였을 때 개발자가 원하는 방향(throw RuntimeException)으로 컨트롤하기 위해 try-catch를 사용
  • catch로 가지 않을 시 반드시 객체가 반환 될 것이기 때문에 굳이 Optional 클래스로 감싸지 않고 바로 반환
  • em.createQuery
    • JPQL을 사용하여 사용자 이름으로 User 엔티티를 조회하는 쿼리를 생성
  • select u from User u where u.username = :username
    • 추상적이고 객체 지향적인 방식으로 조회
    • JPQL의 :은 SQL의 ?역할
  • q.setParameter("username", username)
    • 쿼리 파라미터로 사용자 이름 설정
  • q.getSingleResult()
    • 쿼리를 실행하여 단일 결과 반환
    • 결과가 없거나 여러 결과가 있을 경우 예외 발생 할 수 있음
  • 예외 처리
    • RuntimeException이 발생하면 메세지로 새 예외 던지기 ⇒ 사용자에게 적절한 오류 메세지 전달을 위함
@RequiredArgsConstructor @Repository public class UserRepository { private final EntityManager em; public User findByUsername(String username) { // createQuery 사용 / jpql => :이 ?역할 Query q = em.createQuery("select u from User u where u.username = :username", User.class); q.setParameter("username", username); try { // 테스트를 해보니 optinal을 리턴할 필요가 없어져서 리팩토링 // 예외 처리를 try-catch로 하기로 바꿈 return (User) q.getSingleResult(); } catch (RuntimeException e) { throw new RuntimeException("아이디 혹은 패스워드가 일치하지 않습니다."); } }
 

웹 실행해서 로그인 후 Set-Cookie 값 확인

  • 웹 실행해서 로그인하면 관리자 모드(F12)의 Headers에서 확인 가능
    • notion image
 

Postman을 활용한 로그인 테스트

  • ‘username = ssar, password = 1234’ 더미데이터가 h2DB에 저장되어있는 상태
  • 로그인 요청
    • http://localhost:8080/login 경로로 HTTP POST 요청
  • JSESSIONID 생성
    • 서버는 사용자의 인증 정보 확인 후 로그인에 성공하면 고유한 세션ID(JSESSIONID) 생성
    • 생성된 세션ID는 서버 측 세션 스토리지에 저장됨
  • 쿠키를 통해 세션ID 전달
    • 서버는 응답으로 JSESSIONID를 쿠키에 담아 클라이언트(브라우저)로 전송
    • 클라이언트는 쿠키 저장하고 이후 모든 요청에 대해 해당 쿠키를 자동으로 서버에 전달
  • 세션 확인
    • 클라이언트가 이후 다른 요청을 보낼 때마다, 브라우저는 JSESSIONID 쿠키를 함께 전송
    • 서버는 요청에 포함된 JSESSIONID 쿠키 값을 확인하여 해당 세션 ID가 유효한지 확인
    • 유효한 세션 ID가 있으면, 서버는 사용자가 로그인된 상태로 간주하고 요청을 처리
      • notion image
  • POST 요청 후 다른 경로 요청했을 때
    • POST 요청에서 전달 받은 JSESSIONID 의 Value 값을 쿠키로 함께 전송하는 것을 확인
      • notion image
 
Share article

Nakyeom's Study