Spring Security 로그인

바그다드·2023년 4월 3일
0

Spring Security

목록 보기
4/17
  • 지난번까지 회원가입 기능을 구현하였다. 이제 로그인 기능을 구현해보자!

SecurityConfig 수정

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.authorizeRequests()
                .antMatchers("/user/**").authenticated() // user 페이지에는 권한 필요
                .antMatchers("/manager/**").access("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')") // manager페이지에 필요한 권한
                .antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')") // admin페이지에 필요한 권한
                .anyRequest().permitAll() // 모든 요청은 권한 허가
                .and()
                .formLogin()
                .loginPage("/loginForm")
                // 로그인 관련 추가
                .loginProcessingUrl("/login")
                // 로그인 성공시 이동할 페이지
                .defaultSuccessUrl("/");

        return http.build();
    }
  • "/login"으로 접근을 하면 security가 낚아채서 대신 로그인을 처리해준다. 따라서 따로 컨트롤러에 login메소드를 만들지 않아도 된다.
  • defaultSuccessUrl은 loginForm페이지로 접근하여 로그인 성공 시에 이동하게 될 페이지이다.

loginForm.html 수정

  • form 태그에 action과 method를 추가해주자
<form action="/join" method="POST">

PrincipalDetails 생성

  • config 패키지 안에 auth 패키지를 생성 후 PrincipalDetails를 생성하자
public class PrincipalDetails implements UserDetails {

    private User user;//콤포지션

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

    // 해당 User의 권한을 리턴하는 메서드!!!
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collect = new ArrayList<>();
        collect.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return user.getRole();
            }
        });
        return collect;
    }

    @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() {

        //우리 사이트는 1년동안 회원 로그인을 안하면 휴면 계정으로 하기로 함!!!
        // 현재시간 - 로그인 시간 = 1년이상이면 휴면 계정으로 전환
        return true;
    }
}
  • 로그인을 시도하면 security가 이를 낚아채 로그인 기능을 대신 수행해준다.
    로그인이 성공하면 session이 생성되는데 시큐리티에서는 이렇게 만들어진 session을 Security ContextHolder라고 하는 시큐리티가 자체적으로 관리하는 저장 공간에서 session을 관리한다.
  • 이 세션에 접근하기 위해서는 Authentication이라는 오브젝트가 필요하고, Authentication에서는 User정보를 필요로 하는데, 이 때 User정보의 오브젝트 타입은 UserDetails타입이어야 한다!!
  • 위의 각 메서드에서 boolean타입을 리턴하는 메서드들은 다 true로 설정해주었는데, 로그인 기능을 다 구현하고 로그인을 시도해봤는데 에러가 떠서 확인해보니 중간에 false를 리턴하는 메서드가 있어서 그런거였다. 로그인이 안된다면 확인해보자!

PrincipalDetailsService 생성

  • config.auth.PrincipalService를 생성하자
@Service
public class PrincipalDetailsService implements UserDetailsService {

    private UserRepository userRepository;

    public PrincipalDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    // 중요!!!
    // 여기서 username과 loginForm.html의 username과 이름이 동일하게 username이어야 한다.
    // 중요!!
    // Security Session(Authentication(UserDetails))
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        System.out.println("username = " + username);
        User user = userRepository.findByUsername(username);
        if (user != null) {
            return new PrincipalDetails(user);
        }
        return null;
    }
}
  • '/login'이 호출되면 스프링은 컨테이너에서 UserDetailsService를 찾는다.
    - 여기서는 PrincipalDetailsService가 UserDetailsService를 상속중이다
  • 스프링은 UserDetailsService에서 loadUserByUsername메소드를 호출하고 이 메서드에서는 username을 인자값으로 받는다.
    - 스프링에서 username의 값을 자동으로 매칭시켜주기 때문에 loginForm.html에서 정의한 input의 name은 'username'으로 정의가 되어야 한다.
    - 굳이 다른 이름으로 정의하고 싶다면?
    // SecurityConfig에 아래의 코드를 추가해주자
      .usernameParameter("newName");

PrincipalDetailsService 수정

@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        User userEntity = userRepository.findByUsername(username);
        return null;
    }
  • 먼저 Authentication에서 user정보를 필요로 한다고 했으므로 User를 찾아오자

UserRepository 수정

public interface UserRepository extends JpaRepository <User, Integer> {

    // Jpa Query 메서드 규칙
    // findBy는 규칙 -> Username문법
    // select * from user where username = :username
    public User findByUsername(String username);
}
  • 이렇게 되면 Jpa Query 메서드 규칙에 의해 위에 적힌 쿼리문을 실행시켜준다!

PrincipalDetailsService 수정

@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
        User userEntity = userRepository.findByUsername(username);
        
        if(userEntity!=null){
        
        	return new PrincipalDetails(userEntity);
        }
    }
  • 이제 이 user객체가 null이 아니라면 PrincipalDetails(userEntity)를 리턴해주자
  • 앞서 Session에 접근하려면 Authentication이 필요하고 Authentication은 UserDetails를 필요로 한다고 하였다.
  • 여게서 PrincipalDetails는 위에서 봤듯이 UserDetails를 상속하고 있으므로 PrincipalDetails이 Authentication(PrincipalDetails) 이런식으로 들어가게 되고 Authentication는 다시 SecuritySession(Authentication(PrincipalDetails)) 이런식으로 들어가게 된다!

로그인 기능 테스트

http://localhost:8080/user

  • user로 접근을 시도하면 loginForm으로 이동하고

  • 로그인을 성공하면 user페이지로 다시 돌아간다.
  • 그런데 SecurityConfig를 보면 로그인 성공 시에 기본 이동 페이지는 '/'로 설정되어 있는데, user페이지로 접근을 시도하고 로그인을 했더니 user페이지로 이동을 하였다.
				.loginPage("/loginForm")
                // 로그인 관련 추가
                .loginProcessingUrl("/login")
                // 로그인 성공시 이동할 페이지
                .defaultSuccessUrl("/");
  • 여기서 defaultSuccessUrl은 '/loginForm'로 접근을 하여 로그인을 성공했을 때 이동할 페이지이다.
profile
꾸준히 하자!

0개의 댓글