[점프 투 스프링] Spring Security 로그인-로그아웃

Honam Kim·2024년 12월 12일
post-thumbnail

깃허브 링크 : https://github.com/tigerpoint123/jsb-20241210
출처 : https://wikidocs.net/162814

스프링 시큐리티를 통해 로그인 로그아웃 구현을 해봐요

먼저 SecurityConfig 클래스를 수정해줍니다.

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests
                .requestMatchers(new AntPathRequestMatcher("/**")).permitAll())
            .csrf((csrf) -> csrf
                .ignoringRequestMatchers(new AntPathRequestMatcher("/h2-console/**")))
            .headers((headers) -> headers
                .addHeaderWriter(new XFrameOptionsHeaderWriter(
                    XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN)))
             // formLogin 메서드 = 스프링 시큐리티의 로그인 설정 담당
            .formLogin((formLogin) -> formLogin
                .loginPage("/user/login") // 로그인 페이지 URL
                .defaultSuccessUrl("/")) // 로그인 성공 시
        ;
        return http.build();
    }
}

이제 로그인 페이지 url을 컨트롤러에서 생성해줍니다.

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

GetMapping으로 url로 들어오는 요청을 처리합니다. 이제 login_form 이름의 html 템플릿을 만들어줍니다.

<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>

스프링 시큐리티에서는 로그인이 실패할 경우 로그인 페이지로 리다이렉트된다.

이때 페이지 매개변수로 error가 전달되기 때문에, 이게 전달되면 사용자에게 ID 혹은 비밀번호를

확인해달라는 문구를 출력하도록 했다. "div th:if="${param.error}">"

이제 회원 정보를 조회해서 로그인하는 방법으로 구현을 해봅시다.

public interface UserRepository extends JpaRepository<SiteUser, Long> {
    Optional<SiteUser> findByusername(String username);
    //사용자 id로 엔티티를 조회
}

스프링 시큐리티는 권한도 관리하기 때문에, UserRole 클래스도 별도로 생성

@Getter // 권한 상수값을 변경할 필요 없으니 setter는 생략
public enum UserRole {
    ADMIN("ROLE_ADMIN"),
    USER("ROLE_USER");

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

    private String value;
}

이제 스프링 시큐리티 서비스 부분을 구현해봅니다.

@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 인터페이스는 스프링 시큐리티에서 제공하고 있다.

이 친구는 loadUserByUsername 메서드를 구현하도록 강제한다. (진짜 처음 앎). 순서는 이와 같다.

  1. loadUserByUsername은 사용자명으로 SiteUser 객체 조회
    1-1. 데이터가 없을 경우 UsernameNotFoundException 발생
  2. 사용자명이 admin인 경우 관리자 권한을 부여, 이외에는 user 권한을 부여.
  3. 그리고 user 객체를 생성해 반환하고, 생성자는 사용자명, 비밀번호, 권한 리스트가 전달됨.

이제 시큐리티 설정에 아래 코드를 추가해 줍니다.

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

AuthenticationManager 빈은 스프링 시큐리티 인증을 처리합니다.

사용자 인증 시 앞에서 작성한 시큐리티 서비스와 PasswordEncoder를 내부적으로 사용해서

인증과 권한 부여 프로세스를 처리합니다.

이제 홈페이지 상단 바에 로그인 버튼을 추가해줍니다. 이때 로그인이 되면 로그인 버튼은 로그아웃이 돼야 하므로, 이를 수정해줍니다.

<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>

sec:authorize="isAnonymous()" 을 통해 비 로그인 상태가 true가 돼서 로그인 버튼이 뜨고,

sec:authorize="isAuthenticated()" 가 true면 로그아웃이 표시가 된다.

로그아웃 기능은 SecurityConfig 클래스를 수정해줍니다.

// 중략
            .formLogin((formLogin) -> formLogin
                .loginPage("/user/login")
                .defaultSuccessUrl("/"))
            .logout((logout) -> logout
                .logoutRequestMatcher(new AntPathRequestMatcher("/user/logout"))
                .logoutSuccessUrl("/")
                .invalidateHttpSession(true)) // 로그아웃이 되면 사용자 세션도 삭제.
// 생략
profile
자유로운영혼

0개의 댓글