
깃허브 링크 : https://github.com/tigerpoint123/jsb-20241210
출처 : https://wikidocs.net/162814
스프링 시큐리티를 통해 로그인 로그아웃 구현을 해봐요
먼저 SecurityConfig 클래스를 수정해줍니다.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests
.requestMatchers(new AntPathRequestMatcher("/**")).permitAll())
.csrf((csrf) -> csrf
.ignoringRequestMatchers(new AntPathRequestMatcher("/h2-console/**")))
.headers((headers) -> headers
.addHeaderWriter(new XFrameOptionsHeaderWriter(
XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN)))
// formLogin 메서드 = 스프링 시큐리티의 로그인 설정 담당
.formLogin((formLogin) -> formLogin
.loginPage("/user/login") // 로그인 페이지 URL
.defaultSuccessUrl("/")) // 로그인 성공 시
;
return http.build();
}
}
이제 로그인 페이지 url을 컨트롤러에서 생성해줍니다.
@GetMapping("/login")
public String login() {
return "login_form";
}
GetMapping으로 url로 들어오는 요청을 처리합니다. 이제 login_form 이름의 html 템플릿을 만들어줍니다.
<div layout:fragment="content" class="container my-3">
<form th:action="@{/user/login}" method="post">
<div th:if="${param.error}">
<div class="alert alert-danger">
사용자ID 또는 비밀번호를 확인해 주세요.
</div>
</div>
<div class="mb-3">
<label for="username" class="form-label">사용자ID</label>
<input type="text" name="username" id="username" class="form-control">
</div>
<div class="mb-3">
<label for="password" class="form-label">비밀번호</label>
<input type="password" name="password" id="password" class="form-control">
</div>
<button type="submit" class="btn btn-primary">로그인</button>
</form>
</div>
스프링 시큐리티에서는 로그인이 실패할 경우 로그인 페이지로 리다이렉트된다.
이때 페이지 매개변수로 error가 전달되기 때문에, 이게 전달되면 사용자에게 ID 혹은 비밀번호를
확인해달라는 문구를 출력하도록 했다. "div th:if="${param.error}">"
이제 회원 정보를 조회해서 로그인하는 방법으로 구현을 해봅시다.
public interface UserRepository extends JpaRepository<SiteUser, Long> {
Optional<SiteUser> findByusername(String username);
//사용자 id로 엔티티를 조회
}
스프링 시큐리티는 권한도 관리하기 때문에, UserRole 클래스도 별도로 생성
@Getter // 권한 상수값을 변경할 필요 없으니 setter는 생략
public enum UserRole {
ADMIN("ROLE_ADMIN"),
USER("ROLE_USER");
UserRole(String value) {
this.value = value;
}
private String value;
}
이제 스프링 시큐리티 서비스 부분을 구현해봅니다.
@RequiredArgsConstructor
@Service
public class UserSecurityService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<SiteUser> _siteUser = this.userRepository.findByusername(username);
if (_siteUser.isEmpty()) {
throw new UsernameNotFoundException("사용자를 찾을수 없습니다.");
}
SiteUser siteUser = _siteUser.get();
List<GrantedAuthority> authorities = new ArrayList<>();
if ("admin".equals(username)) {
authorities.add(new SimpleGrantedAuthority(UserRole.ADMIN.getValue()));
} else {
authorities.add(new SimpleGrantedAuthority(UserRole.USER.getValue()));
}
return new User(siteUser.getUsername(), siteUser.getPassword(), authorities);
}
}
UserDetailsService 인터페이스는 스프링 시큐리티에서 제공하고 있다.
이 친구는 loadUserByUsername 메서드를 구현하도록 강제한다. (진짜 처음 앎). 순서는 이와 같다.
이제 시큐리티 설정에 아래 코드를 추가해 줍니다.
@Bean
AuthenticationManager authenticationManager(
AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
AuthenticationManager 빈은 스프링 시큐리티 인증을 처리합니다.
사용자 인증 시 앞에서 작성한 시큐리티 서비스와 PasswordEncoder를 내부적으로 사용해서
인증과 권한 부여 프로세스를 처리합니다.
이제 홈페이지 상단 바에 로그인 버튼을 추가해줍니다. 이때 로그인이 되면 로그인 버튼은 로그아웃이 돼야 하므로, 이를 수정해줍니다.
<nav th:fragment="navbarFragment" class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
<div class="container-fluid">
<a class="navbar-brand" href="/">SBB</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" sec:authorize="isAnonymous()" th:href="@{/user/login}">로그인</a>
<a class="nav-link" sec:authorize="isAuthenticated()" th:href="@{/user/logout}">로그아웃</a>
</li>
<li class="nav-item">
<a class="nav-link" th:href="@{/user/signup}">회원가입</a>
</li>
</ul>
</div>
</div>
</nav>
sec:authorize="isAnonymous()" 을 통해 비 로그인 상태가 true가 돼서 로그인 버튼이 뜨고,
sec:authorize="isAuthenticated()" 가 true면 로그아웃이 표시가 된다.
로그아웃 기능은 SecurityConfig 클래스를 수정해줍니다.
// 중략
.formLogin((formLogin) -> formLogin
.loginPage("/user/login")
.defaultSuccessUrl("/"))
.logout((logout) -> logout
.logoutRequestMatcher(new AntPathRequestMatcher("/user/logout"))
.logoutSuccessUrl("/")
.invalidateHttpSession(true)) // 로그아웃이 되면 사용자 세션도 삭제.
// 생략