반응형

 

도서 'Spring in Action' 제 5판을 보고 책 내용과 그 이외의 부족한 부분을 채워가며 공부한 내용입니다.


1. 웹 요청 보안 처리

<현재 문제점>

  • 타코를 디자인하거나 주문하기 전 사용자를 인증해야 한다는 것이 타코 클라우드 애플리케이션의 보안 요구 사항
  • 하지만 홈페이지, 로그인 페이지, 등록 페이지는 인증이 되지 않은 모든 사용자가 이용 가능해야함

 

<SecurityConfig>

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
		.authorizeRequests()
		.antMatchers("/design", "/orders")
		.access("hasRole('ROLE_USER')")
		.antMatchers("/", "/**").access("permitAll")
		.and()
		.formLogin()
		.loginPage("/login")
		.and()
		.logout()
		.logoutSuccessUrl("/")
		.and()
		.csrf();
	}
    ....
  • configure() 메서드는 HttpSecurity 객체를 인자로 받음
  • 웹 수준에서 보안을 처리하는 방법을 구성하는데 사용
  • HttpSecurity
    • HTTP 요청 처리를 허용하기 전에 충족되어야 할 특정 보안 조건을 구성
    • 커스텀 로그인 페이지를 구성
    • 사용자가 애플리케이션의 로그아웃을 할 수 있도록 함
    • CSRF 공격으로부터 보호하도록 구성

 

웹 요청 보안 처리

/design과 /orders의 요청은 인증된 사용자에게만 허용되어야 함

이외의 다른 요청은 모든 사용자에게 허용
  • .authorizeRequests()
    • ExpressionInterceptUrlRegistry 객체 반환
    • 해당 객체는 URL 경로와 패턴 및 해당 경로의 보안 요구사항을 구성할 수 있음
  • .antMatchers("/design", "/orders")
  • .access("hasRole('ROLE_USER')")
  • .antMatchers("/", "/**").access("permitAll")
    • /design과 /order의 요청은 ROLE_USER의 권한을 갖는 사용자에게만 허용
    • 이외의 모든 요청은 모든 사용자에게 허용
    • Ant Style Pattern으로 URL을 매핑
    • 웹 개발을 진행하면 URL 매핑을 대부분 Ant Pattern
      • ? : 1개의 문자와 매핑
      • * : 0개 이상의 문자와 매칭
      • ** : 0개 이상의 디렉토리와 파일 매칭(정적 파일들 매칭)
  • .and()
    • 인증 구성 코드와 연결시킨다는 것에 유의
    • 인증 구성이 끝나서 추가적인 HTTP 구성을 적용할 준비가 되었다는 것을 나타냄
    • 새로운 구성을 시작할 떄마다 사용 가능
  • .formLogin()
    .loginPage("/login")
    • .formLogin() -> 커스텀 로그인 폼을 구성하기 위해 호출
    • .loginPage("/login") -> 커스텀 로그인 페이지의 경로를 지정 -> /login 경로에 대한 요청 처리 컨트롤러 제공
@Configuration
public class WebConfig implements WebMvcConfigurer {
	
	@Override
	public void addViewControllers(ViewControllerRegistry registry) {
		registry.addViewController("/").setViewName("home");
		registry.addViewController("/login");  //
	}
}

<login.html>

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
	xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="EUC-KR">
    <title>Taco Cloud</title>
  </head>

  <body>
	<h1>Login</h1>
	<img th:src="@{/images/TacoCloud.png}" />
	
	<div th:if="${error}">Unable to login. Check your username and
		password.</div>
		
	<p>
		New here? Click <a th:href="@{/register}">here</a> to register.
	</p>
	
	<!-- tag::thAction[] -->
    <form method="POST" th:action="@{/login}" id="loginForm">
    <!-- end::thAction[] -->
      <label for="username">Username: </label>
      <input type="text" name="username" id="username" /><br/>

      <label for="password">Password: </label>
      <input type="password" name="password" id="password" /><br/>

      <input type="submit" value="Login"/>
    </form>
  </body>
