Backend/Spring

Spring Security - 세션 인증

hou27 2022. 5. 29. 05:07

지난번에 Spring Security를 적용했었는데,

 

Spring Security 적용하기

 

[Spring Boot] Spring Security 적용하기 - 암호화

프로젝트를 진행하면서 사용자 시스템을 구축한다면 필연적으로 인증 로직도 구현해야한다. 이 과정에서 만약 사용자의 비밀번호를 평문(Plain Text)으로 저장한다면, 심각한 보안상 문제를 초래

hou27.tistory.com

이번엔 이어서 세션을 기반으로 한 로그인을 구현해보도록 하겠다.

가장 먼저 스프링 시큐리티의 설정을 마무리하고,

타임리프를 통해 간단한 페이지를 구현하여 테스트까지 해볼 예정이다.


우선 변경된 Spring Security 설정 파일의 모습이다.

참고로 이 포스트에서 다루는 Spring Security의 버전은 5.7.1.이하이다.
Spring Boot version is lower than 2.7.0 and Spring Security version is older than 5.7.1.

5.7.1 버전의 코드는 잠시 후 다루도록 하겠다.

SpringConfig 5.7 이전 버전

@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
  private final UserDetailsService userDetailsService;

  /**
   * PasswordEncoder를 Bean으로 등록
   */
  @Bean
  public BCryptPasswordEncoder bCryptPasswordEncoder() {
    return new BCryptPasswordEncoder();
  }

  /**
   * 인증에 대한 지원
   */
  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
  }

  /**
   * 인증 or 인가에 대한 설정
   */
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
        .csrf().disable()
        .formLogin()
        .loginPage("/user/signIn")
        .loginProcessingUrl("/user/signInProc")
        .usernameParameter("email")
        .passwordParameter("password")
        .defaultSuccessUrl("/")
        .failureUrl("/user/signIn?fail=true");
    http
        .authorizeRequests()
        .antMatchers("/", "/user/signUp", "/user/userList", "/user/signIn*").permitAll()
        .anyRequest().authenticated();
  }
}

가장 아래쪽을 보면

http
        .csrf().disable()
        .formLogin()
        .loginPage("/user/signIn")
        .loginProcessingUrl("/user/signInProc")
        .usernameParameter("email")
        .passwordParameter("password")
        .defaultSuccessUrl("/")
        .failureUrl("/user/signIn?fail=true");

위와 같은 내용이 추가되었다.

 

formLogin을 이용해 로그인 관련 기능을 정의했는데,

  • loginPage를 통해 인증되지 않은 사용자가 인증을 필요로 하는 endpoint에 접근했을 때 "/user/signIn"으로 이동시키도록 하였다.
  • loginProcessingUrl을 통해 "/user/signInProc"로 POST 요청이 오면 로그인 처리를 수행하도록 하였다.
  • usernameParameter를 통해 인증 수행 시 사용자를 찾기 위해 사용할 매개변수를 정해주었다.
  • passwordParameter를 통해 비밀번호 매개변수를 지정해주었다.(기본값이 password라 사실 위와 같은 경우 기입해주지 않아도 된다.)
  • defaultSuccesUrl을 통해 성공 시 리다이렉트 할 페이지를 정해주었다.
  • failureUrl을 통해 실패 시 URI를 정해주었다.

 

private final UserDetailsService userDetailsService;

