[스프링 부트] 27. 스프링 시큐리티(Spring Security)로 로그인 구현 - 웹 게시판 v5

KangHo Lee's avatar
Nov 28, 2024
[스프링 부트] 27. 스프링 시큐리티(Spring Security)로 로그인 구현  - 웹 게시판 v5
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

devleekangho