사이먼's 코딩노트

[SpringBoot] 로그인 구현 본문

Java/SpringBoot

[SpringBoot] 로그인 구현

simonpark817 2024. 5. 13. 23:34

[로그인 구현하기]

  • 회원가입 기능을 구현하였다면, 이번에는 로그인 기능을 구현해봅시다.
  • 현재 DB에서 site_user라는 테이블에 회원 데이터가 저장되어 있고, 테이블에 저장된 ID와 비밀번호로 로그인을 진행한다.
  • 먼저 스프링 시큐리티가 적용된 SecurityConfig.java 클래스에 로그인을 위한 URL을 아래와 같이 설정한다.
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
            .authorizeHttpRequests(
                    (authorizeHttpRequests) -> authorizeHttpRequests
                    // 로그인 없이도 열람이 가능한 페이지
                    .requestMatchers(new AntPathRequestMatcher("/question/list")).permitAll()
                    .requestMatchers(new AntPathRequestMatcher("/user/signup")).permitAll()
                    .requestMatchers(new AntPathRequestMatcher("/user/login")).permitAll()
                    .requestMatchers(new AntPathRequestMatcher("/style.css")).permitAll()
                    .requestMatchers(new AntPathRequestMatcher("/")).permitAll()
                    // 로그인이 완료되면 나머지 페이지들도 열람 가능
                    .anyRequest().authenticated()
            )
            .formLogin((formLogin) -> formLogin
                    .loginPage("/user/login")
                    .loginProcessingUrl("user/login")
                    .defaultSuccessUrl("/"))
    ;
    return http.build();
}
  • 상단에 추가된 requestMatchers() 메서드를 통해 로그인 없이도 열람이 가능한 URL 주소를 미리 설정한다.
  • 지금의 경우 로그인 없이 열람이 가능한 화면은 질문 목록, 회원가입, 로그인, Root URL과 스타일시트이다. 스타일시트는 폰트적용이나 기본 디자인을 위해서 반드시 포함해야한다.
  • formLogin() 메서드는 스프링 시큐리티의 로그인 설정을 담당하는 부분으로, 설정 내용은 로그인 페이지의 URL은 /user/login이고 로그인 성공 시에 이동할 페이지는 Root URL임을 의미한다.
  • loginPage("user/login")은 GET 방식으로 요청되면 스프링 시큐리티에게 우리가 만든 로그인 페이지 URL을 전송한다.
  • 이 코드를 작성하지 않으면 로그인 페이지는 기본으로 되어있는 "/login"이 된다.
  • loginProcessingUrl("/user/login")은 POST 방식으로 요청되면 스프링 시큐리티에게 로그인 폼 처리 URL을 알려준다.

 

[로그인 URL 매핑 추가]

  • 스프링 시큐리티에 로그인 URL을 /user/login으로 설정했다면 UserController.java 클래스에서 해당 URL을 매핑해야한다.
  • 아래는 해당 코드를 UserController.java 클래스에서 추가한 모습이다.
@GetMapping("/login")
public String login() {
    return "login_form";
}
  • @GetMapping 어노테이션을 통해 /user/login URL로 들어오는 GET 요청을 login() 메서드가 처리한다.
  • 매핑한 login() 메서드는 login_form.html 템플릿을 return 하도록한다.
  • 실제 로그인을 진행하는 @PostMapping 방식의 메서드는 스프링 시큐리티가 대신 처리하기 때문에 직접 컨트롤러에 코드를 추가할 필요는 없다.

 

[login_form.html 템플릿 생성]

  • 아래는 실제로 페이지에 로그인 화면이 표출될 수 있도록 login_form.html 템플릿을 생성하고 작성한 코드이다.
<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>
  • 사용자 ID와 비밀번호로 로그인할 수 있는 템플릿을 작성하고 스프링 시큐리티의 로그인이 실패할 경우에는 시큐리티의 기능으로 인해 로그인 페이지로 리다이렉트된다.
  • 로그인 페이지의 매개변수로 error가 전달되는 경우, 사용자 ID 또는 비밀번호를 확인해 주세요. 라는 에러 메시지를 출력하도록 하였다.
  • 로그인 실패 시 매개변수로 error가 전달되는 것은 스프링 시큐리티의 규칙이라고 생각하면 좋다.
  • 스프링 시큐리티는 로그인 실패 시 localhost:8090/user/login?error와 같이 error 매개변수를 전달하고 이 때, 템플릿에서 th:if="${param.error}"를 통해 error 매개변수가 전달되었는지 확인할 수 있다.

 

