SecurityConfig
@Configuration
public class SecurityConfig {
// 비밀번호 해시화 해주는 객체
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
// CSRF 보호 기능 비활성화
http.csrf(c -> c.disable());
// url pattern filter
http.authorizeHttpRequests( r ->
// 인증이 필요한 기능은 url에 /s/를 넣는다.
r.requestMatchers("/s/**").authenticated().anyRequest().permitAll())
.formLogin(f ->
f.loginPage("/login-form") // 인증 필요 시 "/login-form" 경로로 GET 요청
.loginProcessingUrl("/login") // 로그인 POST 요청 실행(loadUserByUsername 메서드)
.defaultSuccessUrl("/")); // 로그인 완료되면 인덱스로
return http.build();
}
}
- http.csrf(c -> c.disable())
- 시큐리티는 기본적으로 CSRF 공격 보호를 위해 CSRF 토큰 검증 기능이 있는데 지금은 연습을 위해 비활성화했습니다.
- PasswordEncoder
- 시큐리티는 해시화된 비밀번호만 취급하기 때문에 IoC 컨테이너에 비밀번호를 해시화 해주는 객체를 등록해야 합니다.
- requestMatchers("/s/**")
- 인증이 필요한 페이지의 url에 /s 를 추가합니다.
- s 는 security 약자를 쓴 것으로 개발자가 임의로 정하면 됩니다.
2. User Entity 수정
User
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "user_tb")
@Getter
@Entity
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String email;
@CreationTimestamp
private Timestamp createdAt;
// 권한 확인(role -> admin, manager, guest 같은)
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of();
}
// 계정이 만료되지 않았는지 확인합니다.
@Override
public boolean isAccountNonExpired() {
return true;
}
// ID 잠금 여부를 확인합니다.
@Override
public boolean isAccountNonLocked() {
return true;
}
// 비밀번호가 만료되지 않았는지 확인합니다.
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 계정 비활성화 여부를 확인합니다.
@Override
public boolean isEnabled() {
return true;
}
}
- 시큐리티는 UserDetails 형태의 객체만을 받기 때문에 implements 해야 합니다.
- getAuthorities()
- 반환이 List인 이유는 권한이 여러 개 일수도 있기 때문입니다.
- 유저 ID가 username이고 비밀번호가 password일 경우 Getter를 추가로 작성하지 않아도 됩니다.
@Override
public String getUsername() {
return email;
}
@Override
public String getPassword() {
return password;
}
- username이 아니라 email을 검증하고 싶다면 getter를 오버라이드해야 합니다.
3. UserService
@RequiredArgsConstructor
@Service
public class UserService implements UserDetailsService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
@Transactional
public void 회원가입(UserRequest.JoinDTO joinDTO) {
userRepository.save(joinDTO.toEntity(passwordEncoder));
}
// POST 요청
// /login 호출됨
// key 값이 정해져 있다 -> username, password
// Content-Type -> x-www-form-urlencoded
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
return user;
}
}
- loadUserByUsername 메서드
- 설정
- POST 방식 요청
- URL은 “/login”
- key의 이름은 username, password로 고정
- Content-Type 은 application/x-www-form-urlencoded
- 실행하는 작업
- 사용자가 입력한 비밀번호와 DB의 비밀번호가 같은지 체크합니다.
- 일치할 경우 세션에 username을 저장합니다.
컨트롤러에 로그인 핸들러 메서드를 작성하지 않아도 됩니다.
→ LoginDTO도 필요 없습니다.
4. Request DTO에 비밀번호를 해시화 하는 코드 추가
UserRequest
public class UserRequest {
@Data
public static class JoinDTO {
private String username;
private String password;
private String email;
public User toEntity(PasswordEncoder passwordEncoder) {
// 비밀번호 해시화
String encPassword = passwordEncoder.encode(password);
User user = new User(null, username, encPassword, email, null);
return user;
}
}
}
- 시큐리티의 비밀번호 비교가 제대로 작동하려면 비밀번호를 해시화해서 저장해야 합니다.
5. passwordEncoder(BCrypt) 테스트
BCryptTest
package com.metacoding.authblog._core;
import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class BCryptTest {
@Test
public void encode_test() {
String password = "1234";
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String encodedPassword = encoder.encode(password);
System.out.println(encodedPassword);
}
}
- 여기서 해시화 된 비밀번호를 더미데이터로 넣어두면 로그인 테스트를 편하게 할 수 있습니다.
data.sql
insert into user_tb(username, password, email, created_at) values('ssar', '$2a$10$dx/JsPZ3FyiElsRA3p0h9OPkXTwBUZufKfokVSVTBGTdYZfhB8PQK', 'ssar@nate.com', now());
insert into user_tb(username, password, email, created_at) values('cos', '$2a$10$Tc9D9x1weKQZ/Y8nQnWnseTI6jhwb0IuPG5SNKHUXHP0IovpaZ2ie', 'cos@nate.com', now());
Share article