지난 블로그에 이어서 스프링 시큐리티를 적용해 로그인과 회원가입을 구현해보려 한다.
Spring Boot 3.1.3 + Spring Security 6.1.3 + mysql:8.0 + JPA
적용한 스프링 부트 버전과 시큐리티 버전에 따라 문법 등 지원하지 않는게 있을 수 있다.
코드 작성 기준 3.2.2 버전이 나왔지만 안정적으로 개발하고자 그 다음으로 최신 버전인 3.1.3 버전을 선택하여 개발하였다.
Spring Security를 사용하기 위해 다음 라이브러리를 build.gradle에 추가해준다.
//security
implementation 'org.springframework.boot:spring-boot-starter-security'
Spring Security에 필요한 bean을 추가해주는 config 클래스 파일을 추가해준다.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private static final String[] WHITE_LIST = {
"/user-service/**",
"/",
"/error"
};
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(request -> request
.requestMatchers(WHITE_LIST).permitAll()
.anyRequest().authenticated() //어떠한 요청이라도 인증 필요
);
return http.build();
}
config 설정은 스프링 시큐리티 버전마다 다르다. 버전에 맞게 맞춰줘야 한다.
BCryptPasswordEncoder: DB에 비밀번호를 저장할 때 그대로 노출되면 안되기 때문에 암호화를 위해 BCryptPasswordEncoder클래스를 생성하여 빈 등록을 해주었다.
WHITE_LIST : 로그인과 회원가입 api과 "/error", "/" 는 인증없이 호출이 되어야 한다. 따라서 WHITE_LIST에 따로 관리해주었다. (현재 user-service/에는 인증이 필요 없는 /login , /register 밖에 없어서 다음과 같이 작성하였다.)
.anyRequest(). authenticated() : 그 외의 어떠한 요청이라도 인증이 필요하다.
.csrf(AbstractHttpConfigurer::disable) : 로컬에서 확인을 위해 csrf를 비활성화 해주었다.
먼저 User Entity를 구현해준다.
@Setter
@Getter
@Entity
@Table(name = "users")
@AllArgsConstructor
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "product_id") // 외래 키 필드 추가
private Long product_id;
@Column(nullable = false, length = 50, unique = true)
private String email;
@Column(nullable = false, length = 50)
private String name;
@Column(nullable = false, length = 15)
private String phoneNumber;
@Column(nullable = false, length = 100)
private String encryptedPwd;
@Column(nullable = false)
private boolean isApproved;
private UserEntity(Optional<UserEntity> userEntity) {
this.id = id;
this.product_id = product_id;
this.email = email;
this.name = name;
this.phoneNumber = phoneNumber;
this.encryptedPwd = encryptedPwd;
this.isApproved = isApproved;
}
public UserEntity() {
}
public static UserEntity of(Optional<UserEntity> userEntity) {
return new UserEntity(userEntity);
}
}
MyUserDetailsServices는 Spring Security의 사용자 인증 정보를 관리하는 UserDetailsService 인터페이스를 구현한다.
@Service
@Transactional
@RequiredArgsConstructor
public class MyUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
Optional<UserEntity> findOne = userRepository.findByEmail(email);
UserEntity userEntity = findOne.orElseThrow(() -> new UsernameNotFoundException("없는 회원입니다"));
return User.builder()
.username(userEntity.getEmail())
.password(userEntity.getEncryptedPwd())
.authorities(new SimpleGrantedAuthority("ADMIN"))
.build();
}
}
필자는 재사용성, 테스트 용이성, 그리고 유연성을 고려하여 UserDetails를 UserEntity와 합치지 않고 분리하였다. 따라서 loadUserByUsername 에서 username을 찾은 후 UserDetails로 변환해주는 과정을 추가해 주었다.
loadUserByUsername 메서드는 사용자의 이름(여기선 email)으로 사용자의 정보를 불러오는 역할을 한다.
비밀번호와 관련된 것은 PasswordEncoder가 처리하고, 비밀번호를 확인하는 로직은 뒤에서 처리할 수 있기 때문에 일단 username으로 검색하는 것이다.
@Data
public class RequestLogin {
@Email
@NotNull(message = "이메일을 입력해주세요")
@Size(min = 5, message = "이메일은 5자 이상이어야 합니다")
private String email;
@NotNull(message = "비밀번호를 입력해주세요")
@Size(min = 8, message = "비밀번호는 8자 이상이어야 합니다")
private String pwd;
}
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class RequestUser {
@NotNull(message = "이메일을 입력해주세요")
@Size(min = 5, message = "이메일은 5자 이상이어야 합니다")
@Email
private String email;
@NotNull(message = "비밀번호를 입력해주세요")
@Size(min = 8, message = "비밀번호는 8자 이상이어야 합니다")
private String pwd;
@NotNull(message = "이름을 입력해주세요")
@Size(min = 2, message = "이름은 2자 이상이어야 합니다")
private String name;
@NotNull(message = "전화번호를 입력해주세요")
@Size(min = 10, max = 15, message = "전화번호는 10자에서 15자 사이여야 합니다")
private String phoneNumber;
}
로그인과 회원가입을 위한 requestDto를 작성해준다.
다음 포스팅에서는 이어서 JWT 를 이용한 인증 과정에 대해 작성해보겠습니다.
https://curiousjinan.tistory.com/entry/spring-boot-security-userdetailservice-dto-8