/**
* 인증에 대한 지원
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}

그리고 위와 같이 새로운 configure를 Override해주었다.

 

Spring Security의 Filter Chain을 거쳐서 인증, 권한에 대한 작업을 수행하기 위해

인증과정에서 사용하는 Interface들을 구현주어야한다.

 

userDetailService의 loadUserByUsername을 사용하여 사용자 정보를 가져오고, 인증, 인가 작업을 진행하기 위해

configure(AuthenticationManagerBuilder auth)

위 메서드를 오버라이딩하였다.

 

Spring Security Config 5.7.1

In Spring Security 5.7.0-M2 we deprecated the WebSecurityConfigurerAdapter,
as we encourage users to move towards a component-based security configuration.

 

Class WebSecurityConfigurerAdapter

 

WebSecurityConfigurerAdapter (spring-security-docs 5.7.1 API)

Provides a convenient base class for creating a WebSecurityConfigurer instance. The implementation allows customization by overriding methods. Will automatically apply the result of looking up AbstractHttpConfigurer from SpringFactoriesLoader to allow deve

docs.spring.io

 

5.7.0-M2 버전으로 넘어오면서 사용자가 구성 요소 기반 보안 구성으로 이동하도록 권장한다며

위에 설명했던 코드에 사용했었던 WebSecurityConfigurerAdapter를 deprecated하였다.

 

Spring Security Config » 5.7.1 또는

 

Maven Repository: org.springframework.security » spring-security-config » 5.7.1

org.springframework.security spring-security-config 5.7.1 // https://mvnrepository.com/artifact/org.springframework.security/spring-security-config implementation group: 'org.springframework.security', name: 'spring-security-config', version: '5.7.1' // ht

mvnrepository.com

Spring Boot Starter Security » 2.7.0

 

Maven Repository: org.springframework.boot » spring-boot-starter-security » 2.7.0

Starter for using Spring Security org.springframework.boot spring-boot-starter-security 2.7.0 // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security implementation group: 'org.springframework.boot', name: 'spring-boot-s

mvnrepository.com

를 사용하면 2022.06.02. 기준으로 최신 버전을 사용할 수 있다.

 

 

SpringConfig ver 5.7.1

@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
  @Bean
  public UserDetailsService userDetailsService() {
    return new UserDetailsServiceImpl();
  }
  
  @Bean
  public BCryptPasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }
  
  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .csrf().disable()
        .formLogin()
        .loginPage("/user/signIn")
        .loginProcessingUrl("/user/signInProc")
        .usernameParameter("email")
        .passwordParameter("password")
        .defaultSuccessUrl("/")
        .failureUrl("/user/signIn?fail=true");
    http
        .authorizeRequests()
        .antMatchers("/", "/user/signUp", "/user/userList", "/user/signIn*").permitAll()
        .anyRequest().authenticated();
    return http.build();
  }
}

 

기존에 아래와 같이

auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());

사용할 userDetailsService에 passwordEncoder를 명시적으로 설정했었는데,

 

위 코드에서처럼 Bean으로 등록만 해두면 Spring Security에 의해 해당 빈들이 사용된다.

 

또한

configure(HttpSecurity http)

위 메소드를 오버라이드하여 설정해주던 HttpSecurity는

SecurityFilterChain 빈을 등록하여 사용하는 방법을 권장하고 있다.


5.7.1 버전 관련 참고 링크


https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter
https://github.com/spring-projects/spring-security/issues/10822


 

 

Spring Security에서 사용자의 정보를 담는 interface는 UserDetails인데,

UserDetailsService 내의 loadUserByUsername 메서드는 유저의 정보를 불러와 UserDetails로 반환한다.

 

UserDetails

public interface UserDetails extends Serializable {

    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();

}
메소드 리턴 타입 설명 기본값
getAuthorities() Collection<? extends GrantedAuthority> 계정의 권한 목록을 리턴  
getPassword() String 계정의 비밀번호를 리턴  
getUsername() String 계정의 고유한 값을 리턴 ( ex : DB PK값, 중복이 없는 이메일 값 )  
isAccountNonExpired() boolean 계정의 만료 여부 리턴 true ( 만료 안됨 )
isAccountNonLocked() boolean 계정의 잠김 여부 리턴 true ( 잠기지 않음 )
isCredentialsNonExpired() boolean 비밀번호 만료 여부 리턴 true ( 만료 안됨 )
isEnabled() boolean 계정의 활성화 여부 리턴 true ( 활성화 됨 )

참고 링크

UserDetailsImpl

public class UserDetailsServiceImpl implements UserDetailsService {
  @Autowired
  private UserRepository userRepository;

  @Override
  public UserDetails loadUserByUsername(String email) throws UserNotFoundException {

    User user = userRepository.findByEmail(email)
        .orElseThrow(() -> new UserNotFoundException());
    Set<GrantedAuthority> grantedAuthorities = new HashSet<>();

    return new org
        .springframework
        .security
        .core
        .userdetails
        .User(user.getEmail(), user.getPassword(), grantedAuthorities);
  }
}

먼저, email을 사용자를 찾기 위해 사용할 매개변수로 지정해주었었으므로 넘겨받은 email을 통해 사용자를 찾는다.

 

그다음 현재 필자의 프로젝트는 권한이 따로 없으므로 빈 Set을 생성하여

UserDetails를 구현한 User인

org.springframework.security.core.userdetails.User

를 생성할 때 사용하여 해당 객체를 반환해주었다.

 

이후 인증이 이뤄지는데,

auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());

아까 함께 살펴본 코드를 보면 Spring Security 설정 파일에서 설정한 Encoder를 넘겨준 것을 알 수 있다.

/**
 * Allows specifying the {@link PasswordEncoder} to use with the
 * {@link DaoAuthenticationProvider}. The default is to use plain text.
 * @param passwordEncoder The {@link PasswordEncoder} to use.
 * @return the {@link AbstractDaoAuthenticationConfigurer} for further customizations
 */
