Spring Securiy
를 사용하면 따로 로그인 컨트롤러를 구현하지 않고 스프링 시큐리티에게 양도한다.
SecurityConfig
파일에 있는@Bean SecurityFilterChain configure(HttpSecurity http) throws Exception{ //생략 ... http.authorizeRequests() .antMatchers("/","/user/**","/image/**","/subscribe/**", "/comment/**").authenticated() .loginPage("/auth/signin") //get .loginProcessingUrl("/auth/signin") //POST -> 스프링 시큐리티가 로그인 프로세스 진행 .defaultSuccessUrl("/"); return http.build(); }
코드에 따라
"/", "/user/**"
등으로 오는 요청들은 시큐리티가 가로채서authenticated()
메서드를 실행하고 인증이 안된 사용자면"/auth/signin"
로 보내서 로그인을 하게 만든다. 단, HTTP 메서드가 Get 방식일 때 사용하는 루틴이다.
Spring Security
를 사용하지 않는다면 개발자가 로그인 컨트롤러를 직접 짜야한다.
Spring Security
를 사용하면 .loginProcessingUrl("/auth/signin")
이 로직이 POST 방식일 때 실행이 된다.
그리고 IoC 컨테이너에 있는 UserDetailsService
가 실제 로그인 기능을 실행한다.
개발자가 따로 옵션이나 권한같은걸 주기 위해 UserDetailsService
를 implement 한 PrincipalDetailsService
를 만든다
@Service
public class PrincipalDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return null;
}
}
username 만 가져온 이유는 클라이언트가 username , password 를 서버에게 요청하면 시큐리티가 password 를 자체적으로 비교해주기 때문이다.
loadUserByUsername()
메서드는 정상적으로 return 이 되면 username 과 password로 UserDetails 타입으로 세션을 만들어준다.
loadUserByUseranem()
메서드는 UserDetails 타입이기 때문에 UserDetails
타입으로 리턴하면 되지만, UserDetails
는 인터페이스이기 때문에 익명함수를 생성하게 된다. 그래서 UserDetails
를 implement 한 PrincipalUserDetails
를 만들어준다.
PrincipalDetails 은 실제 로그인 로직이 있는 UserDetails 를 implement 했기 때문에 여러 로직을 Override(재정의) 됐다. 실제 서비스 목적에 따라 수정해주면 된다.
중요한 것은 한개라도 false 가 있으면 안된다!!
public class PrincipalDetails implements UserDetails {
private User user;
public PrincipalDetails(User user){
this.user=user;
}
// 권한 : 권한이 한개가 아닐수 있다.(3개 이상의 권한)
@Override
public Collection<? extends GrantedAuthority> getAuthorities() { //권한을 가져오는 메서드
Collection<GrantedAuthority> collector = new ArrayList<>();
collector.add(()->{return user.getRole();});
return collector;
}
@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() { // 비밀번호가 1년이 지낫는데 변경안한거 아니니 ?
return true;
}
@Override
public boolean isEnabled() { //활성화 되었는지 ?
return true;
}
// 하나라도 false 면 로그인이 안된다.
}
다시 PrincipalDetailsService
로 돌아와서 위에 만든 PrincipalDetails
을 리턴해주면 된다.
@RequiredArgsConstructor
@Service
public class PrincipalDetailsService implements UserDetailsService {
private final UserRepository userRepository;
// 1. 패스워드는 스프링시큐리티가 자체적으로 체크해준다.
// 2. return 이 잘되면 자동으로 UserDetails 타입을 세션으로 만든다.
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User userEntity = userRepository.findByUsername(username);
if(userEntity == null){
return null;
}else{
return new PrincipalDetails(userEntity);
}
}
}
동작순서
1) 클라이언트가 username,password 데이터를/auth/signin
URL 로 POST 요청을 한다.
2) 서버를 감싸고 있는 시큐리티가 요청을 가로채서 username 은PrincipalDetailsService
에 있는loadUserByUsername()
에게 넘겨준다.
3)loadUserByUsername()
에서 인증이 성공하면UserDetails
타입으로 세션에 저장해서 로그인 성공을 응답한다.
보통 웹 브라우저 통신을 할때 세션을 저장하면
Key : Value
타입으로 저장된다. 하지만 스프링 시큐리티는 세션 안에있는SecurityContextHolder
라는 곳에Authentication
객체로 감싸서 저장한다 !!
@AuthenticationPrincipal
어노테이션을 활용하면 된다.@GetMapping("/user/{id}/update")
public String update(@PathVariable int id,
@AuthenticationPrincipal PrincipalDetails principalDetails){
System.out.println("세션 정보 : " + principalDetails.getUser());
return "user/update";
}