</html>

 

정리

<특정 URL 접근 시 로그인 하도록 유도하고 나머지는 아무나 사용가능하도록 설정(로그인 가장 먼저 설정)>

http
		.authorizeRequests()
		.antMatchers("/design", "/orders") //특정 URL 접근시 매치
		.access("hasRole('ROLE_USER')")
		.antMatchers("/", "/**").access("permitAll")

<로그인 폼 기본 성정>

.formLogin()
.loginPage("/login")

 

<로그인 경로와 필드 이름을 변경하여 사용>

.and()
.form.login()
.loginPage("/login")
.loginProcessingUrl("/authenticate")
.usernameParameter("user")
.passwordParameter("pwd")
  • 기본적으로 스프링 시큐리티는 /login 경로로 로그인 요청, 사용자 이름 = username, 비밀번호 = password
  • 여기서는 경로를 따로 연결해줌(/authenticate)
  • 사용자 이름과 비밀번호 필드도 새롭게 지정

<사용자가 직접 로그인 페이지로 이동했을 경우 로그인한 후 루트 경로 지정(예를 들어 홈페이지)>

.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/design") // /design에 홈페이지가 들어갈 수 있음
  • 기본적으로 로그인은 하게되면 로그인이 필요하다 판단했을 당시에 사용자가 머물던 페이지로 바로 이동

 

<사용자가 로그인 전에 어떤 페이지에 있었는지와 무관하게 로그인 후에 무조건 /design 페이지로 이동>

 

.and()
.formLogin()
.loginPage("/login")
.defaultSuccessfulUrl("/design", true)
  • defaultSuccessfulUrl 의 두번째 인자를 true로 보냄

<로그 아웃>

.logout()
.logoutSuccessUrl("/")
  • .logout()
    • /logout의 POST 요청을 가로채는 보안 필터 설정
    • 해당 폼에 로그아웃 폼과 버튼을 추가해야 함.
  • .logoutSuccessUrl("/")
    • 로그아웃 후 이동할 페이지 설정

<csrf 활성화>

.and()
.csrf();

 

 

※ hasRole(), permitAll()

요청 경로의 보안 요구를 선언하는 메서드
메서드 하는일
access(String) 인자로 전달된 SpEL표현식이 true라면 접근을 허용
anonymous() 익명의 사용자에게 접근을 허용
authenticated() 익명이 아닌 사용자로 인증된 경우 접근을 허용
denyAll() 무조건 접근 거부
fullyAuthenticated() 익명이 아니거나 또는 remember-me가 아닌 사용자로 인증되면 접근을 허용
hasAnyAuthortiry(String...) 지정된 권한 중 어떤 것이라도 사용자가 갖고 있으면 접근을 허용
hasAnyRole(String...) 지정된 역할 중 어느 하나라도 사용자가 갖고 있다면 접근을 허용
hasAuthority(String) 지정된 권한을 사용자가 갖고 있으면 접근을 허용
hasIpAddress(String) 지정된 IP주소로부터 요청이 오면 접근을 허용
hasRole(String) 지정된 역할을 사용자가 갖고 있으면 접근을 허용
not() 다른 접근 메서드들의 효력을 무효화
permitAll() 무조건 접근을 허용
rememberMe() remember-me(이전 로그인 정보를 쿠키나 데이터베이스로 저장한 후 일정 기간 내에 다시 접근 시 저장된 정보로 자동 로그인 됨)를 통해 인증된 사용자의 접근을 허용

 

※ Spring Security에서 확장된 SpEL

