본문 바로가기
Programming/Spring Boot

[Spring Boot] Spring Security 로그인 처리

by 공부합시다홍아 2024. 5. 29.

로그인 처리하기

이전 게시글에서는 회원가입시 권한을 설정하여 로그인을 할 때 어떤 화면으로 그리고 어떤 화면만 접속할 수 있는지 설정하였다.

 

[Spring Boot] Spring Security 설정

스프링 라이브러리 추가//시큐리티 버전은 스프링버전에 따라 사용방법이 완전히 다릅니다.//시큐리티 5버전 => 스프링 부트 2버전 (수업)//시큐리티 6버전 => 스프링 부트 3버전 (문법이 완전 변경

hong-study.tistory.com

이번 게시글에서는 로그인부터 로그아웃까지의 전반적인 과정에 대해서 공부해볼려고 한다.

이전 게시글에서 로그인을 할 때 권한을 설정하여, 비밀번호 암호화까지 했었고, 이어서 로그인 과정에 대해서 알아본다.

로그인을 할 때 URL을 가로채고, 성공 URL을 작성한다.

SecurityConfig.JAVA

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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;

@Configuration // 설정 파일
@EnableWebSecurity // 이 설정 파일을 시큐리티 필터에 등록
public class SecurityConfig {
	
	@Bean
	public BCryptPasswordEncoder encoder() {
		return new BCryptPasswordEncoder();
	}
	
	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
		
		
		http.csrf().disable();
		
		
		http.authorizeRequests( (authorize) -> authorize
				.antMatchers("/all").authenticated()
				.antMatchers("/user/**").hasAnyRole("USER", "ADMIN", "TESTER")
				.antMatchers("/admin/**").hasRole("ADMIN")
				.anyRequest().permitAll()
				)
		; 
		
		http.formLogin()
			.loginPage("/login")
			.loginProcessingUrl("/loginForm") //로그인을 시도하는 경로 작성
			.defaultSuccessUrl("/hello") // 로그인 성공 후 이동하고자 하는 경로 
			.failureHandler(authenticationFailureHandler())
			//.failureUrl("/login?err=true")
			.and()
			.exceptionHandling().accessDeniedPage("/deny") // 권한이 없을 시에 처리
			.and()
			.logout().logoutUrl("/myLogout").logoutSuccessUrl("/all")
			//.usernameParameter("id") //username이 아닌 다른 값으로 변경 가능
			//.passwordParameter(null) // pasword 가 아닌 다른 값으로 변경
			; 
		
		return http.build();
	}
	
	// 인증 실패 핸들러
	@Bean
	public AuthenticationFailureHandler authenticationFailureHandler() {
		CustomAuthenticationFailure custom = new CustomAuthenticationFailure();
		custom.setRedirectURL("/login?err=true");
		
		return custom;
	}
}

위 파일은 securityConfig 파일로, Spring Security 필터에 등록하여 사용한다.
위의 과정을 작성하면, 개발자가 직접 처리하기 보다는 이미 정해진 IOC에 의해 자동적으로 로그인 기능이 수행된다.

http.formLogin()
			.loginPage("/login")
			.loginProcessingUrl("/loginForm") //로그인을 시도하는 경로 작성
			.defaultSuccessUrl("/hello") // 로그인 성공 후 이동하고자 하는 경로 
			.failureHandler(authenticationFailureHandler())
			//.failureUrl("/login?err=true")
			.and()
			.exceptionHandling().accessDeniedPage("/deny") // 권한이 없을 시에 처리
			.and()
			.logout().logoutUrl("/myLogout").logoutSuccessUrl("/all")
			//.usernameParameter("id") //username이 아닌 다른 값으로 변경 가능
			//.passwordParameter(null) // pasword 가 아닌 다른 값으로 변경
			; 
		
		return http.build();
	}
	
	// 인증 실패 핸들러
	@Bean
	public AuthenticationFailureHandler authenticationFailureHandler() {
		CustomAuthenticationFailure custom = new CustomAuthenticationFailure();
		custom.setRedirectURL("/login?err=true");
		
		return custom;
	}

loginProcessingUrl("사용할 주소") , defaultSuccessUrl("로그인 성공시 URL")

http.formLogoin() 메서드를 이용하여 form을 통해 로그인 동작을 수행한다.
이후 loginPage 메서드를 이용해 개발자가 커스터마이징한 화면을 사용할 수 있게 한다.


로그인 처리

로그인을 처리 한 이후에는

