본문 바로가기

카테고리 없음

스프링 시큐리티를 이용하여 로그인 구현

이 글은 '스프링 시큐리티를 이용하여 로그인 화면 띄워보자' 와 이어지는 내용입니다.

이해가 잘 안되신다면 '스프링 시큐리티를 이용하여 로그인 화면 띄워보자' 을 먼저 읽으면서 실습한 후 따라오시면 한 결 편하게 이해하실 수 있을 것입니다.

 

 

스프링 시큐리티 사용 전과 사용 후 과정을 비교해 보겠습니다.

기존에는 클라이언트가 요청을 보내면 컨트롤러로 바로 요청이 전달되었으나

이제는 스프링 시큐리티가 중간에서 검열을 하기 시작합니다.

보안 요원이 등장한 것이죠

 

 

1.Spring Security의 진행 과정을 살펴봅시다.


그렇다면 스프링 시큐리티는 어떤 역할을 어떻게 진행하고 있을까요?

아래 그림을 살펴보면서 이해해 보도록 하겠습니다.

 

우선 회원가입은 이미 완료된 상태라고 가정합니다.

 

1-1. 로그인시도

로그인을 하면서 클라이언트가 username과 password를 입력합니다. 

이후 로그인 버튼을 클릭하고 로그인 될 때 까지 기다립니다.

 

1-2. Authentication Manager (인증 관리자)

username과 password를 클라이언트로 부터 전달받고

UserDetails Service 에게 username을 넘겨줍니다.

 

1-3. UserDetails Service

진짜 회원이 맞는지 검사하기 위해 db에서 회원 정보를 꺼내오는 단계입니다.

Authentication Manager로 부터 받은 username으로 회원 정보 db에서 회원 데이터가 있는지 확인합니다.

 

회원 데이터가 존재한다면 UserDetails에 회원 정보 (User)를 Authentication Manager에게 전달해준다.

UserDetails는 뒤에서 조금 더 자세히 살펴보도록 하겠습니다.

 

회원 데이터가 존재하지 않는다면 Error가 발생합니다.

 

1-4. Authentication Manager (인증 관리자)

UserDetails Service로 부터 받은 회원 정보와 클라이언트에게 받은 회원 정보를 비교하는 단계입니다.

UserDetails Service에게서 전달 받은 UserDetails에서 회원 정보 (User)를 꺼내고

클라이언트에게 입력 받은 password와 회원 정보(User)에 존재하는 password를 비교합니다.

이전 '비밀번호 암호화' 글에서 말했던 것과 같이 비밀번호는 '일방향' 암호 알고리즘을 사용해서 저장해 놓은 상태이기 때문에

클라이언트에게 입력 받은 password와 회원 정보에 존재하는 password를 단순 비교할 수는 없습니다.

클라이언트에게 입력 받은 password를 암호화 하고, 암호화 된 password를 회원 정보에 존재하는 password와 비교하게 됩니다.

Authentication Manager가 알아서 진행해 주는 부분입니다.

 

일치할 경우 http에서 상태 저장하기 위해서 Session을 생성해서 클라이언트에게 전달해줍니다.

불일치 할 경우 Error가 발생합니다.

 

2. 직접 구현해 봅시다.


회원 정보 DB가 회사마다 다르게 설계가 되어 있을 것이기 때문에

회원 정보를 찾아오는 역할을 하는 UserDetails Service는 직접 구현을 해주어야 합니다.

 

스프링 시큐리티에서 만들어 놓은 인터페이스를 가지고 UserDetails Service와 UserDetails를 구현하면 나머지는 알아서 Spring Security가 진행을 해줍니다.

 

로그아웃 처리

서버 세션에 저장되어 있는 로그인 사용자 정보 삭제

 

* 주의

로그인을 처리할 때 사용자 id와 password는 username과 password로 전달을 해주어야 합니다. (클라이언트 -> 서버)

이 것은 스프링 시큐리티에 지정된 key 값이기 때문에 front에서 넘어올 때 반드시 username과 password로 넘어올 수 있도록 부탁합시다.

 

2-1. WebSecurityConfig

Spring Web security config를 아래와 같이 변경해 봅시다.

아래 내용이 잘 이해가 가지 않는다면 '스프링 시큐리티를 이용하여 로그인 화면 띄워보자' 부분을 확인 후 실습해 보시기 바랍니다.

 