보안 표현식 산출 결과
authentication 해당 사용자의 인증 객체
denyAll 항상 false를 산출
hasAnyRole(역할 내역) 지정된 어느 역할 중 하나라도 해당 사용자가 갖고 있으면 true
hasRole(역할) 지정된 역할을 해당 사용가자 갖고 있다면 true
hasIpAddress(IP 주소) 지정된 IP 주소로부터 해당 요청이 온 것이면 true
isAnonymous() 해당 사용자가 익명 사용자이면 true
isAuthenticated() 해당 사용자가 익명이 아닌 사용자로 인증되었으면 true
isFullyAuthenticated() 해당 사용자가 익명이 아니거나 또는 remember-me가 아닌 사용자로 인증되었으면 true
isRememberMe() 해당 사용자가 remember-me 기능으로 인증되었으면 true
permitAll 항상 true 산출
principal 해당 사용자의 principal 객체

 

※ CSRF(Cross-Site Request Frogery)

크로스 사이트 요청 위조

보안 공격 중 하나

사용자가 웹사이트에 로그인한 상태에서 악의적인 코드(사이트 간의 요청을 위조하여 공격)가 삽입된 페이지를 열면 공격 대상이 되는 웹사이트에 자동으로 폼이 제출되고 이 사이트는 위조된 공격 명령이 믿을 수 있는 사용자로부터 제출된 것이라고 판단하게 되어 공격에 노출
  • CSRF 공격을 막기위한 방법
    • 어플리케이션에서는 폼의 숨김(hidden) 필드에 넣을 CSRF 토큰을 생성할 수 있음
    • 해당 필드에 토큰을 넣은 후 나중에 서버에서 사용
    • 이후 해당 폼이 제출될 때 폼의 다른 데이터와 함께 토큰도 서버로 전송
    • 서버에서는 이 토큰을 원래 생성되었던 토큰과 비교하며, 토큰이 일치하면 해당 요청의 처리가 허용
  • Spring Security
    • 내장된 CSRF 방어 기능 존재
    • CSRF 토큰을 넣을 '_csrf' 라는 이름의 필드를 애플리케이션이 제출하는 폼에 포함시키면 됨
<input type="hidden" name="_csrf" th:value="${_csrf.token}"/>

또는 Thymeleaf를 스프링 시큐리티 dialect와 함께 사용 중인 경우

<form method="POST" th:action="@{/register}" id="registerForm">
//폼의 action에 @만 붙여줘도 자동으로 숨김 필드 생성
  • Rest API서버로 실행되는 어플리케이션인 경우를 제외하고 CSRF 지원을 비활성화하면 안됨
.and()
.csrf()
.disable() //비활성화 됨

 

2. 사용자 인지하기

OrderController에서 (즉, 주문할 때) 주문 폼에 바인딩 되는 Order 객체를 최초 생성할 때 해당 주문을 하는 사용자의 이름과 주소를 주문 폼에 미리 넣을 수 있다면 효과적

사용자 주문 데이터를 데이터베이스에 저장할 때 주문이 생성되는 User와 Order를 연관시킬 수 있어야 함

<Order Class>

@Data
@Entity
@Table(name="Taco_Order")
public class Order implements Serializable {
	
	private static final long serialVersionUID = 1L;
	@Id
	@GeneratedValue(strategy=GenerationType.AUTO)
	private Long id;
	
	private Date placedAt;
	
	@ManyToOne
	private User user;
    
    ....
  • @ManyToOne
    • 한 건의 주문이 한 명의 사용자에게 속한다는 것을 나타냄
    • Order : User = 多 : 1

 

<OrderController Class>

@Slf4j
@Controller
@RequestMapping("/orders")
@SessionAttributes("order")
public class OrderController {
	
	private OrderRepository orderRepo;
	
	public OrderController(OrderRepository orderRepo) {
		this.orderRepo = orderRepo;
	}
	
