반응형
도서 '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가지를 통해 확인이 가능
-
Principaluser를 식별 하며 ‘누구?’에 대한 정보UserDetailsService에 의해 반환된 UserDetails의 instance
-
authoritiesuser에게 부여된 권한 (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 |