목적 | 로그인, 세션 트래킹 |
스프링 이전 | HttpSession, Cookie |
스프링 적용 | 스프링 시큐리티 |
스프링 시큐리티 중 Spring Web Security에 구현되어있는 기능
- 로그인
- 자동 로그인
- 로그인 후 페이지 이동
- 그 외, HttpSession과 Cookie로 구현했던 기능들을 자동으로 처리
스프링 시큐리티 기본 설정
- 경로 제외 설정
- 로그 레벨 설정
- 정적 자원 필터 제외
1. 의존성 추가
//스프링 웹 시큐리티
implementation 'org.springframework.boot:spring-boot-starter-security'
2. 스프링 시큐리티 설정
- 스프링 시큐리티 설정의 경우 application.properties 설정보다 자바 코드를 이용하는 경우가 많다.
- 따라서 별도의 Config클래스 작성
(1) CustomSecurityConfig 클래스 작성
package org.zerock.b01.config;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Configuration;
@Log4j2
@Configuration
@RequiredArgsConstructor
public class CustomSecurityConfig {
}
스프링 시큐리티 기본값
스프링 시큐리티 기본 유저 : user
스프링 시큐리티 기본 비밀번호 출력
스프링 기본값 | |
유저 | user |
비밀번호 | 랜덤생성 후 출력 |
별도 설정 없을 경우 | 모든 자원에 권한과 로그인 여부 확인 |
로그인 하지 않도록 별도 설정 | SecurityFilterChain 객체 반환 메서드 작성 |
(2) 로그인 하지 않도록 설정하는 filterChain() 메서드
-모든 사용자가 모든 경로에 접근
-추후에 경로를 제한해야한다.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
return http.build();
}
(3) application.properties에 스프링 시큐리티 로그 레벨 설정
-스프링 시큐리티는 웹에서 사용하는 필터로 동작
-단계별로 동작하는 수많은 필터들이 존재하므로, 로그레벨을 낮춰서 에러를 출력하도록 설정한다.
logging.level.org.springframework=info
logging.level.org.zerock=debug
logging.level.org.springframework.security=trace
(4) 정적 자원의 처리
-기본값으로 css 파일 / js 파일 등에도 필터가 적용
-정적 파일에 시큐리티 적용을 해제해야 한다.
@Bean
public WebSecurityCustomizer webSecurityCustomizer(){
log.info("---web configure---");
//정적 파일을 시큐리티 설정에서 제외하는 설정
return (web) -> web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());
//: 해당 요청을 필터 적용에서 제외한다.
}
- ignoring()을 통해, 필터에서 제외할 패턴 설정하는 방법
- mvcMatchers
- antMatchers
- regexMatchers
- requestMatchers
스프링 시큐리티의 핵심 원리
인증과 인가/권한
- 인증(Authentication)
- 자신을 증명하기 위해 자신의 정보를 제공한다.
- ex) Login
- 인가(Authorization)
- 인증된 사용자가 접근 가능한 권한이 있는지 확인한다.
- ex) 관리자 페이지
인증 처리 단계
- 사용자 아이디로 먼저 아이디에 해당하는 사용자 정보 로딩
- 로딩된 사용자의 정보로 패스워드 검증(Validation)
미리 구현된 인증 제공자 (Authentication Provider)를 이용해 인증처리를 수행하지만,
-> 일반적으로 인증 제공자는 그대로 사용하며, 인증 처리를 담당하는 객체를 커스터마이징해 사용한다.
UserDetailsService
인증 처리 객체 구현을 위한 인터페이스
UserDetailsService의 loadUserByUsername() 메서드
: 실제 인증을 처리하기 위해 호출되는 메서드로 아이디를 이용해 사용자 정보를 불러오는 메서드이다.
인증 구현 단계
- 시큐리티 설정 클래스에서 로그인 처리 추가
- UserDetailService 인터페이스 구현
- username을 이용해 사용자 정보를 불러온다.
- 불러온 사용자 정보의 패스워드를 검증한다.
1. CustomSecurityConfig의 filterChain()메서드에 로그인 메서드 호출 추가
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
log.info("---configure---");
//로그인 처리
http.formLogin();
return http.build();
}
2. security 패키지를 추가하고, UserDetailService 인터페이스를 구현한 CustomUserDetailsService 클래스 작성
-해당 유저 이름에 해당하는 정보가 존재하면, 확인된 유저 이름 출력하는 loadUserByUsername 메서드
-UserDetails 인터페이스 반환
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("loadUserByUsername : "+username);
return null;
}
3. UserDetails 반환 타입
-해당 인터페이스 객체를 이용해 패스워드를 검사 후, 사용자 권한을 확인하는 방식으로 동작
- UserDetails 인터페이스의 추상 메서드 목록
- getAuthorities() : 모든 인가 정보 반환
- getPassword() / getUserName()
- isAccountNonExpired() / isAccountNonLocked()
- isCredentialNonExpired()
- isEnabled()
(1) CustomUserDetailsService에서 UserDetails 인터페이스의 구현체를 반환하는 메서드 작성
-스프링 시큐리티가 제공하는 UserDetails 인터페이스 구현한 User 클래스 이용해 임시 로그인 처리 구현
-빌더 방식을 지원으로 User 클래스 반환
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("loadUserByUsername : "+username);
UserDetails userDetails = User.builder()
.username("user1")
.password("1111")
.build();
return userDetails;
}
-> Password는 반드시 인코딩 필요
(2) PasswordEncoder 작성
-PasswordEncoder 인터페이스를 구현한 BCryptPasswordEncoder 클래스 반환하는 메서드 작성
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
(2-1) CustomSecurityConfig에서 PasswordEncoder 확인
-BCryptPasswordEncoder 생성해 동작 확인
@Log4j2
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
//패스워드 인코더 의존성 주입
private final PasswordEncoder passwordEncoder;
public CustomUserDetailsService(){
this.passwordEncoder=new BCryptPasswordEncoder();
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("loadUserByUsername: " + username);
// return null;
UserDetails userDetails = User.builder()
.username("user1")
//.password("1111")
.password(passwordEncoder.encode("1111"))
//권한 부여 필수
.authorities("ROLE_USER")
.build();
return userDetails;
}
}
4. 특정 경로에 시큐리티 적용
-어노테이션을 이용해 권한 체크
-코드로 작성하면 나중에 관리의 어려움이 있기 때문에, 어노테이션으로 이용하는 방식을 사용한다.
인증과 권한 여부에 따른 분류
- 로그인 여부 관계 없이 :목록
- 로그인이 필요한 작업 : 글쓰기
- 로그인과 인가 둘다 필요 : 관리자 페이지
-@EnableGlobalMethodSecurity : 어노테이션으로 권한을 설정하는 어노테이션
- prePostEnabled 속성 : @PreAuthorize, @PostAuthorize 사용 가능
- securedEnabled 속성 : @Secured 사용 가능
- jsr250Enabled 속성 : @RoleAllowed 사용 가능
(1) CustomSecurityConfig 클래스에 어노테이션 추가
-prePostEnabled =true 설정으로, 사전 혹은 사후의 권한을 체크하는 @PreAuthorize, @PostAuthorize 어노테이션을 사용한다.
package org.zerock.b01.config;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.zerock.b01.security.CustomUserDetailsService;
@Log4j2
@Configuration
//사전 사후에 권한을 체크하는 속성을 위한 어노테이션
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class CustomSecurityConfig {
//의존성 주입
private final CustomUserDetailsService customUserDetailsService;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
log.info("---configure---");
//로그인 처리
http.formLogin();
return http.build();
}
@Bean
public WebSecurityCustomizer webSecurityCustomizer(){
log.info("---web configure---");
//정적 파일을 시큐리티 설정에서 제외하는 설정
return (web) -> web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());
//: 해당 요청을 필터 적용에서 제외한다.
}
}
(2) 게시물 등록 URL 매핑 시, USER 권한 체크하도록 BoardController의 registerGet()에 어노테이션 추가
@Controller
@RequestMapping("/board")
@Log4j2
//사전 사후에 권한을 체크하는 속성을 위한 어노테이션
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class BoardController {
//사전에 권한 체크하도록 추가
@PreAuthorize("hasRole('USER')")
@GetMapping("/register")
public void registerGet(){
}
}
로그인 하지 않는 사용자가 게시물 등록 시도시
- '/board/register' 호출
- @PreAuthorize에 걸려서 로그인 페이지로 이동
- 권한을 가진 사용자인지 체크
- 권한을 가진 사용자면 인가를 부여하고, @PreAuthorize에서 hasRole('USER')는 true가 된다.
- 스프링 시큐리티가 어떤 페이지에서 리다이렉트되었는지 저장
- 로그인 후에, 저장된 페이지로 자동 이동
@PreAuthorize / @PostAuthorize의 속성은 표현식으로 사용된 형태
표현식 | 설명 |
authenticated() | 인증된 사용자들만 허용 |
permitAll() | 모두 허용 |
anonymous() | 익명의 사용자 허용 |
hasRole(표현식) | 특정한 권한이 있는 사용자 허용 |
hasAnyRole(표현식) | 여러 권한 중 하나만 존재해도 허용 |
5. 커스텀 로그인 페이지
-커스터마이징 가능한 별도의 로그인 페이지 적용
(1) CustomSecurityConfig의 filterChain() 메서드에 별도의 로그인 사용하도록 설정
-별도의 로그인 페이지는 사용자를 통한 로그인을 위해 "/member/login"으로 설정한다.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
log.info("---configure---");
//별도의 로그인 페이지를 이용해 로그인 처리하도록 변경
http.formLogin().loginPage("/member/login");
return http.build();
}
(2) 사용자 로그인을 위해 MemberController 작성
-로그인 페이지 URL 매핑을 위한 loginGet() 메서드
- 메서드 명 : loginGET
- 파라미터 : error, logout
- 특징 : 로그인 문제 발생 시, 로그아웃 시 사용하기 위한 파리미터를 전달한다.
package org.zerock.b01.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/member")
@Log4j2
@RequiredArgsConstructor
public class MemberController {
@GetMapping("/login")
//로그인 과정에서 문제 발생 시, 로그아웃 시 사용하기 위한 파라미터
public void loginGET(String error, String logout){
log.info("login get...");
log.info("logout : "+logout);
}
}
(3) 별도의 로그인 페이지 작성
-사용자를 통한 로그인을 위해 /member/login.html 작성
- <form> 태그의 action 속성값은 '/member/login'으로 동일한 경로 지정 후, POST 방식으로 전송
- <input> 태그의 name 속성값은 username과 password로 지정되어 있다.
(추후에 password는 type속성값을 이용하도록 변경)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="description" content="" />
<meta name="author" content="" />
<title>Simple Sidebar - Login</title>
<!-- Favicon-->
<link rel="icon" type="image/x-icon" th:href="@{/assets/favicon.ico}" />
<!-- Core theme CSS (includes Bootstrap)-->
<link th:href="@{/css/styles.css}" rel="stylesheet" />
</head>
<body class="align-middle" >
<div class="container-fluid d-flex justify-content-center" style="height: 100vh">
<div class="card align-self-center">
<div class="card-header">
LOGIN Page
</div>
<div class="card-body">
<form id="registerForm" action="/member/login" method="post">
<div class="input-group mb-3">
<span class="input-group-text">아이디</span>
<input type="text" name="username" class="form-control" placeholder="USER ID">
</div>
<div class="input-group mb-3">
<span class="input-group-text">패스워드</span>
<input type="text" name="password" class="form-control" placeholder="PASSWORD">
</div>
<div class="my-4">
<div class="float-end">
<button type="submit" class="btn btn-primary submitBtn">LOGIN</button>
</div>
</div>
</form>
</div><!--end card body-->
</div><!--end card-->
</div>
</body>
</html>
(4) CSRF 토큰을 요구하는 스프링 시큐리티
-스프링 시큐리티는 GET 방식을 제외한 요청에 CSRF 토큰을 요구한다.
CSRF 토큰 (Cross-Site Request Forgery)
- 크로스 사이트 간 요청 위조로 권한이 있는 사용자를 통해 사용자 모르게 요청 전송하는 공격
: CSRF 토큰을 이용해 사용자가 사이트 이용시 매번 변경되는 문자열을 생성하고, 이를 요청시에 검증해 막는다. - 로그인 페이지 화면 내부에 '_csrf'라는 CSRF 토큰을 사용한다.
- Ajax로 POST 방식 이용시에도 추가 작업이 필요하다.
CSRF 토큰 비활성화
-CustomSecurityConfig 클래스에 CSRF 비활성화 설정
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
log.info("---configure---");
//별도의 로그인 페이지를 이용해 로그인 처리하도록 변경
http.formLogin().loginPage("/member/login");
//CSRF 토큰 비활성화 설정
http.csrf().disable();
return http.build();
}
-> POST 방식의 로그인 처리 없이도 스프링 시큐리티가 로그인을 대신 수행한다.
'Server Programming > Spring Boot Backend Programming' 카테고리의 다른 글
8장-3. 회원 데이터 처리 (+ UserDetailsService, enum, MSA) (1) | 2022.12.14 |
---|---|
8장-2. 로그아웃과 자동 로그인 처리 (+ 인증된 사용자 처리, currentUser, AccessDeniedHandler) (0) | 2022.12.14 |
7장-5. 이미지 추가를 위한 컨트롤러와 화면 처리 (+ 파일명에 언더바가 들어간 경우 에러 발생) (0) | 2022.12.11 |
7장-4. 이전 프로젝트에 이미지 추가 (0) | 2022.12.10 |
7장-3. 일대다 연관관계의 N+1 문제와 @BatchSize (0) | 2022.12.09 |