package com.example.board_project.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
//빈 등록 : 스프링 컨테이너에서 객체를 관리할 수 있게 하는 것
@Configuration //빈에 등록(IoC관리)
@EnableWebSecurity //시큐리티 필터가 등록이 된다(필터 설정을 여기서 해주겠다).
@EnableGlobalMethodSecurity(prePostEnabled = true) //특정 주소로 접근을 하면 권한 및 인증을 미리 체크하겠다
public class SecurityConfig {
@Bean //IoC 가능 비밀번호를 암호화 하기위함
public BCryptPasswordEncoder encoder() {
return new BCryptPasswordEncoder(); //이 값을 스프링이 관리
}
//시큐리티가 대신 로그인을 해줄때에 password를 가로채기를 하는데 해당 password가 뭘로 해쉬가 되어 회원가입이 되었었는지를 알아야
//같은 해쉬로 암호화해서 DB에 있는 해쉬랑 비교가 가능하다.
//새로 도입된 방식 사용하자
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable() //csrf 토큰 비활성화 (테스트시 걸어두는 게 좋음)
.authorizeRequests() //인증요청이들어올때
.antMatchers("/", "/auth/**","/userJoin") //auth/밑으로 들어오면 + 폴더 허용도 해줘야한다
.permitAll() //누구나 허용
.anyRequest() //그밖에는
.authenticated() //인증이 되어야된다
.and()
.formLogin()
.loginPage("/auth/loginForm") //auth/ 밑이 아닌 주소들은 인증이필요하기때문에 /aut/loginForm으로 전달된다
.loginProcessingUrl("/auth/loginProc") //시프링 시큐리티가 해당 주소요청오는 로그인을 가로채고 대신 로그인해준다
.defaultSuccessUrl("/"); //정상적일때 이 주소로 보낸다.
return http.build();
}
@Bean
AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
중요!
antMathcers()로 허용해주는 주소를 설정해줄때에 url 주소만 포함인줄 알았는데 파일(html, css, js 등등)인증없이 사용하기위해서는 이부분에 넣어주어야한다
허용된 주소가 아니라면 /auth/loginForm 으로 이동시키기때문에 해당 컨트롤러를 만들어준다.
/auth/loginForm 와 연결된 loginForm.html 에서는 action="/auth/loginProc" 을 갖는 폼태그로 정보를 전달하면 해당 주소로 요청오는 정보를 security 가 loadUserByUsername()함수로 가져온다.
<!DOCTYPE html>
<html lang="en">
<head>
<title>로그인</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css">
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.4/dist/jquery.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"></script>
</head>
<body>
<div id="header"></div>
<div class="container">
</br>
</br>
<h2>LOGIN</h2>
</br>
</br>
<form action="/auth/loginProc" method="post">
<div class="form-group">
<label for="loginId"> 아이디 : </label>
<input type="text" class="form-control" id="loginId" placeholder="Enter ID" name="loginId">
</div>
<div class="form-group">
<label for="password"> 비밀번호 : </label>
<input type="password" class="form-control" id="password" placeholder="Enter password" name="password">
</div>
</br></br>
<button type="submit" class="btn btn-primary" >로그인</button>
</form>
</div>
<div id="footer"></div>
<script
src="https://code.jquery.com/jquery-3.7.0.js"
integrity="sha256-JlqSTELeR4TLqP0OG9dxM7yDPqX1ox/HfgiSLBj8+kM="
crossorigin="anonymous">
</script>
<script>
$(function() {
$("#header").load("/templates/header.html");
$("#footer").load("/templates/footer.html");
});
</script>
</body>
</html>
loadUserByUsername() 함수를 사용해서 로그인을 진행해야하기때문에 UserDetails 인터페이스를 상속받는 클래스 설정 -> PrincipalDetail
package com.example.board_project.config.auth;
import com.example.board_project.model.User;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
//스프링 시큐리티가 로그인 요청을 가로채서 로그인을 진행하고 완료가 되면 UserDetails타입의 오브젝트를
//스프링 시큐리티의 고유한 세션저장소에 저장을 해준다.
@Getter
public class PrincipalDetail implements UserDetails {
private User user; //컴포지션관계
public PrincipalDetail(User user) {
this.user = user;
}
public User getUser() {
return user;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
//계정이 만료되지 않았는지 리턴 (true : 만료안됨)
@Override
public boolean isAccountNonExpired() {
return true;
}
//계정이 잠겨있는지를 리턴 (true : 안잠김)
@Override
public boolean isAccountNonLocked() {
return true;
}
//비밀번호가 만료되지 않았는지 리턴 (true : 만료안됨)
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//계정 활성화가 되어있는지 (true : 활성화)
@Override
public boolean isEnabled() {
return true;
}
//계정이 갖고있는 권한 목록을 리턴한다.(권한이 여러개 있을 수 있어서 루프를 돌아야 하는데 우리는 한개만)
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collectors = new ArrayList<>();
collectors.add(()->{
return "ROLE_" + user.getRole();}); //기본 형식
return collectors;
}
}
UserDetailsService 인터페이스를 상속받아서 loadUserByUsername() 함수 사용
package com.example.board_project.config.auth;
import com.example.board_project.model.User;
import com.example.board_project.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
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;
@Service
public class PrincipalDetailService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
//스프링이 로그인 요청을 가로챌 때 , username, password 변수 2개를 가로채는데
//password 부분 처리는 알아서 함
//username이 DB에 있는지만 확인해주면 된다.
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username).orElseThrow(() -> {
return new UsernameNotFoundException("해당 아이디를 찾을수 없습니다!");
});
return new PrincipalDetail(user);
}
}
로그인할때 입력했던 username 값을 갖는 User가 존재하는지를 확인해주고 그 user를 이전에 만들었던 클래스인 PrincipalDetail로 만들어주고 리턴하면 security가 알아서 비밀번호까지 조회한다.