로그인과 로그아웃

prana·2023년 5월 27일

SpringBoot

목록 보기
20/22
post-thumbnail

로그인 구현하기

회원가입 단계에서 SITE_USER 테이블에 회원 정보를 저장했다.

SITE_USER 테이블에 저장된 사용자명 (사용자 ID)과 비밀번호로 로그인을 하려면 복잡한 단계를 거쳐야 한다.

하지만 스프링시큐리티를 사용하면 보다 쉽게 진행할 수 있다.

스프링 시큐리티는 방대한 프로그램이다.

방대한 프레임워크로서 내부적으로 어떻게 동작하는지 알기 위해서는 자세히 공부해야 한다.
(책 1권 분량으로 나올만큼 방대한 프레임워크이다. 책은 이미 많이 출판되었다고 한다.)
개략적인 개념 설명을 추가하였다고 한다. 위키독스에서!


SecurityConfig.java

//SecurityConfig.java
package com.mysite.sbb;

(생략)

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests().requestMatchers(
                        new AntPathRequestMatcher("/**")).permitAll()
                .and()
                .csrf().ignoringRequestMatchers(
                        new AntPathRequestMatcher("/h2-console/**"))
                .and()
                .headers()
                .addHeaderWriter(new XFrameOptionsHeaderWriter(
                        XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN))
               👉 .and()
                .formLogin().loginPage("/user/login") ---①
                .defaultSuccessUrl("/"); ---②

        return http.build();
    }

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

① - 로그인 페이지의 URL은 /user/login이고,
② - 로그인 성공 시 이동하는 디폴트 페이지는 루트 URL(/)임을 나타낸다.

@GetMapping("/login")

//UserController.java에 추가한다. 

 @GetMapping("/login")
    public String login(){
        return "login_form";
    }

실제 로그인을 진행하는 @PostMapping 방식의 메서드는
스프링 시큐리티가 대신 처리하므로 직접 구현할 필요가 없다.


<!doctype html>
<html lang="en" xmlns:th="">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<nav th:fragment="navbarFragment" class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
    <div class="container-fluid">
        <a class="navbar-brand" href="/">SBB</a>
        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
                aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarSupportedContent">
            <ul class="navbar-nav me-auto mb-2 mb-lg-0">
                <li class="nav-item">
                    <a class="nav-link" 👉th:href="@{/user/login}">로그인</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" th:href="@{/user/signup}">회원가입</a>
                </li>
            </ul>
        </div>
    </div>
</nav>
</body>
</html>


login_form.html

<html layout:decorate="~{layout}" xmlns:layout="http://www.w3.org/1999/xhtml" xmlns:th="http://www.w3.org/1999/xhtml">
<div layout:fragment="content" class="container my-3">
    <form th:action="@{/user/login}" method="post">
        <div th:if="${param.error}">
            <div class="alert alert-danger">
                사용자ID 또는 비밀번호를 확인해 주세요.
            </div>
        </div>
        <div class="mb-3">
            <label for="username" class="form-label">사용자ID</label>
            <input type="text" name="username" id="username" class="form-control">
        </div>
        <div class="mb-3">
            <label for="password" class="form-label">비밀번호</label>
            <input type="password" name="password" id="password" class="form-control">
        </div>
        <button type="submit" class="btn btn-primary">로그인</button>
    </form>
</div>
</html>


일부러 id와 비밀번호를 다르게 했다. 다시 로그인 페이지로 리다이렉트 된다.
error가 함께 전달된다.


그러나 아직 로그인을 수행할 수는 없다.
스프링 시큐리티에 무엇을 기준으로 로그인해야하는지 아직 설정하지 않았기 때문이다.
스프링 시큐리티를 통해 로그인을 수행하는 방법은 여러가지가 있다.

예를 들면, 시큐리티 설정 파일에 직접 아이디, 비밀번호를 등록하여 인증을 처리하는 메모리 방식이 있다. 그러나 우리는 db에 저장했으므로 db에서 회원정보를 조회하는 방법을 사용해야 할 것이다.

사용자를 조회하는 서비스

서비스를 만들고, 그 서비스를 시큐리티에 등록하는 방법에 대하여 알아보자.
그 전에 Repository, UserRole 등을 먼저 준비하도록 한다.

UserRole.java

스프링 시큐리티는 인증 뿐만 아니라, 권한도 관리한다.
따라서 인증 후에 사용자에게 부여할 권한이 필요하다. 다음과 같이 ADMIN,USER 2개의 권한을 갖는 UserRole 을 신규로 작성한다.

package com.mysite.sbb.user;
import lombok.Getter;
@Getter
// 상수 자료형이므로 @Setter 는 없다.
public enum UserRole {
    ADMIN("ROLE_ADMIN"),
    USER("ROLE_USER");

    UserRole(String value){
        this.value = value;
    }

    private String value;
}

이 책에서 구현할 SBB는 권한으로 특정 기능을 제어하지 않는다. 하지만 SBB 서비스를 완성하고 다른 사람이 작성한 질문이나 답변을 ADMIN 권한을 지닌 사용자는 수정이 가능하도록 만들어도 좋을 것이다.
출처 : https://wikidocs.net/162255

UserSecurityService.java

package com.mysite.sbb.user;

