[Spring Security] 로그인 기능 구현 (1) Spring Security Authentication
본문 바로가기

Web 개발/게시판 만들기

[Spring Security] 로그인 기능 구현 (1) Spring Security Authentication

728x90
반응형

✍️ 개념 정리

1. Spring Security란?

Spring 기반 애플리케이션 보안을 위한 표준 프레임워크로, 인증과 인가를 담당합니다.

 

2. 인증(Authentication)과 인가(Authorization)

인증이란?

특정 리소스에 접근하려는 사람의 신원을 확인하는 방법을 의미합니다.

일반적으로 사용자에게 이름과 암호를 입력받는 방법이 있습니다.

 

인가(권한 부여)란?

특정 리소스에 접근할 수 있는 사람을 결정하는 방법을 의미합니다.

Spring Security에서 제공하는 인가 방식에는 Request 기반 과 Method 기반 이 있습니다.

Rrequest(요청) 기반 인가란? URL에 사용자의 접근을 제어하는 방법이며,

Method(방법) 기반 인가란? Method에 사용자의 접근을 제어하는 방법입니다.

 

💻 인증(Authentication) - 로그인 기능 구현

잠깐! 본문의 개발환경은 다음과 같습니다.

개발 환경
Spring Boot : 2.5.2
H2 Database

Java 11
Thymeleaf
Gradle

 

1. 의존성 추가

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

spring security를 추가하면 기본적으로 로그인 화면과 기능을 제공합니다.

 

 

2. 도메인 생성

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Account implements UserDetails {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(length = 20, nullable = false)
    private String userId;

    @Column(length = 100, nullable = false)
    private String password;

    private String korName;

    @Builder
    public Account(Long id, String userId, String password, String korName){
        this.id = id;
        this.userId = userId;
        this.password = password;
        this.korName = korName;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorityList = new ArrayList<>();
        authorityList.add(new SimpleGrantedAuthority("USER"));
        return authorityList;
    }

    @Override
    public String getUsername() {
        return userId;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

 

@Data
@Builder
@AllArgsConstructor
public class AccountDto {
    private Long id;
    private String userId;
    private String password;
    private String korName;

    public Account toEntity(){
        Account build = Account.builder()
                .id(id)
                .userId(userId)
                .password(new BCryptPasswordEncoder().encode(password))
                .korName(korName)
                .build();
        return build;
    }
    
}

  

  • 회원 엔티티(Account)는 UserDetails 인터페이스를 구현합니다.
  • UserDetails 인터페이스를 사용해야 Spring Security에서 사용자 정보로 인식합니다.
  • 이 때 오버라이딩한 메소드들은 추후 구현 예정입니다.
  • AccountDto는 꼭 필요하지는 않지만, 엔티티에 직접 접근하지 않기위해 생성하였습니다.

  

3. Repository, Service 생성

@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {
    Optional<Account> findByUserId(String userId);
}

 

@Service
@RequiredArgsConstructor
@Slf4j
public class AccountService implements UserDetailsService {

    private final AccountRepository accountRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return accountRepository.findByUserId(username)
                .orElseThrow(()-> new UsernameNotFoundException("[" + username + "] Username Not Found"));
    }

}

 

  • 회원 서비스는 UserDetailService 인터페이스를 구현합니다.
  • UserDetailService 인터페이스를 구현하면 loadUserByUsername 메소드를 통해 사용자 정보를 불러올 수 있습니다. 
  • loadUserByUsername 에서는 아이디로 회원을 찾아서
    회원이 있으면 해당 회원(Account)를 반환하고, 회원이 없으면(NULL) 예외를 발생시킵니다.

 

4. 구성 파일 작성

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final AccountService accountService;

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception{
        auth.userDetailsService(accountService).passwordEncoder(passwordEncoder());
    }

    @Override
    public void configure(WebSecurity web) throws Exception{
        web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .mvcMatchers("/login", "/signup", "/api/**").permitAll()
                .anyRequest().authenticated();

        http.formLogin()
                .loginPage("/login")
                .loginProcessingUrl("/login")
//                .defaultSuccessUrl("/")
                .failureHandler(failureHandler())
                .successHandler(successHandler())
                .usernameParameter("userId")
                .permitAll()
            .and()
            .csrf().disable();

        http.logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .logoutSuccessUrl("/login")
                .invalidateHttpSession(true);
    }


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

    @Bean
    public SimpleUrlAuthenticationSuccessHandler successHandler(){
        SimpleUrlAuthenticationSuccessHandler successHandler = new CustomLoginSuccessHandler();
        successHandler.setDefaultTargetUrl("/");
        return successHandler;
    }

    @Bean
    public AuthenticationFailureHandler failureHandler(){
        return new CustomLoginFailureHandler();
    }

}

 

  • 이 예제에서는 SpringBoot 2.5를 사용하기 때문에 WebSecurityConfigurerAdapter 를 상속받았지만,
    Spring Boot 2.7+ 이상을 사용하시는 분들은 이를 상속받지 않고, 모두 빈으로 등록하셔야 합니다.
  • web.ignoring()에 등록한 url은 스프링 시큐리티 규칙이 적용되지 않습니다.
    따라서 static 하위에 있는 css, js, lib 에는 스프링 시큐리티 규칙이 적용되지 않습니다.
  • http.authorizeRequests()는 권한을 설정할 수 있습니다.
    따라서 인덱스 페이지, 로그인, 회원가입, API 는 로그인한 사용자와 로그인하지 않은 사용자 모두 접근 가능하며,
    그 이외에는 로그인을 한 사용자만 접근 가능합니다.
  • http.formLogin()은 로그인 관련 설정을 할 수 있습니다.
    따라서 "/login" url으로 접근했을 때 로그인 화면에 접근할 수 있으며,
    로그인화면에서 form action="/login" action="post"로 로그인 정보를 전송합니다.
    이때, 스프링 시큐리티에서 기본으로 username, password로 전송하기 때문에 usernameParameter, passwordParameter 를 통해 변경할 수 있습니다.
    그리고, csrf().disable()을 통해 csrf 보안설정을 비활성화 하였습니다.(혹은 로그인 폼에서 csrf 토큰 전달)
  • http.logout()은 로그아웃 관련 설정을 할 수 있습니다.
    "/logout" url으로 접근했을 때 로그아웃을 할 수 있으며, 로그아웃 성공 시 "/login"으로 이동합니다.
    로그아웃시 세션을 만료시킵니다.
  • @EnableWebSecurity 어노테이션을 꼭 쓰셔야 SpringSecurityFilterChain 이 자동으로 포함됩니다.

 

