본문 바로가기

Spring Boot

스프링 시큐리티를 이용한 로그인

📌Intro


스프링 시큐리티를 이용한 로그인은 회원가입보다 다소 복잡하다.
아래와 같은 순서로 진행된다.

  • SecurityConfig에 로그인, 로그아웃 URL 등록
  • SignInController에 매핑 함수 구현
  • sign-in.html을 타임리프로 작성
  • UserRole enum 생성
  • UserSecurityService 로 권한 관리
  • SecurityConfig에 AuthenticationManager 빈을 생성
  • 로그인 / 로그아웃에 따라서 네비 바 바꾸기

📌 SecurityConfig에 로그인, 로그아웃 URL 등록


@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests
                        .requestMatchers(new AntPathRequestMatcher("/**")).permitAll())
                .formLogin((formLogin) -> formLogin
                        .loginPage("/sign-in")
                        .loginProcessingUrl("/sign-in")
                        .usernameParameter("email")
                        .passwordParameter("password")
                        .defaultSuccessUrl("/"))
                .logout((logout) -> logout
                        .logoutRequestMatcher(new AntPathRequestMatcher("/sign-out"))
                        .logoutSuccessUrl("/")
                        .invalidateHttpSession(true))
        ;

        return http.build();
    }

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

위와 같이 로그인 form URL를 등록한다.
formLogin은 스프링 시큐리티에서 제공하는 인증(=로그인) 방식이다.
formLogin이 제공하는 메서드는 아래와 같다.

※ usernameParameter에 들어갈 인자는 form 페이지의 <input name=""> 에 해당하는 값이다.
이 부분을 설정 안해줘서 2시간동안 헤매었었다.. 이 글을 보는 분들은 꼭 제대로 설정하길 바란다.

http.formLogin()
                .loginPage("/login.html")     // 사용자 정의 로그인 페이지
                .defaultSuccessUrl("/home")  // 로그인 성공 후 이동 페이지
                .failureUrl("/login.html?error=true")  // 로그인 실패 후 이동 페이지
                .usernameParameter("username")  // 아이디 파라미터명 설정
                .passwordParameter("password")  // 패스워드 파라미터명 설정
                .loginProcessingUrl("/login")  // 로그인 Form Action Url
                .successHandler(loginSuccessHandler())  // 로그인 성공 후 핸들러
                .failureHandler(loginFailureHandler())  // 로그인 실패 후 핸들러

📌 sign-in.html을 타임리프로 작성


로그인 form을 타임리프로 작성한다.
tip> ChatGPT의 도움을 받으면 부트스트랩이 적용된 html을 빨리 작성할 수 있다.

<html layout:decorate="~{layout}" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<div layout:fragment="content" class="container my-3">
    <div>
        <!-- Header Text -->
        <h1 class="text-center">GatchTable Sign In</h1>
        <!-- Sign In Form -->
        <form th:action="@{/sign-in}" th:object="${signInInputDto}" method="post">
            <div th:if="${param.error}" class="alert alert-danger"> 사용자ID 또는 비밀번호를 확인해 주세요.</div>
            <div class="mb-3">
                <label for="email" class="form-label">E-mail</label>
                <input type="text" name="email" id="email" placeholder="email" class="form-control">
            </div>
            <div class="mb-3">
                <label for="password" class="form-label">Password</label>
                <input type="password" name="password" id="password" placeholder="password" class="form-control">
            </div>
            <button type="submit" class="btn btn-primary">Sign In</button>
        </form>
    </div>
</div>
</html>

위 코드에서 <div th:if="${param.error}"> 사용자ID 또는 비밀번호를 확인해 주세요.</div>를 볼 수 있다.
스프링 시큐리티에서는 로그인 페이지에서 로그인 실패시 자동으로 parameter로 erorr를 보내준다.
따라서 praram.error를 조사하여 에러를 띄울 수 있는 것이다.

📌 UserRole enum 생성

사용자의 권한을 담을 enum을 생성한다.
이때 그냥 enum이 아니라 String value를 갖는 enum이여야 한다.
추후 사용자에게 권한을 줄 때 UserRole 타입으로 넘겨주는게 아니라 String 타입으로 넘겨줘야 하기 때문이다.

