본문 바로가기
Spring

스프링부트 로그인 구현하기

by 쌩욱 2021. 8. 14.

https://ysu96.tistory.com/8

 

 

스프링부트 회원가입 구현하기

출처 : 스프링부트 SNS 프로젝트 - 포토그램 만들기 (최주호) 1. SecurityConfig 생성 public class SecurityConfig extends WebSecurityConfigurerAdapter{ @Override protected void configure(HttpSecurity htt..

ysu96.tistory.com

https://ysu96.tistory.com/9

 

스프링부트 회원가입 구현하기 2

출처 : 스프링부트 SNS 프로젝트 - 포토그램 만들기 (최주호) 스프링부트 회원가입 구현하기 1 : https://ysu96.tistory.com/8 스프링부트 회원가입 구현하기 출처 : 스프링부트 SNS 프로젝트 - 포토그램 만

ysu96.tistory.com

출처 : 스프링부트 SNS 프로젝트 - 포토그램 만들기 (최주호)

1. 로그인 페이지

회원가입이 끝났으면 로그인 창으로 가서 아이디와 비밀번호를 입력하게 됩니다.

그러면 해당 페이지는 username, password라는 이름으로 데이터를 담아 POST 요청을 보내도록 합니다.

<!--로그인 인풋-->
<!-- 로그인 정보는 중요한 정보이기 때문에 post 메소드를 사용 (body에 넣어서 사용) -->

<form class="login__input" action="/auth/siginin" method="POST">
	<input type="text" name="username" placeholder="유저네임" required="required" />
	<input type="password" name="password" placeholder="비밀번호" required="required" />
	<button>로그인</button>
</form>

<!--로그인 인풋end-->

2. SecurityConfig 수정

이전에 구현했던 SecurityConfig클래스의 configure 함수를 수정합니다.

이 프로젝트에서 로그인은 스프링 시큐리티에게 위임합니다.

로그인이 필요하면 .loginPage("/~") 함수로 해당 페이지로 GET 요청을 보냈었다면

이제 로그인을 시도하면 .loginProcessingUrl("/~") 함수로 POST요청을 받아 가로챕니다.

그 후 스프링 시큐리티가 로그인 프로세스를 진행합니다.

@EnableWebSecurity // 해당 파일로 시큐리티를 활성화
@Configuration //IoC 
public class SecurityConfig extends WebSecurityConfigurerAdapter{
	
	//비밀번호 암호화
	@Bean  // 이 클래스가 IoC에 등록될 때 @Bean 어노테이션을 읽어서 이 함수를 리턴해 IoC가 들고있음 / 우리는 쓰기만 하면 됨
	public BCryptPasswordEncoder encode() {
		return new BCryptPasswordEncoder();
	}
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		// super 삭제 - 기존 시큐리티가 가지고 있는 기능이 다 비활성화됨.
		http.csrf().disable(); //csrf토큰 비활성화 (디폴트는 활성)
		http.authorizeRequests()
			.antMatchers("/", "/user/**", "/image/**", "/subscribe/**", "/comment/**").authenticated() //이런 주소로 들어오면 인증이 필요해
			.anyRequest().permitAll() //그게 아닌 모든 요청은 허용할게
			.and()
			.formLogin() //로그인 해야지
			.loginPage("/auth/signin") //로그인 페이지 / 인증이 필요하면 여기로 보내  , GET
			.loginProcessingUrl("/auth/signin") //POST, 누군가가 해당 주소로 로그인 요청을 하면 얘가 낚아 챔, 스프링 시큐리티에게 로그인을 위임함
			.defaultSuccessUrl("/"); //로그인이 정상적으로 되면 여기로 보내
	}
}

 

* 스프링 시큐리티 로그인 프로세스 과정 *

1) 시큐리티 설정파일에서 로그인 POST 요청을 계속 기다립니다.

2) 누군가가 로그인을 시도하고 POST요청을 보내면 요청을 낚아챕니다.

3) IoC컨테이너에서 UserDetailsService 라는 애가 정보를 받아 로그인을 진행합니다.


3. UserDetailsService, UserDetails 구현

해당 프로젝트에서는 UserDetailsService를 구현한 PrincipalDetailsService를 따로 만들겁니다.

이 클래스에 @Service 어노테이션을 붙이면 스프링 컨테이너에 원래 있는 UserDetailsService를 대체합니다.

로그인 진행 시 loadUserByUsername 함수를 실행합니다. 저희는 오버라이딩으로 구현합니다.

이 함수의 반환값은 UserDetails 인터페이스임으로 UserDetails를 구현한 PrincipalDetails도 따로 구현합니다.

로그인을 실행하게 되면 아이디와 비밀번호를 확인하고 userEntity를 담은 PrincipalDetatils가 세션으로 생성됩니다.