	@GetMapping("/current")
	public String orderForm(@AuthenticationPrincipal User user,
			@ModelAttribute Order order) {
		if (order.getDeliveryName() == null) {
			order.setDeliveryName(user.getFullname());
		}
		if (order.getDeliveryStreet() == null) {
			order.setDeliveryStreet(user.getStreet());
		}
		if (order.getDeliveryCity() == null) {
			order.setDeliveryCity(user.getCity());
		}
		if (order.getDeliveryState() == null) {
			order.setDeliveryState(user.getState());
		}
		if (order.getDeliveryZip() == null) {
			order.setDeliveryZip(user.getZip());
		}
		return "orderForm";
	}
	
	@PostMapping
	public String processOrder(@Valid Order order, Errors errors, SessionStatus sessionStatus
							, @AuthenticationPrincipal User user) {
		if (errors.hasErrors()) {
			return "orderForm";
		}
		
		order.setUser(user);
		
		orderRepo.save(order);
		sessionStatus.setComplete();
		
		return "redirect:/";
	}
}
  • processOrder()
    • 주문을 저장하는 일을 수행
    • 인증된 사용자가 누군지 결정한 후 Order 객체의 setUser()를 호출해서 해당 유저와 연결해야함
  • orderForm()
    • @AuthenticationPrincipal User user를 통해 user를 통해서 사용자 정보를 가져올 수 있음
    • get 함수를 통해 받을수 있는 값은 받아옴(이름, 주소, 도시, State...)

 

사용자(User) 결정 법

로그인 한 사용자의 정보를 받고 싶을때는 기본적으로 Principal 객체로 받아서 사용
  • Principal 객체를 컨트롤러 메서드에 주입
@PostMapping
	public String processOrder(@Valid Order order, Errors errors, SessionStatus sessionStatus
							,Principal principal) {
		if (errors.hasErrors()) {
			return "orderForm";
		}
		
        	User user = userRepository.findByUsername(principal.getName());
		
        	order.setUser(user);
        
		orderRepo.save(order);
		sessionStatus.setComplete();
		
		return "redirect:/";
	}

 

  • Authentication 객체를 컨트롤러 메서드에 주입
@PostMapping
	public String processOrder(@Valid Order order, Errors errors, SessionStatus sessionStatus
							, Authentication authentication) {
		if (errors.hasErrors()) {
			return "orderForm";
		}
		
        	User user = (User) authentication.getPrincipal():
        
		order.setUser(user);
		
		orderRepo.save(order);
		sessionStatus.setComplete();
		
		return "redirect:/";
	}

 

여기까지 ↑위의 방법들은 잘 동작은 하지만 보안과 관련없는 코드도 존재하게 됨

 

  • SecurityContextHolder를 사용해서 보안 컨텍스트를 얻음
    • 보안 특정 코드가 길지만 컨트롤러 처리 메서드는 물론이고 애플리케이션의 어디서든 사용가능
@PostMapping
	public String processOrder(@Valid Order order, Errors errors, SessionStatus sessionStatus
							, Authentication authentication) {
		if (errors.hasErrors()) {
			return "orderForm";
		}
			
                Authentication authentication = SecurityContextHolder.getContext()   	 ////
            		.getAuthentication();                                            ////
        	User user = (User) authentication.getPrincipal():                        ////
        
		order.setUser(user);
		
		orderRepo.save(order);
		sessionStatus.setComplete();
		
		return "redirect:/";
	}

 

 