@Getter
public enum UserRole {
    CUSTOMER("ROLE_CUSTOMER"),
    OWNER("ROLE_OWNER");

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

    private String value;
}

 

 

📌 UserSecurityService 로 권한 관리


UserSecurityService는 스프링 시큐리티 로그인 처리의 핵심 부분이라고 할 수 있다.
UserSecurityService는 SecurityConfig에 등록한 formLogin 페이지로부터 입력을 받는다.
입력받는 값은 formLogin에서 선언해둔 usernameParameter("username") 에 해당하는 값이다.
입력한 값에 대응하는 비밀번호를 찾고, 사용자가 입력한 비밀번호와 입력하는지 비교해준다.

비밀번호 비교 로직은 내부에서 구현되어있기 때문에, 구현하지 않아도 된다.
우리가 구현할 부분은 입력받는 값에 해당하는 객체를 찾고, 권한을 설정하여 User 객체를 반환하는 것이다.

cf. User 와 UserDetails의 차이
UserDetails : 사용자의 인증 및 권한 정보를 제공하는 인터페이스
User : UserDetails 인터페이스를 구현한 클래스. username, password, authorities를 저장

@Service
@RequiredArgsConstructor
public class UserSecurityService implements UserDetailsService {

    private final CustomerRepository customerRepository;
    private final OwnerRepository ownerRepository;

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
    
        Optional<Customer> optionalCustomer = customerRepository.findByEmail(email);
        Optional<Owner> optionalOwner = ownerRepository.findByEmail(email);

        List<GrantedAuthority> authorities = new ArrayList<>();
        UserDetails userDetails = new User("fail", "fail", authorities);

        if (optionalCustomer.isEmpty() && optionalOwner.isEmpty()) {
            System.out.println("등록된 이메일 계정이 없습니다.");
            throw new UsernameNotFoundException("등록된 이메일 계정이 없습니다.");
        }

        if(optionalCustomer.isPresent()){
            Customer customer = optionalCustomer.get();
            authorities.add(new SimpleGrantedAuthority(UserRole.CUSTOMER.getValue()));
            System.out.println("커스터머 이메일 : " + customer.getEmail() + "비번 : " + customer.getPassword() + "권한" + authorities);
            userDetails = new User(customer.getEmail(), customer.getPassword(), authorities);
        }

        if(optionalOwner.isPresent()) {
            Owner owner = optionalOwner.get();
            authorities.add(new SimpleGrantedAuthority(UserRole.OWNER.getValue()));
            userDetails = new User(owner.getEmail(), owner.getPassword(), authorities);
        }

        return userDetails;
    }
}

📌 SecurityConfig에 AuthenticationManager 빈을 생성


@Configuration
@EnableWebSecurity
public class SecurityConfig {
	// 중략
	
    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

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

AuthenticationManager는 스프링 시큐리티의 인증(로그인)을 담당한다.
사용자 인증시 앞에서 작성한 UserSecurityService와 PasswordEncoder를 사용한다.
따라서 UserSecurityService를 이용해 로그인하기 위해서는 AuthenticationManager를 빈으로 등록해야 한다.

📌 로그인 / 로그아웃에 따라서 네비 바 바꾸기


Thymeleaf 에서는 인증(=로그인) 상태에 따라서 html을 표시하거나 숨기는 기능을 제공한다. 
sec:authorize="isAuthenticated()" - 이 속성은 로그인 된 경우에만 해당 엘리먼트가 표시되게 한다.
sec:authorize="isAnonymous()" - 이 속성은 로그인 되지 않은 경우에만 해당 엘리먼트가 표시되게 한다.
따라서 로그인 버튼은 isAnonymous()로, 로그아웃 버튼은 isAuthenticated()로 설정해준다.

<a sec:authorize="isAnonymous()" class="nav-link" th:href="@{/sign-in}" >SignIn</a>
<a sec:authorize="isAuthenticated()" class="nav-link" th:href="@{/sign-out}" >SignOut</a>