[UserSecurityService 서비스 생성]

  • 지금 상태에서 로그인을 수행할 수는 없다. 그 이유는 스프링 시큐리티에 무엇을 기준으로 로그인해야 하는지 아직 설정하지 않았기 때문이다.
  • 스프링 시큐리티를 통해 로그인을 수행하는 방법에는 여러 가지가 있지만 우리는 이미 회원 정보를 DB에 저장했기 때문에 DB에서 회원 정보를 조회하여 로그인하는 방법을 사용할 것이다.
  • 그렇기 때문에 DB에서 사용자를 조회하는 서비스인 UserSecuritySerivce.java 클래스를 생성하고, 그 서비스를 스프링 시큐리티에 등록해야 한다.
  • 먼저 서비스를 생성하기 전에 UserRepository를 수정하고 UserRole.java 클래스를 생성하는 과정이 필요하다.
  • 아래는 UserRepository.java 클래스에서 사용자 ID로 SiteUser 엔티티를 조회하는 findByUsername() 메서드를 추가한 코드이다.
Optional<SiteUser> findByusername(String username);

 

  • 스프링 시큐리티는 인증뿐만 아니라 권한도 관리하기 때문에 사용자 인증 후 사용자에게 부여할 권한과 관련된 내용도 필요하다.
  • 그러므로 사용자가 로그인한 후, Admin 또는 User와 같은 권한을 부여해야 한다.
  • 아래는 UserRole.java 클래스를 추가하여 작성한 코드이다.
package com.sbs.sbb.User;

import lombok.Getter;

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

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

    private String value;
}
  • UserRole은 enum 자료형으로 작성하였다.
  • 관리자를 의미하는 Admin과 사용자를 의미하는 User라는 상수를 만들었고, ADMIN은 'ROLE_ADMIN', USER는 'ROLE_USER'라는 값을 부여하였다.
  • UserRole의 Admin과 User 상수는 값을 변경할 필요가 없기 때문에 @Getter만 사용할 수 있도록 하였다.

 

  • 준비가 완료되었다면 UserSecurityService.java 클래스를 생성하여 아래와 같이 코드를 작성한다.
package com.sbs.sbb.User;

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
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);
    }
}
  • 스프링 시큐리티가 로그인 시 사용할 UserSecurityService는 스프링 시큐리티가 제공하는 UserDetailsService 인터페이스를 구현해야 한다.
  • UserDetailsService는 loadUserByUsername() 메서드를 구현하도록 강제하는 인터페이스이다.
  • loadUserByUsername() 메서드는 사용자명으로 스프링 시큐리티의 사용자 객체를 조회하여 return 하는 메서드이다.
  • loadUserByUsername() 메서드는 사용자명으로 SiteUser 객체를 조회하고, 만약 사용자명에 해당하는 데이터가 없을 경우에는 UsernameNotFoundException을 발생시킨다.
  • 사용자명이 'admin'인 경우에는 Admin 권한인 'ROLE_ADMIN'을 부여하고 그 외의 경우에는 User 권한을 부여하였다.
  • 마지막으로 User 객체를 생성하여 return 하는데, 이 객체는 스프링 시큐리티에서 사용하며 User 생성자에는 사용자명, 비밀번호, 권한 리스트가 전달된다.
  • 참고로 스프링 시큐리티는 loadUserByUsername() 메서드에 의해 return된 User 객체의 비밀번호가 사용자로부터 입력받은 비밀번호와 일치하는지 검사하는 기능을 내부에 가지고있다.

 

[로그인 화면 링크 추가]

  • 마지막으로 로그인 페이지에 진입할 수 있도록 로그인 링크인 /user/login을 내비게이션 바에 추가해봅시다.
  • 아래는 navbar.html 에서 수정된 코드이다.
<li class="nav-item">
    <a class="nav-link" th:href="@{/user/login}">로그인</a>
</li>
  • th:href="@{/user/login}"와 같이 링크를 수정해주면 '로그인'  버튼을 클릭했을 때 로그인 화면이 성공적으로 표시된다.

 

  • 모든 코드 작성을 마쳤다면 로컬 서버를 다시 실행시켰을 때 아래 사진과 같은 로그인 화면이 표시된다.
  • 포스팅 가장 상단에서 적용했던 것을 다시 기억해보면, 스프링 시큐리티를 통해 로그인을 하지 않았을 때 열람이 되지 않는 페이지가 있다는 것을 참고하면서 테스트를 해보면 좋을 것 같다.

로그인 기능 구현

 

반응형