로그인과 로그아웃

suhan cho·2022년 7월 29일
0

로그인 구현하기

  • 비밀번호로 로그인 하려면 복잡한 단계를 거치지만 시큐리티를 사용하면 쉽게 진행 할 수 있다.

로그인 URL등록

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        http.authorizeRequests().antMatchers("/**").permitAll()
        .and()
                .headers()
                .addHeaderWriter(new XFrameOptionsHeaderWriter(
                        XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN))
        .and()
                .formLogin()
                .loginPage("/user/login")
                .defaultSuccessUrl("/");
        
        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}
  • and().formLogin().loginPage("/user/login").defaultSuccessUrl("/")은 스프링 시큐리티의 로그인 설정을 담당하는 부분이다
  • 로그인 페이지의 URL은 /user/login이고 로그인 성공시에 이동하는 디폴트 페이지는 루트 URL(/)의미

UserController

  • 시큐리티에서 로그인을 /user/login으로 설정했으므로 User컨트롤러에 해당 매핑을 추가
 @GetMapping("/login")
    public String login() {
        return "login_form";
    }
  • login_form.html 템플릿을 렌더링하는 GET방식의 login메서드를 추가
  • 실제 로그인을 진행하는 @PostMapping 방식의 메서드는 스프링 시큐리티가 대신 처리하므로 직접 구현할 필요 없다.

login_form 템플릿

<html layout:decorate="~{layout}">
<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>
  • 시큐리티의 로그인이 실패할 경우에는 로그인 페이지로 다시 리다이렉트
  • error가 함께 전달되며 동시에 확인해달라는 오류 메시지 전달

시큐리티 로그인 수행하는 방법

  • 시큐리티 설정 파일에 직접 아이디, 비밀번호를 등록하여 인증을 처리하는 메모리 방식
  • 회원가입을 통해 회원 정보를 데이터베이스에 저장했으므로 데이터베이스에서 회원 정보를 조회하는 방법(여기서는 이 방법)

UserSecurityService

데이터베이스에서 사용자를 조회하는 서비스를 만들고 그 서비스를 스프링 시큐리티에 등록하는 방법

public interface UserRepository extends JpaRepository<SiteUser, Long> {
    Optional<SiteUser> findByUsername(String username);
}

UserRole작성

@Getter
public enum UserRole {
    ADMIN("ROLE_ADMIN"),
    USER("ROLE_USER");

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

    private String value;
}
  • 열거 자료형으로 작성 ADMIN은 "ROLE_ADMIN", USER는 "ROLE_USER"값을 가지도록 했다.
  • 상수 자료형이므로 @Setter없이 @Getter만 사용
  • 추후 ADMIN권한을 지닌 사람에게 특정 기능을 제어할 수 있도록 수정해볼 것

UserSercurityService


@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);
    }
}
  • 시큐리티에 등록하여 사용하려면 시큐리티가 제공하는 UserDetailsService 인터페이스를 구현해야 한다.
  • UserDetailsService는 loadUserByUsername메서드를 구현하도록 강제하는 인터페이스이다.
  • loadUserByUsername 메서드는 사용자명으로 비밀번호를 조회하여 리턴하는 메서드

loadUserByUsername 메서드

  • UserDetailsService 인터페이스를 사용해서 구현
  • SiteUser객체를 조회하고 만약 사용자명에 해당하는 데이터가 없을 경우에는 UsernameNotFoundException 오류를 내게 함
  • 사용자명이 admin인 경우 ADMIN권한을 부여하고 그 이외의 경우에는 USER권한을 부여
  • 사용자명, 비밀번호, 권한을 입력으로 시큐리티의 User객체를 생성 리턴
  • 리턴된 User객체의 비밀번호가 화면으로부터 입력 받은 비밀번호와 일치하는지를 검사하는 로직 내부적으로 지님

SecurityConfig 수정

@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    private final UserSecurityService userSecurityService;

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration)throws Exception{
        return authenticationConfiguration.getAuthenticationManager();
    }
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        http.authorizeRequests().antMatchers("/**").permitAll()
        .and()
                .headers()
                .addHeaderWriter(new XFrameOptionsHeaderWriter(
                        XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN))
        .and()
                .formLogin()
                .loginPage("/user/login")
                .defaultSuccessUrl("/");

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    
}
  • AuthenticationManger빈을 생성 스프링 시큐리티의 인증을 담당한다.
  • 빈 생성시 스프링의 내부 동작으로 인해 위에서 작성한 UserSecurityService와 PasswordEcoder가 자동으로 설정된다
<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>


로그인 성공!!!!!!!!

로그아웃 기능

  • 타임리프의 sec:authorize 속성 통해 알 수 있다.
  • sec:authorize= "isAnonymous()"
    로그인 되지 않은 경우에만 해당 엘리먼트가 표시되게 한다.
  • sec:authorize= "isAuthenticated()"
    로그인 된 경우에만 해당 엘리먼트가 표시되게 한다.
<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" 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>
  • 로그인 로그아웃으로 변하게 설정해놓음
  • 로그아웃 기능을 구현하지 않아 눌렀을때 오류 발생

SecurityConfig 추가

.and()
                .logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/user/logout"))
                .logoutSuccessUrl("/")
                .invalidateHttpSession(true)
  • 로그아웃 Url /user/logout설정
  • 로그아웃이 성공하면 루트 페이지로 이동
  • 로그아웃이 생성된 사용자 세션 삭제
    출처: https://wikidocs.net/162255
profile
안녕하세요

0개의 댓글