[Springboot] 스프링 시큐리티 로그인&로그아웃

728x90

안녕하세요🖐

 

오늘은 스프링부트 환경에서 스프링 시큐리티를 활용한 로그인&로그아웃을 구현해보겠습니다.

스프링 시큐리티는 내용이 정말 많은 것 같아요😂

그만큼 공부할 부분이 많고, 활용범위가 넓다는 뜻입니다.

그만큼 한번 잘 공부해두면 두고두고 잘 사용할 수 있습니다.

 

스프링 시큐리티는 방대한 프레임워크이다. 따라서 스프링 시큐리티가 내부적으로 어떻게 동작하는지 알기 위해서는 스프링 시큐리티에 대해서 자세히 공부해야 한다. (스프링 시큐리티는 책 1권 분량으로 나올만큼 방대한 프레임워크이다. 실제로 스프링 시큐리티에 대한 책은 많이 출판되었다.) 이 책은 스프링 시큐리티 자체에 대한 내용보다는 활용적인 측면에 대해서만 다룰 것이다. 하지만 개략적인 개념 설명을 추가했으니 이해에 도움이 되기를 바란다.

 

 


 

기능구현

 

1) Config설정

package spring.project.toy;

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.header.writers.frameoptions.XFrameOptionsHeaderWriter;
import org.springframework.security.web.SecurityFilterChain;
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("/mysql-console/**"))) //DB시큐리티 설정등록하기.
			.headers((headers) -> headers
				.addHeaderWriter(new XFrameOptionsHeaderWriter(
					XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN)))
			.formLogin((formLogin) -> formLogin
				.loginPage("/user/login")
				.defaultSuccessUrl("/question/list"));
		;
		return http.build();
	}

	//비밀번호 암호화 bean 등록
	@Bean
	PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
}

전체 코드입니다.

위 코드에서 HttpSecurity 클래스를 사용해서 기본 설정을 할때 로그인 관련된 설정을 추가로 해줍니다.

.formLogin((formLogin) -> formLogin
.loginPage("/user/login")
.defaultSuccessUrl("/question/list"))

formLogin 메소드를 통해서 로그인을 할때 매핑 주소를 잡아주고

defaultSuccessUrl 메소드를 통해 로그인이 완료될시 매핑될 주소를 넣어 줬습니다.

 

조금 이따가 또 config는 추가/수정 할 내용이 생깁니다. 참고만 해주세요!

 

2) Form

<html layout:decorate="~{layout}">

<div layout:fragment="content" class="contatiner 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" calass="form-control">
    </div>
    <button type="submit" class="btn btn-primary">로그인</button>
  </form>
</div>
</html>

로그인을 할 템플릿을 만들어 줍니다. 항상 db에 column 이름이랑 name값은 같게 해야되는 건 알고 있어야 합니다.

스프링 시큐리티의 로그인이 실패할 경우에는 로그인 페이지로 다시 리다이렉트 되는게 원칙입니다. 이 때 페이지 파라미터로 error가 함께 전달된다. 따라서 로그인 페이지의 파라미터로 error가 전달될 경우 "사용자ID 또는 비밀번호를 확인해 주세요." 라는 오류메시지를 출력하도록 했다.

 

로그인 실패시 파라미터로 error가 전달되는 것은 스프링 시큐리티의 규칙이다.

 

3) Controller

위 기본 설정 및 폼을 작성했으니 컨트롤러로 가서 로그인 창으로 갈 수 있게 매핑을 진행합니다.

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

 

위 맵핑 주소로 이동하게 되면

로그인폼이 구현된 위 사진을 볼 수 있습니다.

 

하지만 id랑 password를 입력할 수는 있지만 로그인이 되지 않을 것 입니다.

왜냐하면 스프링 시큐리티에 무엇을 기준으로 로그인을 해야 할지 설정하지 않았기 때문이다. .

저는 회원가입을 통해 회원 정보를 DB에 저장했으므로 DB에서 회원에 id,password를 조회하는 방법을 사용해서

진행해보겠습니다.

 

4) repository

package spring.project.repository;

import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import spring.project.dto.SiteUser;

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

위 Optional클래스에 SiteUser 객체를 담아서 SiteUser 안에있는 username을 찾는 로직을 만들었다.

다음으로 스프링 시큐리티에서 사용자 인증 및 권한을 관리할 클래스를 하나 만들어야 했다.

따라서 인증 후에 사용자에게 부여할 권한이 필요합니다.

다음과 같이 Admin, User 2개의 권한을 갖는 Enum [UserRol](<http://UserRole.java>)e 클래스르 작성했습니다.

 

import lombok.Getter;

@Getter
public enum UserRole {
	ADMIN("ROLE_ADMIN"),
	USER("ROLE_USER");

	UserRole(String value) {
		this.value=value;
	}
	
	private String value;
	
}

UserRole 클래스를 열거 자료형(Enum)으로 작성한 이유는 서로 연관된 변수를

상수로 만들어서 모아두기 위함입니다.

그리고 ADMIN, USER는 절대 변하지 않는 상수이기 때문에 모두 대문자로 처리를 했으며, 객체의 수정이 없어야하니 @setter 는 사용하지않고 @Getter 만 사용했습니다.

 

5) Service

