728x90
반응형
SMALL

로그인 기능 구현하기

회원가입 단계에서 SITE_USER 테이블에 정보를 저장했다.

여기에 저장된 데이터로 로그인을 하려면 복잡한 단계가 필요하지만 스프링 시큐리티를 활용하면 쉽게 진행할 수 있다.

 

로그인 URL 등록하기

SecurityConfig.java

package com.mysite.sbb;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@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
					.loginPage("/user/login")
					.defaultSuccessUrl("/"))
			;
		return http.build();
	}
	
	@Bean
	PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
}

.formLogin 메서드를 추가했다. 이 메서드는 스프링 시큐리티의 로그인 설정을 담당하는 부분으로,

설정 내용은 로그인 페이지의 URL은 /user/login이고 성공시 / 루트로 이동함을 의미한다.

 

User 컨트롤러에 URL 매핑

UserController.java

...
@GetMapping("/login")
	public String login() {
		return "login_form";
	}
...

 

로그인 템플릿 작성하기

/templates/login_form.html

<html layout:decorate="~{layout}">
	<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>
</html>

스프링 시큐리티의 로그인이 실패할 경우에 시큐리티 기능으로 로그인 페이지로 리다이렉트된다.

이 때 매개변수로 error가 함께 전달된다.

따라서 error가 전달될 경우 오휴 메시지를 출력한다.

 

 

http://localhost:8080/user/login 확인을 해보자.

 

하지만 우리가 등록했던 데이터로 아직 로그인할 수 없다. 왜냐면 스프링 시큐리티에 어떤 기준으로 로그인을 해야 하는지 설정하지 않았기 때문이다.

 

스프링 시큐리티를 통해 로그인을 수행하는 방법은 SecurityConfig.java와 같은 시큐리티 설정 파일에 사용자ID와 비밀번호를 등록하여 인증하는 방법이 있는데, 우리는 DB에서 회원 정보를 조회하여 로그인하는 방법을 이용할 것이다.

 

이제 DB에서 조회하는 서비스를 만들고, 서비스를 시큐리티에 등록해보자.

 

User 리포지터리 수정하기

사용자 ID를 조회하는 기능을 추가하자.

/user/UserRepository.java

package com.mysite.sbb.user;

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<SiteUser, Long> {
	Optional<SiteUser> findByUsername(String username);
}

 

UserRole 파일 생성하기

스프링 시큐리티는 인증뿐만 아니라 권한도 관리한다.

사용자가 로그인을 하면 ADMIN 또는 USER같은 권한을 줘야 한다.

/user/UserRole.java

package com.mysite.sbb.user;

import lombok.Getter;

@Getter
public enum UserRole {
	ADMIN("ROLE_ADMIN"),
	USER("ROLE_USER");
	
	UserRole(String value) {
		this.value = value;
	}
	
	private String value;
}

 

UserSecurityService 서비스 생성하기

/user/UserSecurityService.java

package com.mysite.sbb.user;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;

@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);
	}
	

}

스프링 시큐리티가 로그인 시 사용할 UserSecurityService는 스프링 시큐리티가 제공하는 UserDetailsService 인터페이스를 구현해야한다.

상속받은 메서드 loadUserByUsername 메서드는 사용자명으로 스프링 시큐리티의 사용자 객체를 조회하여 리턴하는 메서드이다.

사용자명으로 SiteUser 객체를 조회하고, 만약 데이터가 없으면 예외를 발생시킨다.

만약 사용자명이 admin이면 ADMIN권한을 부여하고 이외의 사용자명이면 USER권한을 부여한다.

마지막으로 User객체를 반환하는데, 이 객체는 스프링 시큐리티에서 사용하며 User 생성자에 사용자명, 비밀번호, 권한 리스트를 전달한다.

 

스프링 시큐리티 설정 수정하기

로그인 기능을 완성하기 위해 파일을 수정하자.

SecurityConfig.java

...
@Bean
AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
	return authenticationConfiguration.getAuthenticationManager();
}
...

위처럼 AuthenticationManager 빈을 생성했다. 이 빈은 스프링 시큐리티의 인증을 처리한다.

AuthenticationManager는 사용자 인증 시 앞에서 작성한 UserSecurityService와 PasswordEncoder를 내부적으로 사용해 인증과 권한 부여를 한다.

 

로그인 화면 수정하기

/templates/navbar.html

...
<li class="nav-item">
	<a class="nav-link" th:href="@{/user/login}">로그인</a>
</li>
...

네비게이션 바에서 로그인을 클릭 시 화면이동이 되게끔 url을 변경했다.

 

DB에 없는 사용자나 잘못된 비밀번호를 입력하면 오류 메시지가 발생한다.

 

현재 로그인을 해도 상단바에 아직도 '로그인'이라고 나와있다.

 

로그인 하면 '로그아웃', 로그아웃 상태에서는 '로그인'으로 바뀌어야 한다.

 

아래와 같은 스프링 시큐리티의 타임리프 확장 기능을 사용해보자.

sec:authorize="isAnonymous()" //로그인 되지 않은 경우에 '로그인' 표시
sec:authorize="isAuthenticated()" //로그인된 경우에 '로그아웃' 표시 

 

/templates/navbar.html

...
<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>
...

화면을 다시 실행해보면 로그아웃으로 변경된걸 볼 수 있다.

 

로그아웃 기능 구현하기

위 html에서 로그아웃의 링크를 '/user/logout'으로 지정했다. 

하지만 로그아웃을 누르면 기능 구현이 안되어 있어 404 오류가 발생한다.

 

로그아웃 설정 추가하기

SecurityConfig.java

...
	@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
					.loginPage("/user/login")
					.defaultSuccessUrl("/"))
			.logout((logout) -> logout
					.logoutRequestMatcher(new AntPathRequestMatcher("/user/logout"))
					.logoutSuccessUrl("/")
					.invalidateHttpSession(true))
			;
		return http.build();
	}
...

로그아웃 URL을 '/user/logout'으로 설정하고 로그아웃을 성공했을 경우 '/' 루트 페이지로 이동하도록 했다.

또한 .invaliddateHttpSession(true)를 통해 로그아웃 시 생성된 사용자 세션도 삭제하게 했다.

 

로그아웃하면 메인으로 이동하고 로그인으로 변경된 것을 볼 수 있다.

728x90
반응형
LIST

+ Recent posts