로그인, 로그아웃 기능 구현

송영재·2022년 10월 23일

Spring

목록 보기
10/45
  • 18) 로그인, 로그아웃 처리 과정 이해

    • 스프링 시큐리티 사용 전

    • 스프링 시큐리티 사용 후

      • Client 의 요청은 모두 Spring Security 를 거침
      • Spring Security 역할
        1. 인증/인가
          1. 성공 시: Controller 로 Client 요청 전달
            1. Client 요청 + 사용자 정보 (UserDetails)
          2. 실패 시: Controller 로 Client 요청 전달되지 않음
            1. Client 에게 Error Response 보냄
    • 로그인 처리 과정

      • 상세 처리 과정 설명
        1. Client
          1. 로그인 시도

          2. 로그인 시도할 username, password 정보를 HTTP body 로 전달 (POST 요청)

          3. 로그인 시도 URL 은 WebSecurityConfig 클래스에서 변경 가능

            • 아래와 같이 설정 시 "POST /user/login" 로 설정됨
            @Override
            protected void configure(HttpSecurity http) throws Exception {
                http.authorizeRequests()
                        .anyRequest().authenticated()
                        .and()
                            // [로그인 기능]
                            .formLogin()
                            // 로그인 처리 (POST /user/login)
                            **.loginProcessingUrl("/user/login")**
                            .permitAll();
            }
        2. 인증 관리자 (Authentication Manager)
          1. UserDetailsService 에게 username 을 전달하고 회원상세 정보를 요청
        3. UserDetailsService
          1. 회원 DB 에서 회원 조회

            User user = userRepository.findByUsername(username)
                    .orElseThrow(() -> new UsernameNotFoundException("Can't find " + username));
            • 회원 정보가 존재하지 않을 시 → Error 발생
          2. 조회된 회원 정보(user) 를 UserDetails 로 변환

            UserDetails userDetails = new UserDetailsImpl(user)
          3. UserDetails 를 "인증 관리자"에게 전달

        4. "인증 관리자" 가 인증 처리
          1. 아래 2 개의 username, password 일치 여부 확인
            1. Client 가 로그인 시도한 username, password
            2. UserDetailsService 가 전달해준 UserDetails 의 username, password
          2. password 비교 시
            1. Client 가 보낸 password 는 평문이고, UserDetails 의 password 는 암호문
            2. Client 가 보낸 password 를 암호화해서 비교
          3. 인증 성공 시 → 세션에 로그인 정보 저장
          4. 인증 실패 시 → Error 발생
    • 로그아웃 처리

      • "GET /user/logout" 요청 시 로그아웃
      • 서버 세션에 저장되어 있는 로그인 사용자 정보 삭제
  • 19) 로그인, 로그아웃 구현

    1. 로그인, 로그아웃 처리 URL 설정
      • [코드스니펫] security > WebSecurityConfig
        import org.springframework.context.annotation.Bean;
        import org.springframework.context.annotation.Configuration;
        import org.springframework.security.config.annotation.web.builders.HttpSecurity;
        import org.springframework.security.config.annotation.web.builders.WebSecurity;
        import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
        import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
        import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
        
        @Configuration
        @EnableWebSecurity // 스프링 Security 지원을 가능하게 함
        public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        
            @Bean
            public BCryptPasswordEncoder encodePassword() {
                return new BCryptPasswordEncoder();
            }
        
            @Override
            public void configure(WebSecurity web) {
                // h2-console 사용에 대한 허용 (CSRF, FrameOptions 무시)
                web
                        .ignoring()
                        .antMatchers("/h2-console/**");
            }
        
            @Override
            protected void configure(HttpSecurity http) throws Exception {
                // 회원 관리 처리 API (POST /user/**) 에 대해 CSRF 무시
                http.csrf()
                        .ignoringAntMatchers("/user/**");
        
                http.authorizeRequests()
                        // image 폴더를 login 없이 허용
                        .antMatchers("/images/**").permitAll()
                        // css 폴더를 login 없이 허용
                        .antMatchers("/css/**").permitAll()
                        // 회원 관리 처리 API 전부를 login 없이 허용
                        .antMatchers("/user/**").permitAll()
                        // 그 외 어떤 요청이든 '인증'
                        .anyRequest().authenticated()
                        .and()
                            // [로그인 기능]
                            .formLogin()
                            // 로그인 View 제공 (GET /user/login)
                            .loginPage("/user/login")
                            // 로그인 처리 (POST /user/login)
                            .**loginProcessingUrl("/user/login")**
                            // 로그인 처리 후 성공 시 URL
                            .defaultSuccessUrl("/")
                            // 로그인 처리 후 실패 시 URL
                            .failureUrl("/user/login?error")
                            .permitAll()
                        .and()
                            // [로그아웃 기능]
                            .logout()
                            // 로그아웃 처리 URL
                            **.logoutUrl("/user/logout")**
                            .permitAll();
            }
        }
    2. DB 의 회원 정보 조회 → 스프링 시큐리티의 "인증 관리자" 에게 ****전달
      1. UserDetailsService 구현
        1. UserDetailsService 인터페이스 → UserDetailsServiceImpl 클래스
        • [코드스니펫] security > UserDetailsServiceImpl
          import com.sparta.springcore.model.User;
          import com.sparta.springcore.repository.UserRepository;
          import org.springframework.beans.factory.annotation.Autowired;
          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;
          
          @Service
          public class UserDetailsServiceImpl implements UserDetailsService {
          
              private final UserRepository userRepository;
          
              @Autowired
              public UserDetailsServiceImpl(UserRepository userRepository) {
                  this.userRepository = userRepository;
              }
          
              public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                  User user = userRepository.findByUsername(username)
                          .orElseThrow(() -> new UsernameNotFoundException("Can't find " + username));
          
                  return new UserDetailsImpl(user);
              }
          }
      2. UserDetails 구현
        1. UserDetails 인터페이스 → UserDetailsImpl 클래스
        • [코드스니펫] security > UserDetailsImpl
          import com.sparta.springcore.model.User;
          import org.springframework.security.core.GrantedAuthority;
          import org.springframework.security.core.userdetails.UserDetails;
          
          import java.util.Collection;
          import java.util.Collections;
          
          public class UserDetailsImpl implements UserDetails {
          
              private final User user;
          
              public UserDetailsImpl(User user) {
                  this.user = user;
              }
          
              public User getUser() {
                  return user;
              }
          
              @Override
              public String getPassword() {
                  return user.getPassword();
              }
          
              @Override
              public String getUsername() {
                  return user.getUsername();
              }
          
              @Override
              public boolean isAccountNonExpired() {
                  return true;
              }
          
              @Override
              public boolean isAccountNonLocked() {
                  return true;
              }
          
              @Override
              public boolean isCredentialsNonExpired() {
                  return true;
              }
          
              @Override
              public boolean isEnabled() {
                  return true;
              }
          
              @Override
              public Collection<? extends GrantedAuthority> getAuthorities() {
                  return Collections.emptyList();
              }
          }
  • 20) 회원 로그인 / 로그아웃 UI 처리

    • 로그인 성공 시 페이지
    1. "로그아웃" 버튼 클릭 시

      • "GET /user/logout" 로 API 설계 했는데, "POST /user/logout" 으로 처리 필요
        • 이유: CSRF protection 이 기본적으로 enable 되어 있기 때문
        • CSRF protection 을 disable 하면 GET /user/logout 으로도 사용 가능
    2. 로그인 성공한 회원의 username 표시

      • UI 에서 username 대신 nickname (별칭), name (회원 본명) 을 표시해 주기도 함
      • Controller 에서 "로그인된 회원 정보" 사용 가능
        1. Spring Security 가 "로그인된 회원 정보"를 Controller 에게 전달해 줌

        2. Controller 에서 "로그인된 회원 정보 (UserDetailsImpl)" 사용하는 방법

          @Controller
          public class TestController {
              @GetMapping("/")
              public String test(**@AuthenticationPrincipal UserDetailsImpl userDetails**) {
          		}
          }
      • 로그인한 사용자의 username 적용 구현
        1. Controller 에서 model 에 'username' 전달
          • [코드스니펫] controller > HomeController
            import com.sparta.springcore.security.UserDetailsImpl;
            import org.springframework.security.core.annotation.AuthenticationPrincipal;
            import org.springframework.stereotype.Controller;
            import org.springframework.ui.Model;
            import org.springframework.web.bind.annotation.GetMapping;
            
            @Controller
            public class HomeController {
                @GetMapping("/")
                public String home(Model model, @AuthenticationPrincipal UserDetailsImpl userDetails) {
                    model.addAttribute("username", userDetails.getUsername());
                    return "index";
                }
            }
        2. 타임리프 적용 필요
          • index.html 파일을 static 폴더 → templates 폴더로 이동
          • [코드스니펫] resources > templates > index.html
            <!doctype html>
            <html lang="en">
            <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">
                <meta property="og:title" content="00만의 셀렉샵">
                <meta property="og:description" content="관심상품을 선택하고, 최저가 알림을 확인해보세요!">
                <meta property="og:image" content="images/og_selectshop.png">
                <link href="https://fonts.googleapis.com/css2?family=family=Nanum+Gothic&display=swap" rel="stylesheet">
                <link rel="stylesheet" href="css/style.css">
                <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
                <script src="basic.js"></script>
                <title>나만의 셀렉샵</title>
            </head>
            <body>
            <div class="header" style="position:relative;">
                <div id="header-title-login-user">
                    <span th:text="${username}"></span> 님의
                </div>
                <div id="header-title-select-shop">
                    Select Shop
                </div>
            
                <form id="my_form" method="post" action="/user/logout">
                    <a id="logout-text" href="javascript:{}" onclick="document.getElementById('my_form').submit();">로그아웃</a>
                </form>
            </div>
            <div class="nav">
                <div class="nav-see active">
                    모아보기
                </div>
                <div class="nav-search">
                    탐색하기
                </div>
            </div>
            <div id="see-area">
                <div id="product-container">
            
                </div>
            </div>
            <div id="search-area">
                <div>
                    <input type="text" id="query">
                    <!--    <img src="images/icon-search.png" alt="">-->
                </div>
                <div id="search-result-box">
            
                </div>
                <div id="container" class="popup-container">
                    <div class="popup">
                        <button id="close" class="close">
                            X
                        </button>
                        <h1>⏰최저가 설정하기</h1>
                        <p>최저가를 설정해두면 선택하신 상품의 최저가가 떴을 때<br/> 표시해드려요!</p>
                        <div>
                            <input type="text" id="myprice" placeholder="200,000"></div>
                        <button class="cta" onclick="setMyprice()">설정하기</button>
                    </div>
                </div>
            </div>
            </body>
            </html>

0개의 댓글