스프링 시큐리티(6 버전)

정의

1) 웹 애플리케이션의 보안을 담당하는 프레임워크
2) 회원가입, 로그인, 로그아웃
3) 인증, 권한
4) 스프링 시큐리티는 인증과 권한의 과정이다.

인증, 권한

  1. 두 가지 단어가 핵심.
  2. 인증(Authentication)
    • 사용자의 신원을 확인하는 과정
    • 로그인 여부
  3. 권한(Authorization)
    • 로그인한 사람에게 특정 리소스에 접근할 수 있는 권한이 있는지 확인

동작 원리

  • 스프링 시큐리티는 크게 보면 사용자 인증
    -> 권한 검사
    -> 요청 승인/거부 과정으로 진행
  1. 사용자가 로그인 요청
  2. Spring Security의 필터 체인이 요청 처리
  3. 인증 성공시 SecurityContext에 저장
  4. 사용자 권한 체크
  5. 요청 승인 또는 거부

시큐리티 환경설정

  1. build.graddle에 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
testImplementation'org.springframework.security:spring-security-test'
  1. src/main/java/com/gn/mvc아래에 security패키지 생성
  2. security패키지에 스프링 시큐리티 설정을 관리하는 WebSecurityConfig 클래스 생성
    • @Configuration // 스프링이 읽는 환경 파일입니다~ 라는 뜻
    • @EnableWebSecurity // 스프링 시큐리티 쓸거에요~ 라는 뜻
  • 정적인 리소스 처리하지 않도록 주의
  • 스프링 시큐리티 만들때부터 static 아래의 파일들은 읽지 않도록 Security 비활성화하는 코드 필요
	// 1-1. 요청중에 정적인 리소스가 있는 경우 -> Security 비활성화
	@Bean // security가 언제든 읽힐 수 있는 상태로 만든다. @Bean 쓰면 자동으로 public 처리 된다.
	WebSecurityCustomizer configure() {
		// 람다식 이라고 부른다. 화살표 함수랑 똑같다.
		// ignoring 무시할거다! 무엇을? 뒤의 url들을
		// requestMatchers는 특정 url을 정해주는 것이다.
		return web -> web.ignoring()
				.requestMatchers(PathRequest.toStaticResources().atCommonLocations());
	}

람다식

  • 함수를 간결하게 표현
  • JavaScript의 화살표함수와 유사
    - 입력값 -> 수행할 동작
    - (parameters) -> {expression}
  • Spring Security의 환경설정 코드를 구성할때 주로 사용

Spring Security

  • authorizeHttpRequests 메소드에 전달되는 객체를 requests라고 이름 지어줌
  • test를 위해 .anyRequest().permitAll()) 를 적용 해놨으나 실제로는 아래의 코드를 적용할 것.
http.userDetailsService(customUserDetailsService)
		.authorizeHttpRequests(requests -> requests.requestMatchers("/login", "/signup", "/logout", "/").permitAll().anyRequest().permitAll())

예시 코드

	// 1. 특정 요청이 들어왔을 때 어떻게 처리할 것인가
	@Bean
	SecurityFilterChain filterChain(HttpSecurity http, UserDetailsService customUserDetailsService) throws Exception {
		// http 방식으로 /login, /signup, /logout은 일단 허락. 나머지 부분으로 갈때는 인증 받은 분(authenticated)만 받아주세요. 로그인은 로그인 페이지로, 성공하면 /board로. 로그아웃할때는 인증정보 지워주고, 로그아웃 성공하면 로그인쪽으로, 로그아웃 성공하면 세션 정보를 싹 날려주세요.
		http.userDetailsService(customUserDetailsService)
		.authorizeHttpRequests(requests -> requests
//				.requestMatchers("/login", "/signup", "/logout", "/", "/**").permitAll()
//				.anyRequest().authenticated())
				.anyRequest().permitAll())
		.formLogin(login -> login.loginPage("/login")
								.successHandler(new MyLoginSuccessHandler())
								.failureHandler(new MyLoginFailureHandler()))
		.logout(logout -> logout.clearAuthentication(true)
							.logoutSuccessUrl("/login")
							.invalidateHttpSession(true));
		return http.build();
	}

객체 생성

  • 스프링 시큐리티를 도입하여 사용하기 위해서 Security가 사용할 객체 생성
  • 사용자 정보를 제공하는 별도의 클래스 필요함.
  • 다른 곳에서도 이 정보를 써야하니 변환하는 뭔가가 필요?
  • UserDetails(스프링이 사용하는 사용자 정보 객체)를 구현한 구현체
    -> implements UserDetails
  1. MemberDetails 생성
package com.gn.mvc.security;

import java.util.Collection;
import java.util.List;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import com.gn.mvc.entity.Member;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

//UserDetails(스프링이 사용하는 사용자 정보 객체)를 구현한 구현체
@RequiredArgsConstructor
@Getter
public class MemberDetails implements UserDetails {
	
	private static final long serialVersionUID = 1L;
	// 멤버 Entity를 가져와서 쓴다?
	private final Member member;
	
	public Member getMember() {
		return member;
	}

	// 사용자 권한 설정
	// Collection 여러개의 권한을 가질 수 있으니깐.
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return List.of(new SimpleGrantedAuthority("user"));
	}

	// 사용자의 비밀번호 반환
	@Override
	public String getPassword() {
		return member.getMemberPw();
	}

	// 사용자 이름 반환
	@Override
	public String getUsername() {
		// 아이디로서 쓸 수 있는 정보
		return member.getMemberId();
	}
	