@Configuration
@EnableWebSecurity // 스프링 Security 지원을 가능하게 함
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public BCryptPasswordEncoder encodePassword() {
        return new BCryptPasswordEncoder();
    }

    @Override
    public void configure(WebSecurity web) {
// h2-console 사용에 대한 허용 (CSRF, FrameOptions 무시)
        web
            .ignoring()
            .antMatchers("/h2-console/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 회원 관리 처리 API (POST /user/**) 에 대해 CSRF 무시
        http.csrf()
            .ignoringAntMatchers("/user/**");

        http.authorizeRequests()
            // image 폴더를 login 없이 허용
            .antMatchers("/images/**").permitAll()
            // css 폴더를 login 없이 허용
            .antMatchers("/css/**").permitAll()
            // 회원 관리 처리 API 전부를 login 없이 허용
            .antMatchers("/user/**").permitAll()
            // 그 외 어떤 요청이든 '인증'
            .anyRequest().authenticated()
            .and()
            // [로그인 기능]
            .formLogin()
            // 로그인 View 제공 (GET /user/login)
            .loginPage("/user/login")
            // 로그인 처리 (POST /user/login)
            .loginProcessingUrl("/user/login")
            // 로그인 처리 후 성공 시 URL
            .defaultSuccessUrl("/")
            // 로그인 처리 후 실패 시 URL
            .failureUrl("/user/login?error")
            .permitAll()
            .and()
            // [로그아웃 기능]
            .logout()
            // 로그아웃 처리 URL
            .logoutUrl("/user/logout")
            .permitAll();
    }
}

 

로그인 로그아웃에 관한 내용을 한번 살펴보도록 하겠습니다.

 

.loginPage에 대한 내용은 '스프링 로그인'에서도 확인할 수 있었습니다.

로그인 화면에 대한 처리는 GET 메서드를 이용하여 /user/login url로 요청을 하겠다는 내용입니다.

 

// 로그인 View 제공 (GET /user/login)
.loginPage("/user/login")

 

 

로그인에 관한 처리는 POST 메서드를 이용하여 /user/login url로 요청을 하겠다는 내용입니다.

 

// 로그인 처리 (POST /user/login)
.loginProcessingUrl("/user/login")

 

logout은 GET으로 처리되지 않습니다.

이 부분은 뒤에서 다시 살펴보도록 하겠습니다.

 

// [로그아웃 기능]
.logout()

 

2-2. UserDetails Service

앞에서 Authentication Manager가 UserDetails Service에게 회원 정보를 찾아달라고 요청을 한다고 하였습니다.

UserDetailsService는 인터페이스입니다.

인터페이스는 함수가 정의되어 있어서 상속을 받으면, 인터페이스에 존재하는 함수를 꼭 구현해주어야 합니다.

UserDetailsService를 상속 받아 구현해 보도록 하겠습니다.

 

상속 받아서 구현을 해보기 전에 UserDetailsService를 어떻게 구현하면 되는지 확인을 해 봐야겠죠??

UserDetailsService는 'loadUserByUsername 메서드를 구현해야 하며, username을 매개변수로 받아서 UserDetails를 반환해야 한다.' 라고 나타나 있네요

 

 

자 그럼 앞에서 살펴본 것과 같이 username를 이용하여 User를 가지고 와서 UserDetails에 User 정보를 넣어 반환하는 메서드를 구현해 보도록 하겠습니다.

 

username을 이용하여 User 정보를 찾습니다.

 

User user = userRepository.findByUsername(username)

 

들고온 회원 정보 (User)는 UserDetails에 회원 정보 (User)를 넣어서 Authentication Manager에게 전달해 줍니다.

 

return new UserDetailsImpl(user);

 

UserDetailsService를 상속 받아 구현한 UserDetailsServiceImpl의 전체 코드는 아래와 같습니다. 

 

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;

    @Autowired
    public UserDetailsServiceImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException("Can't find " + username));

        return new UserDetailsImpl(user);
    }
}

 

 

2-2. UserDetails

return 문을 살펴보시면 UserDetailsImpl에 User 정보를 담아서 전달하고 있죠??

UserDetailsImpl은 UserDetails 인터페이스를 상속 받아 구현한 클래스입니다.

같이 구현해 보도록 하겠습니다.

 

public interface UserDetails extends Serializable {
	Collection<? extends GrantedAuthority> getAuthorities();
	String getPassword();
	String getUsername();
	boolean isAccountNonExpired();
	boolean isAccountNonLocked();
	boolean isCredentialsNonExpired();
	boolean isEnabled();
}

 

 

UserDetails 인터페이스는 User 정보에서 password와 username 정보를 꺼낼 수 있도록 메서드를 구현해 주도록 합니다.

getAuthorities 메서드는 뒤에서 다시 한번 살펴보도록 하겠습니다.

UserDetails를 상속받아 UserDetailsImpl 클래스를 아래와 같이 구현합니다.

 

public class UserDetailsImpl implements UserDetails {

    private final User user;

    public UserDetailsImpl(User user) {
        this.user = user;
    }

    public User getUser() {
        return user;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Collections.emptyList();
    }
}

 

자 이제 적용을 완료하고 회원 가입 후 로그인을 하게되면 static에 존재하는 index.html 페이지가 나타나는 것을 확인하실 수 있습니다.