
1) 웹 애플리케이션의 보안을 담당하는 프레임워크
2) 회원가입, 로그인, 로그아웃
3) 인증, 권한
4) 스프링 시큐리티는 인증과 권한의 과정이다.
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
testImplementation'org.springframework.security:spring-security-test'
// 1-1. 요청중에 정적인 리소스가 있는 경우 -> Security 비활성화
@Bean // security가 언제든 읽힐 수 있는 상태로 만든다. @Bean 쓰면 자동으로 public 처리 된다.
WebSecurityCustomizer configure() {
// 람다식 이라고 부른다. 화살표 함수랑 똑같다.
// ignoring 무시할거다! 무엇을? 뒤의 url들을
// requestMatchers는 특정 url을 정해주는 것이다.
return web -> web.ignoring()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations());
}
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();
}
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;
}
}
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에 추가하여 인증
<a th:href="@{/signup}">회원가입</a>
@GetMapping("/signup")
public String createMemberView(){
return "member/create";
}
fetch("/signup",{
method : 'post',
body : payload
})...
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();
...
}
}
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="/";
}
})
}
<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 |
<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을 기억해서 그곳으로 이동