//	이 아래는 추가로 넣을 수 있는 것들
//	계정 상태 관리
//	is~ 로 시작하는 메소드. boolean 타입으로 반환
	
	// 계정 만료 여부 반환 메소드
	// 임시 계정, 비활성화된 계정(퇴사 상태 등)
	@Override
	public boolean isAccountNonExpired() {
//		이런식으로 재직 여부 판단하기도 가능!
//		if(member.getExpired().equals("Y")) {
//			return false;
//		} else {
//			return true;
//		}
//		이런식으로 재직 여부 판단하기도 가능!
		return true;
	}
	
	// 계정 잠금 여부
	// 비밀번호 5번 틀리면 -> 10분간 로그인 금지
	@Override
	public boolean isAccountNonLocked() {
		// 쓸 수 있는 계정인가? 언제부터 몇 번 틀렸는가?
//		if(member.getLockedDate() + 10 > 현재시간 -> isAfter 써도 될듯) {
//			return false;
//		} else {
//			return true;
//		}
		// 쓸 수 있는 계정인가? 언제부터 몇 번 틀렸는가?
		return true;
	}
	
	// 패스워드 만료 여부
	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}
	
	// 계정 사용 가능 여부
	@Override
	public boolean isEnabled() {
		return true;
	}
	
}
  1. MemberDetailService 생성
package com.gn.mvc.security;

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 com.gn.mvc.entity.Member;
import com.gn.mvc.repository.MemberRepository;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class MemberDetailsService implements UserDetailsService{
	
	private final MemberRepository repository;
	
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		Member member = repository.findByMemberId(username);
		if(member == null) {
			throw new UsernameNotFoundException("존재하지 않는 회원입니다.");
		}
		return new MemberDetails(member);
	}

}

회원가입 기능

바로 회원가입이 안 되는 이유!
- CSRF 공격을 차단
- Cross-Site Request Forgery(사이트 간 요청 위조)의 줄임말
- 사용자 모르게 악성 요청을 보내도록 유도하는 공격 방식
- 나도 모르게 내 계정으로 해커가 요청을 보내는 것
- 스프링의 CSRF 보호

POST, PUT, DELETE 요청 일단 차단
- GET 요청은 허용되지만, 데이터를 변화하는 요청은 기본적으로 차단(POST)
해결방법
- CSRF 보호를 비활성화하기(보안에 취약해짐)
- CSRF 토큰을 HTML에 추가하여 인증

코드 수정

  1. nav.html의 코드 수정
<a th:href="@{/signup}">회원가입</a>
  1. MemberController회원가입 화면 전환
@GetMapping("/signup")
public String createMemberView(){
	return "member/create";
}
  1. fetch 코드 수정
fetch("/signup",{
				method : 'post',
				body : payload
			})...
  1. MemberService 클래스에 암호화 코드 추가
package com.gn.mvc.service;

import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import com.gn.mvc.dto.MemberDto;
import com.gn.mvc.entity.Member;
import com.gn.mvc.repository.MemberRepository;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class MemberService {
	...
	private final PasswordEncoder passwordEncoder;
	
	public MemberDto createMember(MemberDto param) {
		param.setMember_pw(passwordEncoder.encode(param.getMember_pw()));
		Member entity = param.toEntity();
		...
	}
}

CSRF 관련 코드 수정

HTML에 토큰 추가
1. templates/include.layout.html의 태그 내부에 csrf관련 meta 태그 추가

<head>
	...
<meta id="_csrf" name="_csrf" th:attr="content=${_csrf.token}"/>
<meta id="_csrf_header" name="_csrf_header" th:attr="content=${_csrf.headerName}"/>
</head>
<meta id="_csrf" name="_csrf" th:attr="content=${_csrf.token}">
  • 로그인한 사람의 브라우저의 정보를 가지고 요청을 보내는 것이다~ 라는 뜻
<meta id="_csrf_header" name="_csrf_header" th:attr="content=${_csrf.headerName}">
  • 실제 이 기능을 사용하는 사용자와 위조 요청을 판단할 수 있다.

2.templates/member/create.html 아래 fetch 헤더 부분에 csrf 토큰값을 받아오는 코드 추가

if(vali_check == false){
	alert(vali_text);
} else{
	const payload = new FormData(form);
	fetch("/signup",{
		method : 'post',
		headers: {
              'header': document.querySelector('meta[name="_csrf_header"]').content,
              'X-CSRF-Token': document.querySelector('meta[name="_csrf"]').content
		},			
		body : payload
	})
	.then(response => response.json())
	.then(data => {
		alert(data.res_msg);
		if(data.res_code == 200){
			location.href="/";
		}
	})
}

form 태그의 post 방식 속성 추가
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">

fetch 비동기 통신 post 방식 - 두번째 매개변수(headers:{})에 옵션을 넣는다.
SpringSecurity를 사용할 때 get 방식을 제외한 부분은 모두 csrf 토큰 정보가 필요하다.


  • 스프링 시큐리티 로그인 법칙
설정 항목 필수값
요청 URL /login
요청 방식 POST
아이디 input의 name속성 username
비밀번호 input의 name 속성 password
  • login.html의 form 태그 수정
<form name="login_form" action="/login" method="post">
	<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
	<input type="text" name="username" placeholder="아이디"> <br>
	<input type="password" name="password" placeholder="비밀번호"> <br>
	<input type="submit" value="로그인"> 
</form>

post 방식으로 /login 받는 메소드가 없어야함!!!
-> 이대로 실행하면 오류 뜬다. (UserDetailsService returned null, which is an interface contract violation)
로그인 흐름 확인!

스프링 시큐리티의 RequestCache가 로그인 전에 가고자 했던 URL을 기억해서 그곳으로 이동

profile
함께 공부해요!

0개의 댓글