  • @AuthenticationPrincipal 어노테이션을 메서드에 지정
    • User객체를 인자로 전달 가능
    • @AuthenticationPrincipal
      • 타입 변환이 필요 없고 Authentication과 동일하게 보안 특정 코드만 가짐
      • UserDetailsService에서 Return한 객체 를 파라메터로 직접 받아 사용
@PostMapping
	public String processOrder(@Valid Order order, Errors errors, SessionStatus sessionStatus
							, @AuthenticationPrincipal User user) {
		if (errors.hasErrors()) {
			return "orderForm";
		}
		
		order.setUser(user);
		
		orderRepo.save(order);
		sessionStatus.setComplete();
		
		return "redirect:/";
	}

 

 

 

 

 

 

 

 

 

 


<추가 정보>

 

※ Principal 객체

컨트롤러의 처리기 메소드에서 자동 파라미터로 주입받을 수 있는 타입 중 하나

다만 가장 구현체의 최상위 인터페이스이기 때문에 이 타입으로 받으면 사용할만한 메소드가 getName() 정도
(ID 정보만 가져다 사용할 수 있다고 보면 됨)
@RequestMapping("/")
	public String main(Principal principal) {

		if (principal != null) {
			System.out.println("타입정보 : " + principal.getClass());
			System.out.println("ID정보 : " + principal.getName());
		}
		return "main";
	}

 

※ Authentication 객체

UsernamePasswordAuthenticationToken 구현체 및 세션 정보를 보관하는 객체에서 필요한 정보를 뽑아내는 메소드를 가지고 있음

따라서 실제로 인증 정보를 사용하기 위해 사용되는 객체 타입이 바로 Authentication

AuthenticationManager.authenticate(Authentication)에 의해 인증된 principal 또는 token

Principal, credentials, authorities을 가지고 있으며 이 3가지를 통해 확인이 가능
  • Principal
    user를 식별 하며 ‘누구?’에 대한 정보
    UserDetailsService에 의해 반환된 UserDetails의 instance
 
  • authorities
    user에게 부여된 권한 (GrantedAuthority 참고) ex) ROLE_ADMINISTRATOR, ROLE_HR_SUPERVISOR와, ROLE_USER 등등..

 

  • credentials
    주체가 올바르다는 것을 증명하는 자격 증명
    일반적으로 암호이지만 인증 관리자와 관련된 암호일 수 있다

 

메서드

  • Object getPrincipal() : 첫 번째 생성자로 주입한 객체 반환
  • Object getCredentials() : 두 번째 생성자로 주입한 객체 반환 
  • Collection<? extends GrantedAuthority> getAuthorities() : 세 번째 생성자인 권한 리스트 객체 반환
  • Object getDetails() : 세션정보를 가진 WebAuthenticationDetails 객체 반환
@RequestMapping("/")
	public String main(Authentication authentication) {

		if (authentication != null) {
			System.out.println("타입정보 : " + authentication.getClass());
			
			// 세션 정보 객체 반환
			WebAuthenticationDetails web = (WebAuthenticationDetails)authentication.getDetails();
			System.out.println("세션ID : " + web.getSessionId());
			System.out.println("접속IP : " + web.getRemoteAddress());

			// UsernamePasswordAuthenticationToken에 넣었던 UserDetails 객체 반환
			UserDetails userVO = (UserDetails) authentication.getPrincipal();
			System.out.println("ID정보 : " + userVO.getUsername());
		}
		
		return "main";
	}

 

※SecurityContextHolder
시큐리티가 인증한 내용들을 가지고 있으며, SecurityContext를 포함하고 있고 SecurityContext를 현재 스레드와 연결

 

3. 각 폼에 로그아웃 버튼 추가하고 사용자 정보 보여주기 

HTML에 로그아웃 버튼 추가하기

...
<form method="POST" th:action="@{/logout}" id="logoutForm">
	<input type="submit" value="Logout"/>
</form>

 <a th:href="@{/design}" id="design">Design a taco</a>
 
 ...

 

 


참고 자료

1. 크레이그 월즈, Spring in Action, Fifth Edition(출판지 : 제이펍, 2020)

반응형

'Spring' 카테고리의 다른 글

Spring 구성 속성 2  (0) 2022.06.02
Spring - 구성 속성  (0) 2022.05.30
Spring Security 3  (0) 2022.05.25
JDBC - 데이터 UPDATE, INSERT, DELETE  (0) 2022.05.23
JDBC - DBMS 조작 기본 지식, SELECT  (0) 2022.05.21

+ Recent posts