loginProcessingUrl("사용할 주소") , defaultSuccessUrl("로그인 성공시 URL")

위 두 메서드를 이용하여, 로그인을 수행할 주소와 성공한 이후의 주소를 입력하여 사용한다.
위 소스에서 loginProcessiongUrl을 이용하여 개발자가 로그인과정을 수행할 주소를 사용한다.

Login.html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<h3>login</h3>
	
	<form action="loginForm" method="post">
		<input type="text" name="username" placeholder="id">
		<input type="password" name="password" placeholder="pw">
		<input type="submit" value="로그인"><br/>
	</form>
	[[${msg}]]
</body>
</html>

위는 로그인 예제 소스이다. 간단하게 테스트하기 위해서 form에 아이디와 패스워드 그리고 버튼을 만들어주었다.

이전에 회원가입을 통해 저장된 정보를 이용해 로그인을 할 경우, from action 속성의 주소를 통해 Controller로 전달되는 것이 기본 원리이지만, Spring Security를 이용하면 Controller를 거치지 않고, 바로 securityConfig의 loginProcessingUrl로 전달된다.
securityConfig의 loginProcessingUrl는 해당 동작을 인식하여 security 과정을 수행하는데 아래와 같이 추가적인 것들이 필요하다.

  시큐리티는 자신만의 특별한 세션객체를 가집니다. (Security Session객체)
시큐리티 세션객체에는 특별한 객체타입만 저장됩니다 (Authentication객체)
Authentication객체 안에는 특별한 객체타입만 저장됩니다 (UserDetails객체)

위의 객체들과 이전에 Spring Security란 무엇인지에 대해 설명할 때 기본 형태에 대해 설명한 것이 있다.

new 시큐리시세션(new 인증객체(new 유저객체)))
new SecurityContextHolder( new Authentication( new UserDetails() ) )

여기서 UserDetails에 해당 하는 객체를 시큐리티에 전달해야하기 때문에 추가 적으로 만들어준다.


MyUserDetails.JAVA

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import com.project.myWeb.command.MemberVO;

public class MyUserDetails implements UserDetails {

	// 로그인해서 조회한 MemberVO 객체 
	private MemberVO memberVO;
	
	
	// 반드시 UserVO 객체를 멤버변수로 담고 생성
	public MyUserDetails(MemberVO vo) {
		this.memberVO = vo;
	}
	
	// role 리턴
	public String getRole() {
		return memberVO.getRole();
	}
	
	// 로그인시 권한을 리턴해주는 함수
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		
		List<GrantedAuthority> list = new ArrayList<>();
		
		// 권한이 여러 개라면 반복문
		list.add( () -> memberVO.getRole() );
		
		return list;
	}

	// 유저의 비밀번호를 반환하는 자리
	@Override
	public String getPassword() {
		
		return memberVO.getPassword();
	}

	// 유저의 아이디를 반환하는 자리
	@Override
	public String getUsername() {
		
		return memberVO.getUsername();
	}

	// 계정 만료 여부 확인 ( true = 네 )
	@Override
	public boolean isAccountNonExpired() {
			
		return true;
	}

	@Override
	public boolean isAccountNonLocked() {
		
		return true; // 계정 락 여부, 락이 걸리지 않았습니까?
	}

	@Override
	public boolean isCredentialsNonExpired() {
		
		return true; // 비밀번호 만료 여부 / true 만료 x
	}

	@Override
	public boolean isEnabled() {
		
		return true; // 계정 사용 여뷰
	}

}

위와 같이 UserDetails 객체를 생성하면 아래와 같은 동작을 수행 할 수 있다.

로그인 시 권한을 리턴해주는 함수

public Collection<? extends GrantedAuthority> getAuthorities() {}

유저의 비밀번호를 반환하는 자리

public String getPassword() {}

유저의 아아디를 반환하는 자리

public String getUsername() {}

계정의 만료 유무 확인 ( 질문 : 계정이 만료되지 않았습니까?? ▶ true : 네 )

public boolean isAccountNonExpired() {}

계정 잠금 유무 확인 ( 질문은 위와 비슷하다 )

public boolean isAccountNonLocked() {}

비밀번호 만료 유무 확인

public boolean isCredentialsNonExpired() {}

계정 사용 유무 확인

public boolean isEnabled() {}

위와 같은 객체들이 알아서 정해지지만 추가적으로 사용하고 싶은 것들을 더 생성할 수 있다.