이제 스프링 시큐리티 설정에 등록할 서비스를 만들어 보겠습니다.

package spring.project.service;

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;
import spring.project.dto.SiteUser;
import spring.project.repository.UserRepository;

@Service
@RequiredArgsConstructor
public class UserSecurityService implements UserDetailsService {
	private final UserRepository userRepository;

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		Optional<SiteUser> siteUser1 = this.userRepository.findByUsername(username);
		if(siteUser1.isEmpty()) {
			throw new UsernameNotFoundException("사용자를 찾을 수 없습니다.");
		}
		SiteUser siteUser = siteUser1.get();
		List<GrantedAuthority> authorities = new ArrayList<>();

		if("admin".equalsIgnoreCase(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 인터페이스를 구현받아

그 안에 있는 다양한 메소드를 활용했습니다.

스프링 시큐리티의 UserDetailsService는 loadUserByuserName 메소드를 구현하도록 강제하는 인터페이스 입니다.

위 메소드는 아이디를 기준으로 비밀번호를 조회하여 리턴하는 메소드 입니다.

 

🔔 위 서비스는 로그인 처리의 핵심이므로 중요 🔔

 

loadUserByUsername 메소드는 사용자명을 기준으로 SiteUser 객체를 조회하고 만약 username에 해당하는 데이터가 없을 경우에 UsernameNotFoundException 예외가 나오게 설정했습니다.

그리고 사용자명이 "admin"인 경우에는 ADMIN 권한을 부여하고, 그 이외에는

USER 권한을 부여했다.

그리고 사용자명, 비밀번호, 권한을 입력으로 스프링 시큐리티의 User 객체를 생성하여 리턴했다.

스프링 시큐리티는 loadUserByUsername 메소드에 의해 리턴된 User 객체의 비밀번호가 화면으로부터 입력 받은 비밀번호와 일치하는지를 검사하는 로직을 내부적으로 가지고 있어, 우리가 직접 짜지 않아도 됩니다.

 

위 내용하고 코드를 처음 보면 정말 이해가 안가고 감이 잘 잡히지 않을 것이라고 생각합니다.

저도 이 코드를 제 것으로 만들기 위해 3번이상 보면서 이해를 도왔습니다..

스프링 시큐리티 어렵습니다ㅠㅠ

 

📢 아직 끝난게 아니고 위 코드까지 작성했으면 다시 config에 추가해줘야할 것이 있습니다.

스프링 시큐리티에 AuthenticationManager 빈을 생성해두도록 하겠습니다.

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

맨 처음 설정을 해줬던 SecurityConfig에 추가를 해줘야 합니다.

AuthenticationManager인터페이스를 Bean에 등록 함으로써 스프링 시큐리티를 통한 인증을 할 수 있게 되었습니다.

AuthenticationManager는 사용자 인증시 앞에서 작성한 UserSecurityService를 사용할 수 있습니다.

 

위 과정을 하면 스프링 시큐리티를 사용한 로그인을 할 수 있게 되었습니다.

기존에 세션을 사용한 방식에 비하면 복잡하긴 하지만, 그래도 시큐리티를 더 잘 사용할 수있게 되서 좋았습니다.

그리고 추가적으로 로그인을하면은 form에 있던

 

로그인 버튼은 → 로그아웃 으로 바꾸는 작업을 해줘야 한다.

로그아웃 상태 → 로그인 버튼

 

 

타임리프에서는 사용자의 로그인 여부를

sec:authorize 속성을 통해 확인할 수 있습니다.

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

위 코드를 통하면 로그인시 로그아웃, 로그아웃시 로그인 버튼을 만들 수 있다.

이부분에 대해서는 이해가 아닌 외워야 할 것 같아 그냥 외웠습니다.

 

위에 authorize 값은 true 값으로 고정되었으므로, 위 메소드를 작성하면 참이라는 뜻으로 간주가 된다.

다음으로는 로그인을 했으니, 로그아웃을 진행해보겠습니다.

 

로그인 초반 config 세팅을 해줬던 것 처럼, 로그아웃 또한 config 설정을 통해

로그아웃시 이동알 주소를 매핑 해줍니다.

.logout((logout) -> logout
.logoutRequestMatcher(new AntPathRequestMatcher("/user/logout"))
.logoutSuccessUrl("/question/list")
.invalidateHttpSession(true))

 

로그아웃을 위한 설정을 추가 했습니다.

로그아웃시 이동할 URL을 설정하였고, httpsession을 없앴으므로 이제

정상적으로 로그아웃이 될 것 입니다.

 

이상입니다 스프링 시큐리티를 활용한 로그인&로그아웃 입니다 

지적은 언제나 환영입니다 감사합니다.

 

 

 

**ref : https://wikidocs.net/162255#_1**

728x90