public class CustomLoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        super.onAuthenticationSuccess(request, response, authentication);
    }
}

 

public class CustomLoginFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        String errormsg = "";

        if(exception instanceof LockedException) { //계정 잠금 여부
            errormsg = "locked";
        } else if(exception instanceof DisabledException) { //계정 활성화 여부
            errormsg = "disabled";
        } else if(exception instanceof AccountExpiredException) { //계정 기한 만료
            errormsg = "accountExpired";
        } else if(exception instanceof CredentialsExpiredException) { //비밀번호 기한 만료
            errormsg = "credentialExpired";
        } else if(exception instanceof BadCredentialsException){ // 비밀번호 입력 오류, ID 입력 오류

        }

        StringBuilder sb = new StringBuilder();
        sb.append("/login?error");

        if(!errormsg.equals("")){
            sb.append("=").append(errormsg);
        }

        response.sendRedirect(sb.toString());

    }
}

 

  • 반드시 필요한 것은 아니지만, successHandler와 failureHandler를 생성하여
    로그인 성공 또는 실패할 때 어떤 동작을 할지 지정하였습니다.
  • successHandler에 setDefaultTargetUrl 를 통해 로그인 성공시 "/" 로 이동하도록 하였습니다.
  • failureHandler에서 로그인 실패시 "/login?error" 뒤에 오류 메세지를 추가하였습니다.

 

 

참고 사이트:

https://docs.spring.io/spring-security

https://velog.io/@pjh612/Deprecated된-WebSecurityConfigurerAdapter-어떻게-대처하지

https://shinsunyoung.tistory.com/78

https://devuna.tistory.com/59

https://velog.io/@ewan/Spring-security-success-failure-handler

https://samtao.tistory.com/71

728x90
반응형