@RequiredArgsConstructor
@Service //IoC,  스프링컨테이너에 원래 있는 UserDetailsService를 덮어 씌워 대체함
public class PrincipalDetailsService implements UserDetailsService{
	private final UserRepository userRepository;
	
	//로그인을 실행하면 이 함수가 실행됨
	//리턴이 잘되면 자동으로 UserDetails타입을 세션으로 만든다.
	//패스워드는 알아서 체킹하니깐 신경쓸 필요 없음
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		// TODO Auto-generated method stub
		User userEntity = userRepository.findByUsername(username);
		if(userEntity == null) {
			return null;
		}
		else {
			return new PrincipalDetails(userEntity);
		}
		//비밀번호는 시큐리티가 알아서 비교해줌
		
	}

}

 

 

저희는 User 객체를 담을 것이므로 생성자를 만들어주고 권한, 비밀번호, 아이디를 해당 get함수들이 반환하도록 바꿔줍니다.

나머지 함수들은 구현하지 않고 다 true를 반환하도록 하겠습니다.

(하나라도 false를 반환하면 로그인이 되지 않습니다)

@Data
public class PrincipalDetails implements UserDetails{
	private static final long serialVersionUID = 1L;
	
	private User user;

	public PrincipalDetails(User user) {
		this.user = user;
	}
	
	@Override //권한을 가져오는 함수, 권한이 한개가 아닐 수 있어서 콜렉션타입 리턴
	public Collection<? extends GrantedAuthority> getAuthorities() {
		// TODO Auto-generated method stub
		Collection<GrantedAuthority> collector = new ArrayList<>();
		
//		collector.add(new GrantedAuthority() {	
//			@Override
//			public String getAuthority() {
//				// TODO Auto-generated method stub
//				return user.getRole();
//			}
//		});
		
		//람다식 사용
		collector.add(()-> {return user.getRole();});
		
		
		return collector;
	}

	@Override
	public String getPassword() {
		// TODO Auto-generated method stub
		return user.getPassword();
	}

	@Override
	public String getUsername() {
		// TODO Auto-generated method stub
		return user.getUsername();
	}

	
	
	@Override //이 계정이 만료가 되었니? , 아래 함수들 다 false이면 로그인 안됨
	public boolean isAccountNonExpired() {
		// TODO Auto-generated method stub
		return true;
	}

	@Override //이 계정이 잠겼는지?
	public boolean isAccountNonLocked() {
		// TODO Auto-generated method stub
		return true;
	}

	@Override //이 비밀번호..?
	public boolean isCredentialsNonExpired() {
		// TODO Auto-generated method stub
		return true;
	}

	@Override
	public boolean isEnabled() {
		// TODO Auto-generated method stub
		return true;
	}

}

4. 세션 정보 확인하기

UserDetailsService클래스의 loadUserByUsername 함수가 성공적으로 반환되면 새로운 세션을 생성한다고 했습니다.

이 세션을 개발자가 직접 확인하는 방법을 정리하겠습니다.

 

새로운 세션이 생성되면 SecurityContextHolder라는 클래스 안에 정보가 저장됩니다.

저희는 새롭게 생성한 User 정보가 들어있는 PrincipalDetatils 객체를 반환하기 때문에 Principal에 해당 객체가 저장됩니다.

순서 : SecurityContextHolder -> SecurityContext -> Authentication -> Principal

 

@AuthenticationPrincipal 어노테이션을 사용하면 Authentication 객체에 바로 접근할 수 있어 편리합니다.

 

SecurityContextHolder 클래스 구조

 

세션에 접근하는 두 가지 방법을 정리하겠습니다.

1. SecurityContextHolder에서 순서대로 접근하기

2. @AuthenticationPrincipal 어노테이션 사용해 Principal에 바로 접근하기

 

@Controller
public class UserController {

	...
    
	@GetMapping("/user/{id}/update")
	public String update(@PathVariable int id, @AuthenticationPrincipal PrincipalDetails principalDetails) {
		//1.추천
		System.out.println("세션 정보 : "+ principalDetails.getUser());
		
		//2.극혐
		Authentication auth = SecurityContextHolder.getContext().getAuthentication();
		PrincipalDetails mPrincipalDetails = (PrincipalDetails) auth.getPrincipal();
		System.out.println("직접 찾은 세션 정보 : "+ mPrincipalDetails.getUser()); //복잡한 방법, 결과는 위랑 같음
        
		return "user/update";
	} 
}

결과

 

결과는 동일한 것을 확인 할 수 있습니다.

2번 방법은 바로 User정보에 접근할 수 있지만

1번 방법은 Context와 Authentication을 얻고 또 Principal을 얻어야 해서 번거롭다는 특징이 있습니다.