본문 바로가기

프로젝트 & TIL/일별 공부 기록 (백엔드 스쿨)

22일차 - 점프 투 스프링부트 3장

스프링 시큐리티

스프링 기반 애플리케이션의 인증과 권한을 담당하는 스프링의 하위 프레임워크

  

dependencies {
    ...
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.1.RELEASE'
}

build.gradle에 추가

- springsecurity6 버전 정보를 제거하고 사용하더라도 오류가 없다면 버전 정보 없이 사용하면 된다.

  

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests().requestMatchers(
                new AntPathRequestMatcher("/**")).permitAll();
        return http.build();
    }
}

- @EnableWebSecurity :  모든 요청 URL이 스프링 시큐리티의 제어를 받도록 만드는 애너테이션. 내부적으로 SpringSecurityFilterChain이 동작하여 URL 필터가 적용된다.

- 스프링 시큐리티의 세부 설정은 SecurityFilterChain 빈을 생성하여 설정할 수 있다.


회원가입

@Getter
@Setter
@Entity
public class SiteUser {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true)
    private String username;

    private String password;

    @Column(unique = true)
    private String email;
}

SiteUser 엔티티 클래스 생성

- 스프링 시큐리티에 이미 User 클래스가 있으므로 이름을 SiteUser로 정함

- @Column(unique = true) : 중복 저장 X


UserService

@RequiredArgsConstructor
@Service
public class UserService {

    private final UserRepository userRepository;

    public SiteUser create(String username, String email, String password){
        SiteUser user = new SiteUser();
        user.setUsername(username);
        user.setEmail(email);
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        user.setPassword(passwordEncoder.encode(password));

        this.userRepository.save(user);
        
        return user;
    }
}

- 시큐리티의 BCryptPasswordEncoder 클래스를 사용하여 암호화하여 비밀번호를 저장한다.

- BCryptPasswordEncoder는 BCrypt 해싱 함수(BCrypt hashing function)를 사용해서 비밀번호를 암호화한다.

- 그러나 이렇게 BCryptPasswordEncoder 객체를 직접 new로 생성하는 방식보다는 PasswordEncoder 빈(bean)으로 등록해서 사용하는 것이 좋다. 왜냐하면 암호화 방식을 변경하면 BCryptPasswordEncoder를 사용한 모든 프로그램을 일일이 찾아서 수정해야 하기 때문이다.


 UserService(수정)

public class SecurityConfig {

    ...
    
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

  

@RequiredArgsConstructor
@Service
public class UserService {

        ...

        user.setPassword(passwordEncoder.encode(password));

        ...
        
    }
}

회원가입 폼 - 유효성 검증

@Getter
@Setter
public class UserCreateForm {
    @Size(min = 3, max = 25)
    @NotEmpty(message = "사용자ID는 필수항목입니다.")
    private String username;

    @NotEmpty(message = "비밀번호는 필수항목입니다.")
    private String password1;

    @NotEmpty(message = "비밀번호 확인은 필수항목입니다.")
    private String password2;

    @NotEmpty(message = "이메일은 필수항목입니다.")
    @Email
    private String email;
}

- @Email : 해당 속성의 값이 이메일형식과 일치하는지를 검증


로그인과 로그아웃

@Getter
public enum UserRole {
    ADMIN("ROLE_ADMIN"), USER("ROLE_USER");

    UserRole(String value) {
        this.value = value;
    }

    private String value;
}

- enum 클래스 : 열거 자료형

- 상수 자료형이므로 @Setter없이 @Getter만 사용


UserSecurityService

@RequiredArgsConstructor
@Service
public class UserSecurityService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        
        ...
        
        return new User(siteUser.getUsername(), siteUser.getPassword(), authorities);
    }
}

- 스프링 시큐리티가 제공하는 UserDetailsService 인터페이스를 구현

- loadUserByUsername 메서드를 필수 구현해야 함

- 스프링 시큐리티는 loadUserByUsername 메서드에 의해 리턴된 User 객체의 비밀번호가 화면으로부터 입력 받은 비밀번호와 일치하는지를 검사하는 로직을 내부적으로 가지고 있다.


@Configuration
@EnableWebSecurity
public class SecurityConfig {

    ...

    @Bean
    AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
}

AuthenticationManager 빈 생성시 스프링의 내부 동작으로 인해 위에서 작성한 UserSecurityService와 PasswordEncoder가 자동으로 설정된다.


navbar 수정

<li class="nav-item">
    <a class="nav-link" sec:authorize="isAnonymous()" th:href="@{/user/login}">로그인</a>
    <a class="nav-link" sec:authorize="isAuthenticated()" th:href="@{/user/logout}">로그아웃</a>
</li>

- 사용자의 로그인 여부는 타임리프의 sec:authorize 속성을 통해 알수 있다.

  • sec:authorize="isAnonymous()" - 로그인 되지 않은 경우에만 해당 엘리먼트 표시
  • sec:authorize="isAuthenticated()" - 로그인 된 경우에만 해당 엘리먼트 표시