🤔 시작하기 전에
로그인, 로그아웃 기능 구현을 위해 SpringMVC의 Interceptor를 활용하여 구현하려고 했습니다.
하지만 관련 방법들을 찾아보면서 스프링 시큐리티를 통해 인증과 인가에 대한 부분을 좀 더 안전하고 손쉽게 처리할 수 있었습니다.
사용하기 전에 전체적인 구조, 흐름을 알고 싶어 스프링 시큐리티에 대해 알아보았습니다.
[Spring Security] 스프링 시큐리티 알아보기, 구조(Filter Chain) 및 인증 과정
🤔 스프링 시큐리티? 스프링 기반의 어플리케이션의 보안(인증과 인가)을 담당하는 프레임워크를 말합니다. 인증(Authentication) 로그인 과정처럼 사용자가 시스템에게 자신을 식별할 수 있는 자
average1.tistory.com
✔ 스프링 시큐리티 설정
🐘 build.gradle
스프링 시큐리티와 타임리프에서 스프링 시큐리티를 사용할 수 있도록 의존성을 설정해 줍니다.
//스프링 시큐리티//
implementation 'org.springframework.boot:spring-boot-starter-security' // 스프링 시큐리티
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' // 타임리프
⚙ WebSecurityConfig.java
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig{
private final AuthenticationFailureHandler customFailureHandler;
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/post").hasRole("USER")
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().permitAll()
//.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
//.anyRequest().authenticated()
)
.formLogin( (formLogin) -> formLogin
.loginPage("/members/login")
.loginProcessingUrl("/loginProc")
.usernameParameter("email")
.passwordParameter("password")
.defaultSuccessUrl("/")
.failureHandler(customFailureHandler)
.permitAll()
)
.logout( (logout) -> logout
.logoutUrl("/logoutProc")
.logoutSuccessUrl("/")
.permitAll()
)
.csrf( (csrf) -> csrf.disable() ); //로컬 환경에서 확인을 위해 disable;
return http.build();
}
}
@EnableWebSecurity : 모든 요청에 대해 @Bean을 통해 등록된 SpringSecurity의 필터 체인을 거치도록 합니다.
authorizeHttpRequests
requestMatchers() : 요청 URL을 지정할 때 사용합니다.
permitAll() : 모든 사용자에게 접근을 허용합니다.
hasRole() : 지정된 URL에 대해 접근 권한을 확인합니다.
anyRequest() : 설정되지 않은 모든 요청을 가리킵니다.
authenticated() : 요청이 인증된 사용자에게만 접근을 허용한다는 의미입니다.
formLogin
loginPage() : 로그인 페이지를 설정합니다.
loginProcessingUrl() : 실제로 로그인을 요청할 URL을 설정합니다.
usernameParameter(), passwordParameter() : 요청되는 파라미터의 이름을 설정합니다.
( 기본값 username, password )
defaultSuccessUrl() : 로그인 성공 시 이동할 페이지의 기본값을 설정합니다.
failureHandler() : 로그인 실패 시 이동할 페이지를 설정합니다
logout
logoutUrl() : 로그아웃 요청할 URL을 설정합니다.
logoutSuccessUrl() : 로그아웃 성공 시 이동할 경로를 설정합니다.
csrf
csrf().disable() : csrf에 대한 설정을 할 때 사용합니다, 로컬 환경이기 때문에 disable 함으로써 csrf보호를 해제합니다.
📌 CSRF ( Cross-Site-Request Forgery )
인증된 사용자가 웹 애플리케이션에 특정 요청을 보내도록 유도하는 공격 행위를 말합니다.
생성된 요청이 사용자의 동의를 받았는지 확인할 수 없는 웹 애플리케이션의 CSRF 취약점을 이용합니다.
공격자의 요청이 사용자의 요청인 것처럼 속이는 공격 방식입니다.
CSRF공격을 방어하기 위해서 토큰을 사용합니다.
서버에서는 각 사용자 세션마다 고유한 CSRF토큰을 생성하여 사용자 요청 시 토큰을 함께 전송하여 요청을 검증합니다.
스프링 시큐리티에서는 기본적으로 CSRF토큰을 사용하도록 설정되어 있습니다.
✔ 로그인 기능
loginProcessingUrl을 통해 설정한 URL(/loginProc)에 로그인 요청이 오게 되면, 스프링 시큐리티의 인증 과정을 거쳐 로그인됩니다.
요청된 정보를 검증하기 위해 UserDetailsService의 loadUserByUsername메소드를 실행합니다.
요청된 메소드는 UserDetails를 반환합니다.
UserDetails는 인터페이스로 이를 구현한 MemberDetails를 직접 구현하여 DB에서 조회한 사용자 정보를 설정해 줍니다.
이후 AuthenticationProvider는 반환받은 UserDetails을 검증하여 인증 객체(Authentication)를 생성합니다.
인증 객체는 SecurityContextHolder에 저장됩니다.
인증이 완료되면 사용자는 인증된 세션을 받고 로그인이 완료됩니다.
🔎 loginForm.html
<form method="post" action="/loginProc">
<h1 class="h3 mb-3 fw-normal">로그인</h1>
<div class="form-floating">
<input type="email" name="email" class="form-control" id="floatingInput" placeholder="name@example.com">
<label for="floatingInput">Email address</label>
</div>
<div class="form-floating">
<input type="password" name="password" class="form-control" id="floatingPassword" placeholder="Password">
<label for="floatingPassword">Password</label>
</div>
</form>
🔎 MemberDetails.java
public class MemberDetails implements UserDetails {
private final Member member;
public MemberDetails(Member member){
this.member = member;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collect = new ArrayList<>();
String role = member.getRole().value();
collect.add(new SimpleGrantedAuthority(role));
return collect;
}
@Override
public String getPassword() {
return member.getPassword();
}
@Override
public String getUsername() {
//사용자의 email정보를 통해 로그인 하고 있기때문에 email을 가져옵니다.
return member.getEmail();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
🔎 MemberService.java
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MemberService implements UserDetailsService {
private final MemberRepository memberRepository;
private final BCryptPasswordEncoder passwordEncoder;
...
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
Member member = memberRepository.findByEmail(email);
if( member == null ){
throw new UsernameNotFoundException(email);
}
return new MemberDetails(member);
}
}
✔ 로그아웃 기능
logoutSuccessUrl을 통해 설정한 URL(/loginProc)에 요청이 오게 되면 스프링 시큐리티는 SecurityContextHolder에 저장된 인증 객첸(Authentication)를 포함한 현재 세션의 상태를 삭제합니다.
✔ 타임리프 인증여부에 따라 표시
타임리프에서 스프링 시큐리티를 사용하기 위해
xmlns:sec="http://www.thymeleaf.org/extras/spring-security" 을 추가해 줍니다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<div class="header" th:fragment="bodyHeader">
<!-- navigation bar -->
<nav class="navbar navbar-expand-lg bg-primary" data-bs-theme="dark">
<div class="container-fluid">
...
<a sec:authorize="isAnonymous()" class="float-end pe-3 text-white text-decoration-none" href="/members/login">로그인</a>
<a sec:authorize="isAuthenticated()" class="float-end pe-3 text-white text-decoration-none" th:text="${member}"></a>
<a sec:authorize="isAuthenticated()" class="float-end pe-3 text-white text-decoration-none" href="/logoutProc">로그아웃</a>
</div>
</nav>
</div>
sec:authorize="isAnonymous()" : 사용자의 인증 정보가 없다면(로그인 되지 않았다면, true) 태그를 표시합니다.
sec:authorize="isAuthenticated()" : 사용자가 인증 되었다면(로그인 되었다면, true) 태그를 표시합니다.
✔ 타임리프 사용자 정보 표시
그림처럼 로그인 시 사용자의 정보(사용자의 이메일)를 메인 화면 내비게이션 바에 표시하였습니다.
@AuthenticationPrincipal 어노테이션을 사용하여 MemberDetails객체가 있다면 즉, 로그인되었다면 사용자의 정보를 Model을 통해 보내고 있습니다.
( @AuthenticationPrincipal 현재 사용자의 정보를 주입받기 위해 사용합니다. )
@Controller
public class MainController {
@GetMapping("/")
public String home(@AuthenticationPrincipal MemberDetails member, Model model){
if( member != null){
model.addAttribute("member", member.getUsername());
}
return "index";
}
}
보내진 사용자 정보는 아래와 같이 사용할 수 있습니다.
<a sec:authorize="isAuthenticated()" class="..." th:text="${member}"></a>
🚨 추가
위와 같이 사용할 경우 메인페이지(" / ")의 내비게이션 바에서만 사용자의 이메일을 표시하게 됩니다.
<a sec:authorize="isAuthenticated()" class="..." th:text="${#authentication.principal.username}"></a>
위 코드처럼 authentication객체에 직접 접근합니다.
이렇게 하면 내비게이션 바가 표시되는 모든 페이지에서 인증된 사용자의 정보를 표시할 수 있습니다.
'Project > SpringBoot+JPA 게시판' 카테고리의 다른 글
[SpringBoot + JPA 게시판 만들기] 게시판 페이징 및 N+1문제 최적화 ( Pageable, join fetch ) (0) | 2024.04.25 |
---|---|
[SpringBoot + JPA 게시판 만들기] 게시판CRUD 서비스 로직 작성 + 테스트 코드 추가 (0) | 2024.03.09 |
[SpringBoot + JPA 게시판 만들기] 개발 과정에서 만난 고민 builder?, Spring Data JPA?, DTO? (0) | 2024.02.19 |
[SpringBoot + JPA 게시판 만들기] 프로젝트 생성 및 환경 설정 + 엔티티(Entity) 생성 - 2 (0) | 2024.02.13 |
[SpringBoot + JPA 게시판 만들기] 요구사항 및 프로젝트 설계 - 1 (0) | 2024.02.08 |