Programming/Spring Boot

[Spring Security] 두개의 로그인 페이지(Multiple Login Pages)

빈쿵바라기 2022. 8. 10. 20:07

Spring Boot 프로젝트 진행 중 두개의 로그인 페이지(User, Admin)를 각각 구현해야 하는 이슈가 발생해서 정리하고자 합니다.


1. Maven Dependencies 추가

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>2.7.2</version>
</dependency>
<dependency> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-web</artifactId> 
    <version>2.7.2</version> 
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>2.7.2</version>
</dependency>    
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <version>5.7.2</version>
</dependency>

2. SecurityConfig 설정

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    ... // 내부클래스로 User와 Admin 관련 클래스를 작성할 것이다.
}
  • @Configuration
    • 스프링 설정 클래스를 선언하는 어노테이션. xml 설정을 대체할 수 있는 Java Config로 여러 설정들을 자바 클래스 파일로 사용하겠다는 의미 
  • @EnableWebSecurity
    • 해당 어노테이션을 작성해야지 Security를 활성화 시킬 수 있습니다.
    • 해당 어노테이션을 자세히 보면(아래 코드) WebSecurityConfiguration.class, SpringWebMvcImportSelector.class, OAuth2ImportSelector.class, HttpSecurityConfiguration.class들을 import해서 실행시켜주는 것을 알 수 있습니다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import({ WebSecurityConfiguration.class, SpringWebMvcImportSelector.class, OAuth2ImportSelector.class,
		HttpSecurityConfiguration.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity {

	/**
	 * Controls debugging support for Spring Security. Default is false.
	 * @return if true, enables debug support with Spring Security
	 */
	boolean debug() default false;

}

 

이제 Admin 사용자에 대한 Security 코드입니다. 위에서 만든 SecurityConfig.java 안에 작성합니다.

import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@Order(1)
public class SpringBootAdminSecurityConfiguration extends WebSecurityConfigurerAdapter {
	
	@Override
	public void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.inMemoryAuthentication()
			.withUser("admin").password("{noop}password").roles("ADMIN");
	}

	@Override
	public void configure(HttpSecurity http) throws Exception {
		http
			.antMatcher("/admin/**")
			.authorizeRequests().anyRequest().authenticated()
			.and().formLogin().loginPage("/admin/login")
				.defaultSuccessUrl("/admin/dashboard", true)
			.permitAll()
			.and().logout().logoutUrl("/admin/logout").logoutSuccessUrl("/admin/login");
		http.csrf().disable();

	}
}

configurer 메소드안에 코드는 사용자 권한이 없는 상태에서 /admin으로 시작하는 url로 요청이 올 경우 /admin/login 페이지로 이동 후, 로그인 성공 시 /admin/dashboard로 이동합니다.

User 사용자에 대한 코드SecurityConfig.java 안에 작성합니다.

import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@Order(2)
public class SpringBootUserSecurityConfiguration extends WebSecurityConfigurerAdapter {
	
	@Override
	public void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.inMemoryAuthentication()
			.withUser("user").password("{noop}password").roles("USER");
	}

	@Override
	public void configure(HttpSecurity http) throws Exception {
		http
			.antMatcher("/**")
			.authorizeRequests().anyRequest().authenticated()
			.and().formLogin().loginPage("/login")
				.defaultSuccessUrl("/dashboard", true)
			.permitAll()
			.and().logout().logoutSuccessUrl("/login");
		
		http.csrf().disable();
	}
}

사용자 권한이 없는 상태에서 의에 설정한 /admin을 제외한 모든 url로 요청이 올 경우 /login 페이지로 이동 후, 로그인 성공 시 /dashboard로 이동합니다.

각 정적 클래스에 @Order 주석을 배치하여 URL이 요청될 때 일치하는 패턴을 기반으로 두 클래스가 고려되는 순서를 지정합니다. 두 구성 클래스는 동일한 순서를 가질 수 없습니다.


3. Controller

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping
public class TestController {

	@RequestMapping("/")
	public ModelAndView defaultHome() {
		return new ModelAndView("login");
	}

	@RequestMapping("/login")
	public ModelAndView login() {
		return new ModelAndView("login");
	}

	@RequestMapping("/dashboard")
	public ModelAndView userDashboard() {
		return new ModelAndView("dashboard");
	}

	@RequestMapping("/admin/")
	public ModelAndView admin() {
		return new ModelAndView("admin/login");
	}

	@RequestMapping("/admin/login")
	public ModelAndView adminlogin() {
		return new ModelAndView("admin/login");
	}

	@RequestMapping("/admin/dashboard")
	public ModelAndView admindashboard() {
		return new ModelAndView("admin/dashboard");
	}
}

4. Jsp File

<!-- User Login Form -->
<form method="POST" action="/login">
	User Name : <input type="text" name="username" value="user"/><br><br>
	Password  : <input type="password" name="password" value="password"/><br><br>
	<input type="submit" name="submit"/>
</form>

<!-- Admin Login Form -->
<form method="POST" action="/admin/login">
	User Name : <input type="text" name="username" value="admin"/><br><br>
	Password  : <input type="password" name="password" value="password"/><br><br>
	<input type="submit" name="submit"/>
</form>

주의점

  • 사용자 분류를 anonymous(public), user(private), admin(admin)으로 구분하고자 하면 각 권한별로 WebSecurityConfigurerAdapter파일을 구현해야한다. user와 admin은 위의 예제와 같고, anonymous는 http 설정만 해주면 됩니다.
  • 위의 예제 코드는 http.antMatcher("/url")로 구현하고 있는데, http.authorizeRequests().antMatchers()로 구현하게 되면 모든 http.antMatcher() 설정을 무시하고 http.authorizeRequests().antMatchers() 설정만 작동합니다.
  • 사용자 페이지와 관리자 페이지가 다른 경우, 설정을 따로 하기보다 프로젝트를 구분하는 것이 관리 측면에서 더 나은 방법인 것 같습니다.