스프링 시큐리티를 배우면서 회원가입 과정이 복잡하여 정리해보고자 글을 작성한다.
Spring Boot : 3.3.5 버전
Java : 17 버전
스프링에서 인증&인가를 편하게 구현할 수 있도록 도와주는 보안 관련 프레임워크다.
인증(Authentication) : 사용자가 누구인지 검증하는 과정
인가(Authorization) : 사용자의 역할에 따라 권한을 부여하는 과정
서버를 회사라고 비유한다면 회사 출입 카드를 사용하여 회사에 들어가는 과정이
인증을 하는 과정이고, 소속한 부서에 따라 출입가능한 사무실을 다르게 하는 것을 인가라고 한다.
스프링 시큐리티의 내부 구조는 이렇다고 하는데
너무 복잡해서 일단 간단하게 정리하고 넘어가고자 한다.
이렇게 말이다.
전체적인 진행과정을 순서대로 보면서 내부 구조를 이해해보자
사용자의 로그인 요청
-> 스프링 시큐리티 필터 적용
-> UserDetailsService
에서 loadUserByUsername
함수 실행
-> UserDetails
객체 만들어서 시큐리티에 넘겨주기
-> 해당 정보를 바탕으로 인증 완료
로그인 창에서 사용자가 계정 정보를 입력하고 로그인 요청을 보낸다.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/", "/login","/loginProc","/join","joinProc").permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.requestMatchers("/my/**").hasAnyRole("ADMIN", "USER")
.anyRequest().authenticated()
);
http
.formLogin((auth) -> auth
.loginPage("/login")
.loginProcessingUrl("/loginProc")
.permitAll()
);
http
.csrf((auth) -> auth.disable());
return http.build();
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
Spring Security의 전체적인 환경설정을 담당하는 클래스다.
filterChain
이라는 함수를 보자.
먼저, authorizeHttpRequests
함수가 있다.
이는 인가(authorization)를 해준다.
자세한 내용은 다음과 같다.
permitAll()
은 모든 사람이 접근 가능하다는 뜻hasRole("ADMIN")
은 ADMIN 역할만 가진 유저만 접근 가능하다는 뜻hasAnyRole("ADMIN", "USER")
은 여러 역할 중 하나라도 적용되면 접근 가능하다는 뜻anyRequest().authenticated()
는 그 외 나머지 요청들은 로그인을 한 유저면 가능하다는 뜻로그인 요청을 보내는 /joinProc
은 permitAll
이므로
로그인이 안 된 유저도 접근 가능하다.
formLogin
함수를 보면
로그인을 하는 페이지와 로그인을 진행하는 Url을 등록할 수 있다.
이를 통해 로그인을 어떻게 진행할 지 내가 지정할 수 있다.
그 외에도 csrf공격 방지 등 여러 가지 보안 관련 설정을 직접 커스텀 할 수 있다.
비밀번호 암호화에 필요한 BCryptPasswordEncoder
도 정의해놓는다.
이는 회원가입 로직에 사용된다.
로그인 요청이 들어오면 해당 데이터와 DB에 저장된 데이터를 비교해서
올바른 사용자 정보인지 확인해아 한다.
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<User> op_user = userRepository.findByUsername(username);
if(op_user.isPresent()) {
return new CustomUserDetails(op_user.get());
}
return null;
}
}
UserDetailsService 인터페이스를 구현하는 CustomUserDetailsService 클래스를 작성한다.
필수로 구현해야하는 loadUserByUsername
함수를 구현해준다.
이 함수는 UserRepository
에서 User
정보를 가져오고
해당 정보를 가지고 UserDetails
객체를 생성한다.
유저 엔티티는 다음과 같다.
@Setter
@Getter
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column(unique = true)
private String username;
private String password;
private String role; //ADMIN이나 USER 역할 부여용 컬럼
}
UserDetails
객체는 스프링 시큐리티에서 활용하기 편하게
유저 정보를 정리해놓은 데이터(DTO)라고 보면 된다.
@RequiredArgsConstructor
public class CustomUserDetails implements UserDetails {
private final User user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() { // 유저 권한 리턴해주는 함수
Collection<GrantedAuthority> collection = new ArrayList<>();
collection.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return user.getRole(); // 사용자의 권한을 리턴
}
});
return collection;
}
@Override
public String getPassword() { // 유저 비밀번호 리턴해주는 함수
return user.getPassword();
}
@Override
public String getUsername() { // 유저 아이디를 리턴해주는 함수
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() { // 계정이 만료되었는지 확인해주는 함수
return true;
}
@Override
public boolean isAccountNonLocked() { // 계정이 잠금되었는지 확인해주는 함수
return true;
}
@Override
public boolean isCredentialsNonExpired() { // 비밀번호가 만료되었는지 확인해주는 함수
return true;
}
@Override
public boolean isEnabled() { // 계정이 사용가능한지 확인해주는 함수
return true;
}
}
유저 DB에 아래 4가지 함수에 대한 정보를 저장하지 않아서 모두 true를 반환하도록 했다.
스프링 시큐리티는 전달받은 UserDetails
를 확인하여
로그인을 요청한 사용자가 올바른 사용자인지 확인하고 세션을 발급해준다.
일단 처음이라 이렇게 간단하게 공부해도 양이 정말 많았다.
시큐리티 내부구조를 잘 이해할수록 서비스에 중요한 보안을 유지할 수 있기 때문에
내부구조를 제대로 이해할 때까지 열심히 공부해야겠다.
JWT에 OAuth까지.. 배워야할 게 산더미다.
스프링을 쉽게 잘 가르쳐 주신다. 초보자라면 강추!