@SuppressWarnings("unchecked")
public C passwordEncoder(PasswordEncoder passwordEncoder) {
    this.provider.setPasswordEncoder(passwordEncoder);
    return (C) this;
}

passwordEncoder의 선언부를 찾아가 보면,

DaoAuthenticationProvider에서 사용할 암호 인코더를 지정했다는 것을 알 수 있다.

 

 

DaoAuthenticationProvider로 넘어가 보면,

위와 같이 passwordEncoder.matches를 통해 넘겨받은 PasswordEncoder로 비밀번호를 체크하는 것을 알 수 있었다.

 

UserController

@Controller
@RequestMapping("/user")
@RequiredArgsConstructor
public class UserController {
  private final UserService userService;

  @GetMapping("/signUp")
  public String signUp() {
    return "user/signUp";
  }

  @PostMapping("/signUp")
  public String signUp(@Validated UserSignUpRequest signUpReq) throws Exception {
    User user = userService.signUp(signUpReq);

    return "redirect:/user/signIn";
  }

  @GetMapping("/signIn")
  public String signIn(@RequestParam(value = "fail", required = false) String flag, Model model) {
    model.addAttribute("failed", flag != null);

    return "user/signIn";
  }

  @GetMapping("/profile")
  public String profile(Model model, @AuthenticationPrincipal UserDetails userDetails) {
    if (userDetails != null) {
      User userDetail = userService.findByEmail(userDetails.getUsername())
          .orElseThrow(() -> new UserNotFoundException());

      model.addAttribute("userDetail", userDetail);
    }

    return "user/profile";
  }

  ...
}

타임리프를 사용하여 테스트를 진행하기 위해

반환 값을 String으로 하여 리소스를 찾아가게 하였고,

@AuthenticationPrincipal

위 어노테이션을 통해 현재 로그인한 사용자의 정보를 받아 간단한 프로필 페이지를 보여주는 과정을 통해

세션이 형성된 것을 확인하도록 하였다.

 

이때, 위 어노테이션을 사용한 매개변수의 타입은 UserDetails여야 한다.

 

 

 

 

 

 

로그인을 시도한 후

 

프로필 페이지를 확인해보니

 

정상적으로 메일과 이름이 뜨는 것을 확인할 수 있었다.

 

(UI는 테스트용이라 꾸미기 귀찮았다.....)

 

 

직접 테스트해보고 자세한 코드를 확인해보고 싶다면?

https://github.com/hou27/spring-boot-jwt-example/commit/32aa9caf5db2671be7430c9f0c1d3554acc72704

 

Delete useless dto · hou27/spring-boot-jwt-example@32aa9ca

This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

github.com

 


참고


https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter
https://github.com/spring-projects/spring-security/issues/10822