sm 기술 블로그

[스프링 시큐리티] 로그인 본문

스프링부트

[스프링 시큐리티] 로그인

sm_hope 2022. 7. 23. 10:39

로그인 URL 등록

스프링 시큐리티에 다음과 같이 등록한다.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
		...(생략)...
            .and()
                .formLogin()
                .loginPage("/user/login")
                .defaultSuccessUrl("/")
        ;
        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
  • .formLogin()은 스프링 시큐리티에서 제공하는 메소드로 로그인을 진행한다는 뜻이다.
  • .loginPage("/user/login")은 로그인 페이지를 알려주는 것으로 현재 우리가 로그인을 이용하는 페이지는 /user/login 이다.
  • .defaultSuccessUrl("/") 로그인에 성공할 시 /페이지로 이동한다는 뜻이다.

컨트롤러 등록

(... 생략 ...)
public class UserController {

    (... 생략 ...)

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

usr/login으로 매핑이 들어오면 login_form으로 이동한다.

로그인 폼

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

UserRepository에 메소드 추가

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;

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

사용자 조회를 유저 이름으로 찾을 것이기 때문에 findByusername를 사용한다.

UserRole

package com.mysite.sbb.user;

import lombok.Getter;


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

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

    private String value;
}

권한 관리를 진행한다.
이 구문은 열거형 방식으로 작성 되었으며 열거형은 변수나 상수에 대한 의미를 부여한 것이고 어디서든 사용 가능하다고 생각하면 된다.
즉 ADMIN이 들어온다면 ROLE_ADMIN로 반환,
USER가 들어온다면 ROLE_USER로 반환

UserSecurityService

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

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 lombok.RequiredArgsConstructor;

@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);
    }
}

UserDetails loadUserByUsername은 스프링 시큐리티가 제공하는 UserDetailsService의 메소드로 사용자 명을 통해 비밀번호를 조회하는 메소드 이다.
메소드 내부를 살펴보면,
먼저 아까 리포지터리에서 작성한 메소드와 입력받은 사용자 이름으로 유저(_siteUser)를 찾는다.

Optional<SiteUser> _siteUser = this.userRepository.findByusername(username);

만약 유저가 없다면 사용자를 찾을 수 없다는 에러를 던진다.
던져진 에러는 뒤에서 작성할 SecurityConfig의 authenticationManager에 던져진다.
참고로 "사용자를 찾을 수 없습니다"는 프론트에서의 확인용이 아니다.

        if (_siteUser.isEmpty()) {
            throw new UsernameNotFoundException("사용자를 찾을수 없습니다.");
        }

에러가 던져진게 없다면 값을 .get을 통해 가져온다(Optional로 했기 때문에)

SiteUser siteUser = _siteUser.get();

GrantedAuthority는 현재 사용자가 가지고 있는 권한을 의미한다.
GrantedAuthority를 따로 써주는 이유는 어플리케이션 전반에 걸친 권한이여서 특정 도메인에 한정되지 않기 때문이다.
다시말해 /든 /list/detail이든 어디서든 권한을 유지한다.
※만약 특정 도메인에서는 다르게 구성해야 한다면 GrantedAuthority를 사용하지 않는다.

List<GrantedAuthority> authorities = new ArrayList<>();

만약 admin으로 들어온다면 권한을 관리자로, 그렇지 않으면 유저로 부여한다.
SimpleGrantedAuthority객체를 생성해서 권한을 저장하고 authorities에 추가시킨다.

        if ("admin".equals(username)) {
            authorities.add(new SimpleGrantedAuthority(UserRole.ADMIN.getValue()));
        } else {
            authorities.add(new SimpleGrantedAuthority(UserRole.USER.getValue()));
        }

이 User는 시큐리티 자체적으로 제공하는 객체로, User에 값을 넣고 리턴한다.
시큐리티는 loadUserByUsername 메소드에 의해 리턴된 User 객체의 비밀번호가 입력받는 비밀번호와 일치하는지 검사하는 로직이 있다.
즉, User(입력받은 유저이름, 입력받은 유저비밀번호, 권한)을 리턴함으로써 시큐리티가 자체적으로 DB의 비밀번호와 대조한다.

return new User(siteUser.getUsername(), siteUser.getPassword(), authorities);

SecurityConfig 에 UserSecurityService 추가

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;

import com.mysite.sbb.user.UserSecurityService;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final UserSecurityService userSecurityService;

    (... 생략 ...)

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

위에서 만든 UserSecurityService를 연결한다.

private final UserSecurityService userSecurityService;

AuthenticationManager는 인증이다.
AuthenticationManager는 @Bean으로 생성시 스프링 내부 동작으로 인해 UserSecurityService와 PasswordEncoder가 자동으로 설정된다.
또한 UserSecurityService에서 던저진 에러를 받는다.
받은 에러는 다시 던지고 던져진 error는 login_form.html이 받는다.

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

에러를 받는 부분이

        <div th:if="${param.error}">
            <div class="alert alert-danger">
                사용자ID 또는 비밀번호를 확인해 주세요.
            </div>
        </div>

이 부분이며, param에 에러가 있다면,

            <div class="alert alert-danger">
                사용자ID 또는 비밀번호를 확인해 주세요.
            </div>

이부분이 화면에 보일 것이다.

Comments