위 오버라이딩된 메서드들의 기본 값은 false이지만 대부분 초기 단계에 true로 설정한다.


Authentication 객체 만들기 - MyUserDetailsService.JAVA

여기서는 Spring Security의 원리에 대해 잠깐 이해가 가능한데,

loginProcessingUrl()이 호출되면 loadUserByUsername이 자동적으로 호출된다.
import org.springframework.beans.factory.annotation.Autowired;
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 com.project.myWeb.command.MemberVO;
import com.project.myWeb.member.MemberMapper;

//빈으로 등록되어 있으면, 스프링이 UserDetailsService 타입을 찾아서, 사용자가 로그인시
//loadUserByUsername을 실행시킨다.
@Service 
public class MyUserDetailsService implements UserDetailsService{

	@Autowired
	private MemberMapper memberMapper;
	
	//loginProcessingUrl에 로그인 URL을 등록
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		
		System.out.println("사용자가 로그인을 시도함");
		System.out.println("사용자가 입력한 아아디 : " + username);
		
		// 로그인 시도 (비밀번호는 시큐리티가 알아서 비교 후에 처리한다.)
		// 로그인을 하여 vo객체를 얻어온다.
		
		MemberVO vo = memberMapper.login(username);
		
		// 회원정보 존재
		// 비밀번호 비교를 위해, userDeatils 타입으로 리턴
		if(vo != null) {
			return new MyUserDetails(vo); // 스프링 시큐리티가 비밀번호 비교, 권한 확인한 이후 로그인 시도 처리
		}
		// 스프링 시큐리티의 설정한 형식대로, 권한까지 처리를 한다.
		// 아이디가 없거나 비밀번호가 틀리면 login?error 페이지로 기본 이동 ( 설정으로 변경 가능 )
		// 시큐리티는 특별한 세션의 모형을 사용 -> 시큐리티세션(new Authentication(new MyUserDetails() ) ) 모형으로 저장
		
		return null;
	}
	

}

 

이렇게 기본적인 설정을 해주면 일련의 로그인 과정이 수행된다.
이 내부에는 아이디 검사 / 비밀번호 검사 등 자동적인 기능이 다양하게 포함되어 있다.


로그인된 정보 화면에 띄우기 ( 타임리프 기준 )

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"  
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">

타임리프 탬플릿에서 spring-security가 가능한 네임스페이스를 선언한다.

화면

