spring Security 3. - 로그인, 로그아웃

알파로그·2023년 3월 22일
0

Spring Boot

목록 보기
25/57

✏️ 전체적인 순서

  1. Configuration
    • log-in / log-out url 설정
    • 인증 기능 Bean 등록
  2. Repostiory
    • 이름으로 조회기능 추가
  3. 등급 설정
    • enum 객체를 생성해 원하는 등급을 선언
  4. Security Service
    • Security 로직만을 담당하는 객체 를 별도로 생성
    • 사용자가 입력한 name 을 기반으로 User 객체를 조회해줌
    • ⚠️ 이후 Spring Security 에 의해 자동으로 검증 작업 이후 log-in 처리가 완료됨
  5. Controller 계층
    • 요청 url 을 매핑해 login form 을 반환함
  6. Web 계층
    • 클라이언트가 입력한 Param 값을 Configuration 에 등록한 url 로 Post 요청

✏️ Configuration 계층

  • log-in 기능 추가
    • 지난번에 설정했던 접근 권한에 and() 로 기능을 추가해주면 된다.
    • loginPage
      • /user/login 로 오는 요청은 로그인 이라는 뜻
      • 여기서 말하는 요청 url 은 POST 를 뜻한다.
    • defaultSuccessUrl
      • 로그인이 완료될 경우 반환되는 페이지
      • redirect:/ 와 동일한 기능
  • log-out 기능 추가
    • log-in 뒤에 또 and() 를 붙여 추가해주면 된다.
    • logoutRequestMatcher
      • log-out URL 을 매핑
    • logoutSuccessUrl
      • log-out 성공시 홈으로 redirect
    • invalidateHttpSession(true)
      • log-out 에 생성된 사용자 세션 삭제
  • 인증 담당 Method
    • UserSecurityService 의 반환값을 통해 db 의 user 정보와 클라이언트가 입력한 정보가 일치하는지 확인해주는 기능
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    //-- Spring Security 접근 권한 허용 설정 --//
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests().requestMatchers(
                new AntPathRequestMatcher("/**")
        ).permitAll()
                // 특정 url 에 로그인 기능을 등록
                .and()
                .formLogin()
                .loginPage("/user/login")
                .defaultSuccessUrl("/")
                // 로그 아웃 기능
                .and()
                .logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/user/logout"))
                .logoutSuccessUrl("/")
                .invalidateHttpSession(true);

        return http.build();
    }

    //-- 인증을 담당하는 Bean 등록 --//
    // UserSecurityServicePasswordEncoder 가 자동으로 설정된다.
    @Bean
    AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws  Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

		...
}

✏️ login 기준 설정

  • 무엇을 기준으로 로그인 성공 실패를 결정할지 정해주어야 한다.
  • 이미 회원가입 할 때 DB 에 저장했으므로 그 정보를 기준으로 정하면 된다.
    • Security 설정파일에 직접 계정을 등록해 처리하는 방법도 있다.

📍 Repository 계층

  • DB 의 username 을 조회해야 하므로 기능을 추가해준다.
public interface UserRepository extends JpaRepository<SiteUser, Long> {

    //-- username 조회 --//
    Optional<SiteUser> findByUsername(String username);
}

📍 권한 설정

  • Spring Security 는 인증 뿐 아니라 권한도 관리할 수 있다.
    • 인증 (로그인) 이 완료됬을 때 사용자에게 부여할 권한을 설정할 수 있다.
    • ADMIN 과 USER 2개의 권한을 설정했다.
@Getter // 상수 자료형이기 때문에 @Setter 는 선언하지 않았다.
public enum UserRole { // 열거 자료형 enum 을 사용

    ADMIN("ROLE_ADMIN"),
    USER("ROEL_USER");

    private String value;

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

📍 Service 계층

  • Business logic 을 수행했던 기존 Service 가 아닌 Security logic 을 수행하기 위한 UserSecurityService 를 새로 생성한다.
    • 원하는 기능을 사용하기 위해 UserDetailsService 을 상속받는다.
  • Spring Security 는 loadUserByUsername Method 에 의해 반환된 User 객체의 Password 가 클라이언트가 입력한 Password 와 일치하는지 자동으로 검사해준다.
@Service
@RequiredArgsConstructor
public class UserSecurityService implements UserDetailsService {

    private final UserRepository repository;

    //-- Param 값으로 Password 를 조회하여 반환하는 method --//
		// 반환값을 이용해 Spring Security 가 자동으로 일치여부를 검사해준다.
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 클라이언트가 입력한 username 와 일치하는 SiteUSer 를 반환
        Optional<SiteUser> _siteUser = repository.findByUsername(username);

        // 만약 일치하는 username 이 없다면 예외를 발생시킨다.
        if (_siteUser.isEmpty())
            throw new UsernameNotFoundException("사용자를 찾을 수 없습니다.");

        // 일치할경우 get() 으로 UserSite 를 꺼낸다.
        SiteUser siteUser = _siteUser.get();

        // 클라이언트의 권한을 저장하는 List
        ArrayList<GrantedAuthority> authorities = new ArrayList<>();
        // username 이 "admin" 일 경우 ADMIN 권한을 저장한다.
        if ("admin".equals(username))
            authorities.add(new SimpleGrantedAuthority(UserRole.ADMIN.getValue()));
        // 아닐경우 USER 권한을 저장한다.
        else
            authorities.add(new SimpleGrantedAuthority(UserRole.USER.getValue()));

        // username, 비밀번호, 권한을 매개변수로 하는 Spring Security 의 User 객체를 생성해 반환한다.
        return new User(siteUser.getUsername(), siteUser.getPassword(), authorities);
    }
}

✏️ Controller 계층

  • Get 방식으로 login_form.html 을 렌더링하는 Method 를 추가했다.
  • 실제 로그인을 진행하는 @PostMapping Method 는 Spring Security 가 대신 처리해주기 때문에 직접 구현할 필요가 없다.
@Controller
@RequestMapping("/user")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    ...

    //-- 로그인 폼 --//
    @GetMapping("/login")
    public String login() {
        return "login_form";
    }

✏️ Web 계층

📍 로그인 폼

  • 로그인을 실패할경우 다시 폼으로 Redirect 되고 error 가 함께 전달되 예외 처리 로직이 동작한다.
    • 로그인 실패시 파라미터로 error가 전달되는 것은 스프링 시큐리티의 규칙이다.
<html layout:decorate="~{layout}" 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 text="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>
        <a th:href="@{/user/signup}" class="btn btn-primary">회원가입</a>
    </form>
    
</div>
</html>

📍 네비게이션 바

  • 로그인을 완료할 경우 로그인 → 로그아웃으로 버튼을 변경시키는 기능 구현
    • sec:authorize="isAnonymous()”
      • 로그아웃 상태일 경우 true
      • 로그인 링크가 표시됨
    • sec:authorize="isAuthenticated()”
      • 로그인 상태일 경우 true
      • 로그아웃 링크가 표시됨
<nav th:fragment="navbarFragment"
     class="navbar navbar-expand-lg navbar-light bg-light border-bottom" xmlns:sec="http://www.w3.org/1999/xhtml">
    <div class="container-fluid">                                          <!-- sec 기능 import -->
        <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>
                </li>
                <li class="nav-item">
                    <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>
profile
잘못된 내용 PR 환영

0개의 댓글