import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

@RequiredArgsConstructor
@Service
public class UserSecurityService implements UserDetailsService {
    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
        Optional<SiteUser> _siteUser = this.userRepository.findByUsername(username);---ⓐ
        if(_siteUser.isEmpty()){---ⓑ
            throw new UsernameNotFoundException("사용자를 찾을 수 없습니다.");---ⓒ
        }

        SiteUser siteUser = _siteUser.get();
        List<GrantedAuthority> authorities = new ArrayList<>();--⭐

        if("admin".equals(username)){ ---ⓓ
            authorities.add(new SimpleGrantedAuthority(UserRole.ADMIN.getValue()));---ⓔ
        }else{
            authorities.add(new SimpleGrantedAuthority(UserRole.USER.getValue()));
        }

        return new User(siteUser.getUsername(), siteUser.getPassword(), authorities);
    }

}

스프링 시큐리티에 등록하여 사용하게 될 UserSecurityService 는 스프링 시큐리티가 제공하는 UserDetailsService 인터페이스를 구현(implements)해야 한다.

UserDetailsService는 loadUserByUsername 메서드를 구현하도록 강제하는 인터페이스이다.
loadUserByUsername 메서드는 사용자명으로 비밀번호를 조회하여 리턴하는 메서드이다.

UserDetailsService는 스프링 시큐리티 로그인 처리의 핵심 부분이다.

ⓐ- loadUserByUsername 메서드는 사용자명으로 SiteUser 객체를 조회하고,

ⓑ - 만약 사용자명에 해당하는 데이터가 없을 경우

ⓒ - UsernameNotFoundException 오류를 나도록 했다.

ⓓ - 사용자명이 admin인 경우,
ⓔ - USER 권한을 부여한다.
⭐ - 하나의 유저에 여러 개의 권한을 주기 위해..!

그리고 사용자명, 비밀번호, 권한을 입력으로 스프링 시큐리티의 User객체를 생성하여 리턴했다.

스프링 시큐리티는 loadUserByUsername 메서드에 의해 리턴된 User 객체의 비밀번호가 화면으로부터 입력받은 비밀번호와 일치하는지를 검사하는 로직을 내부적으로 가지고 있다!

SecurityConfig.java

 @Bean
    AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception{
        return authenticationConfiguration.getAuthenticationManager();
    }

AuthenticationManager 빈을 생성했다.
이는 스프링 시큐리티의 인증을 담당한다.
AuthenticationManager 빈 생성 시 스프링의 내부 동작으로 인해 위에서 작성한 UserSecurityService와 PasswordEncoder가 자동으로 설정된다.


잘 접속됐다!!!!


로그인/로그아웃 링크

로그인/로그아웃 로그인 상태에 따라 반대로 나와야한다.

사용자의 로그인 여부는 타임리프의 sec:authorize 속성을 통해 알 수 있다.

// navbar.html
<!doctype html>
<html lang="en" xmlns:th="" xmlns:sec="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<nav th:fragment="navbarFragment" class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
    <div class="container-fluid">
        <a class="navbar-brand" href="/">SBB</a>
        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
                aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarSupportedContent">
            <ul class="navbar-nav me-auto mb-2 mb-lg-0">
                <li class="nav-item">
<!--                    <a class="nav-link" th:href="@{/user/login}">로그인</a>-->
                    <a class="nav-link" sec:authorize="isAnonymous()" th:href="@{/user/login}">로그인</a>
                    <a class="nav-link" sec:authorize="isAuthenticated()" th:href="@{/user/logout}">로그아웃</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" th:href="@{/user/signup}">회원가입</a>
                </li>
            </ul>
        </div>
    </div>
</nav>
</body>
</html>

로그인을 안 한 상태라면, sec:authorize="isAnonymous()"가 참이 되어 로그인 링크가 표시되고,
로그인을 한 상태라면 sec:authorize="isAuthenticated()"가 참이 되어 로그아웃 링크가 표시된다.

로그아웃을 누르면..

/user/logout 으로 링크를 지정했다. 아직 구현 안했다.

SecurityConfig.java

package com.mysite.sbb;

(생략)

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests().requestMatchers(
                        new AntPathRequestMatcher("/**")).permitAll()
                .and()
                .csrf().ignoringRequestMatchers(
                        new AntPathRequestMatcher("/h2-console/**"))
                .and()
                .headers()
                .addHeaderWriter(new XFrameOptionsHeaderWriter(
                        XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN))
                .and()
                .formLogin()
                .loginPage("/user/login")
                .defaultSuccessUrl("/")
                .and()
                👉.logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/user/logout"))
                .logoutSuccessUrl("/")
                .invalidateHttpSession(true);
                        
                

        return http.build();
    }

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception{
        return authenticationConfiguration.getAuthenticationManager();
    }
}

로그아웃을 위한 설정을 추가했다.
로그아웃 URL을 /user/logout으로 설정, 로그아웃이 성공하면 루트(/) 페이지로 이동하도록 했다.
그리고 로그아웃 시 생성된 사용자 세션도 삭제하도록 처리했다.

로그아웃을 누르면 다시 로그인 링크가 나타나는지 확인해보자


로그아웃 버튼을 누른 후

0개의 댓글