<div sec:authorize="isAuthenticated()"> <!-- 인증 되었다면 true, 아니면 false를 반환합니다. 
if문처럼 사용됩니다. -->
인증된 경우에 실행됩니다.<br/>
    [[${#authentication.principal}]]<br/>
    [[${#authentication.principal.username}]]<br/> <!-- UserDetails객체에서 만든 getter가 사용됩니다. -->
    [[${#authentication.principal.password}]]<br/> <!-- UserDetails객체에서 만든 getter가 사용됩니다. -->
    [[${#authentication.principal.role}]]<br/> <!-- UserDetails객체에서 만든 getter가 사용됩니다. -->
</div>

기본적으로 authentication 내부에 pricipal을 이용하여 로그인한 사용자에 대한 정보를 가져온다.

Controller - Authentication를 선언하면 자동으로 넣어준다.

@GetMapping("/hello")
public String hello(Authentication authentication) {

    //1st 컨트롤러에서 받기
    //System.out.println(authentication);
    if(authentication != null) { //인증이 되지않았다면 null입니다.
        MyUserDetails user = (MyUserDetails)authentication.getPrincipal(); //인증객체 안에 principal값을 얻으면 유저객체가 나옵니다.
        System.out.println(user.getUsername());
        System.out.println(user.getPassword());
        System.out.println(user.getRole());
    }

    //2nd 직접 꺼내기
    //Authentication authehntication = SecurityContextHolder.getContext().getAuthentication();
    //if( authehntication.getPrincipal() instanceof MyUserDetails ) {
    //	MyUserDetails details = (MyUserDetails)authehntication.getPrincipal();
    //	System.out.println(details);
    //}

    return "hello";
}

로그인 실패시 리다이렉트 시키기

@Configuration
@EnableWebSecurity //시큐리티 설정파일을 시큐리티 필터에 등록
public class SecurityConfig {
	
	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		//생략...
		http
		.formLogin()
		.loginPage("/login") //사용자가 제공하는 폼기반 로그인 기능을 사용할 수 있습니다.
		.loginProcessingUrl("/loginForm"); //로그인 페이지를 가로채 시큐리티가 제공하는 클래스로 로그인을 연결합니다.
		.defaultSuccessUrl("/hello") //로그인 성공시 이동될 URL을 적습니다
		.failureUrl("/login?err=true"); //로그인 실패시 이동될 URL을 적습니다 (매개변수 한글 불가)

		return http.build();
	}

}

로그인 실패시 문제를 처리하는 방법은 총 두 가지가 있다.

  • failureUrl
  • failureHandler(authenticationFailureHandler())

failureUrl

로그인 실패시 이동할 URL을 적는데, 뒤에 GET방식으로 주소에 데이터를 포함해서 보내준다.
이렇게 받은 데이터는 아래 Controller를 통해 처리해준다.

@GetMapping("/login")
public String login(@RequestParam(value = "err", required = false) String err,
                                        Model model) {
    if(err != null) {
        model.addAttribute("msg", "아이디 비밀번호를 확인하세요");
    }

    return "login";
}

@RequestParam을 통해 값을 받은 다음, 해당 값을 model에 싫어서 원하는 문구로 사용하여 보내준다.


failureHandler() - 핸들러를 직접만들어 사용하기

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;

public class CustomAuthenticationFailure extends SimpleUrlAuthenticationFailureHandler{

	private String redirectURL;
	
	// 인증 실패시 실행
	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException exception) throws IOException, ServletException {
		// 로그인 실패시에 다양한 작업 처리
		
		response.sendRedirect(redirectURL);
	}

	public String getRedirectURL() {
		return redirectURL;
	}

	public void setRedirectURL(String redirectURL) {
		this.redirectURL = redirectURL;
	}
	
	

}

로그인 실패시 사용될 동작에 대해 커스터마이징이 가능하다.
이 작업을 이용하면, 로그인에 실패함에 따라 어떻게 처리할 것인지에 대한 것들을 고민할 수 있어서 좋은 것 같다.

이 구문을 배우면서 처음 떠올렸던 사실이 이 부분에서 로그인 횟 수가 몇 번 초과시라는 기능이 떠올랐다.

위 커스터마이징된 실패 로직을 구현할 때는 securityConfig에서 Bean으로 등록해줘야 하는데,
failureUrl과 동일하게 리다이렉트 주소에 값을 가지고 보낼 수 있어 Controller는 이전과 동일하게 사용한다.

@Bean
public AuthenticationFailureHandler authenticationFailureHandler() {
    CustomAuthenticationFailure custom = new CustomAuthenticationFailure();
    custom.setRedirectURL("/login?err=true");

    return custom;
}

권한 거부시 리 다이렉트 시키기.

@Configuration
@EnableWebSecurity //시큐리티 설정파일을 시큐리티 필터에 등록
public class SecurityConfig {
	
	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		//생략...
		
		http.exceptionHandling().accessDeniedPage("/deny"); //권한이 없으면 /deny경로로 리다이렉트 됩니다.

		return http.build();
	}

}

권한 거부에 대한 메서드 - exceptionHandling().accessDeniedPage("url")을 사용한다.
나는 여기서 권한 거부 url을 deny를 사용했고, 이 부분을 컨트롤러에서 처리하였다.

@GetMapping("/deny")
public @ResponseBody String deny() {
    return "권한이 없는 사용자입니다";
}

간단하게 responseBody를 이용해 Rest 방식으로 전달받아, 글만 화면으로 표출하였다.


로그아웃 시키기

로그 아웃은 간단한 기능이기도 하지만, 재밌는 기능 중 하나이다.

spring security를 이용하면 사용자가 로그아웃 요청을 만들 필요가 없다.
/logout이 시큐리티 기본 로그아웃 주소이지만, 아래 처럼 원하는 logout 주소를 지정할 수 있다.

@Configuration
@EnableWebSecurity //시큐리티 설정파일을 시큐리티 필터에 등록
public class SecurityConfig {
	
	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		//생략...

		//로그아웃의 처리 - 요청로그아웃주소, 로그아웃 이후에 이동할 경로
		http.logout().logoutUrl("/logout").logoutSuccessUrl("/hello");

		return http.build();
	}

}

이떄 사용하는 메서드는 http.logout().logoutUrl("주소").logoutSuccessUrl("성공 주소") 이다.

  • logoutUrl("주소") : 로그아웃을 실행할 주소를 입력한다.
  • logoutSuccessUrl("성공 주소") : 로그아웃 성공시 이동할 화면의 주소를 사용한다.
728x90