[SpringBoot] 스프링 시큐리티+타임리프로 회원가입/로그인/로그아웃 구현하기 - (3)

최가희·2022년 7월 31일
0

SpringBoot

목록 보기
9/13
post-thumbnail

로그인 구현

로그인 URL 등록

스프링 시큐리티에 로그인 URL을 등록한다.
SecurityConfig.java

...

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/**").permitAll()
            .and()
                .csrf().ignoringAntMatchers("/h2-console/**")
            .and()
                .headers()
                .addHeaderWriter(new XFrameOptionsHeaderWriter(
                        XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN))
            /* 추가된 부분 */
            .and()
                .formLogin()
                .loginPage("/user/login")
                .defaultSuccessUrl("/")
            /* 추가된 부분 end */
        ;
        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
  • loginPage(url): 로그인 페이지의 URL
  • defaultSuccessUrl(url): 로그인 성공 시 이동하는 디폴트 페이지



UserController

...
public class UserController {

    ...

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



로그인 폼

login_form.html

<html layout:decorate="~{layout}" xmlns:th="http://www.thymeleaf.org" xmlns:layout="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>

시큐리티의 로그인이 실패할 경우에는 로그인 페이지로 다시 리다이렉트 되는데, 이때 페이지 파라미터로 error가 함께 전달된다. 따라서 로그인 페이지의 파라미터로 error가 전달될 경우 "사용자 ID 또는 비밀번호를 확인해 주세요." 라는 오류메시지가 출력된다.


UserRepository

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



UserRole

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

import lombok.Getter;

// 상수 자료형이므로 @Setter는 사용 x
@Getter
// 열거 자료형(enum)
public enum UserRole {
    ADMIN("ROLE_ADMIN"), USER("ROLE_USER");
    
    UserRole(String value){
        this.value = value;
    }

    private String value;
}



UserSecurityService 작성

스프링 시큐리티 설정에 등록할 UserSecurityService 작성

import com.example.sbb.domain.SiteUser;
import com.example.sbb.domain.UserRole;
import com.example.sbb.repository.UserRepository;
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
// 스프링 시큐리티가 제공하는 UserDetailsService 인터페이스 구현
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)){
            // 사용자명이 'admin'일 경우에만 ADMIN 권한 부여
            authorities.add(new SimpleGrantedAuthority(UserRole.ADMIN.getValue()));
        }else{
            authorities.add(new SimpleGrantedAuthority(UserRole.USER.getValue()));
        }
        return new User(siteUser.getUsername(), siteUser.getPassword(), authorities);
    }
}
  • 스프링 시큐리티에 등록하여 사용할 UserSecurityService는 스프링 시큐리티가 제공하는 UserDetailService 인터페이스를 구현한다.
  • UserDetailService는 loadUserByUsername 메서드를 오버라이딩 하도록 되어있다.
  • 사용자명이 "admin"인 경우에는 ADMIN 권한을 부여하고, 그 외에는 USER 권한을 부여한다.
  • 스프링 시큐리티는 loadUserByUsername 메서드에 의해 리턴된 User 객체의 비밀번호가 화면으로부터 입력 받은 비밀번호와 일치하는지를 검사하는 로직을 내부적으로 가지고 있다.



SecurityConfig

스프링 시큐리티에 UserSecurityService를 등록한다.

@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final UserSecurityService userSecurityService;

    ...

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
}
  • AuthenticationManager는 스프링 시큐리티의 인증을 담당한다.
  • AuthenticationManager 빈 생성 시 스프링의 내부 동작으로 인해 위에서 작성한 UserSecurityService와 PasswordEncoder가 자동으로 설정된다.



내비게이션 바에 로그인 페이지에 진입할 수 있는 로그인 링크 추가

<nav th:fragment="navbarFragment" class="navbar navbar-expand-lg
    navbar-light bg-light border-bottom" xmlns:th="http://www.thymeleaf.org">
    <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>
                  	<!--수정된 부분 end-->
                </li>
                <li class="nav-item">
                    <a class="nav-link" th:href="@{/user/signup}">회원가입</a>
                </li>
                
            </ul>
        </div>
    </div>
</nav>



로그인 화면 확인


만약 DB에 없는 username 또는 잘못된 password를 입력하면 다음과 같이 오류 메시지가 출력된다.

username과 password를 제대로 입력하면 로그인이 정상 수행되고 메인 화면으로 이동한다.



로그인 / 로그아웃 링크 수정

로그인 한 후 로그인 링크가 로그아웃으로 바뀌도록 수정해야 한다.

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

  • sec:authorize="isAnonymous()": 로그인되지 않은 경우에만 해당 엘리먼트가 표시된다.
  • sec:authorize="isAuthenticated()": 로그인된 경우에만 해당 엘리먼트가 표시된다.

navbar.html

<nav th:fragment="navbarFragment" class="navbar navbar-expand-lg
    navbar-light bg-light border-bottom" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.w3.org/1999/xhtml">
    <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>
                  	<!--추가된 부분 end-->
                </li>
                <li class="nav-item">
                    <a class="nav-link" th:href="@{/user/signup}">회원가입</a>
                </li>
            </ul>
        </div>
    </div>
</nav>

  • 로그인 전 화면
  • 로그인 후 화면



로그아웃 구현

SecurityConfig

...

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        // 모든 인증되지 않은 요청을 허락
        http.authorizeRequests().antMatchers("/**").permitAll()
                .and()
                    ...
                .and()
                    .logout()
                    .logoutRequestMatcher(new AntPathRequestMatcher("/user/logout"))
                    .logoutSuccessUrl("/")
                    .invalidateHttpSession(true);
        return http.build();
    }
    
...
  • logoutRequestMatcher(new AntPathRequestMatcher("/user/logout"))
    : 로그아웃 URL
  • logoutSuccessUrl("/"): 로그아웃 성공 시 리다이렉트 할 URL
  • invalidateHttpSession(true): 로그아웃 시 생성된 사용자 세션 삭제

0개의 댓글