공부/Spring

스프링부트 3.X 스프링 시큐리티 사용해서 회원가입, 로그인, 로그아웃 구현하기

데부한 2023. 6. 18. 17:16
반응형

스프링 시큐리티?

스프링 시큐리티는 스프링 기반의 애플리케이션 보안(인증, 인가, 권한)을 담당하는 스프링 하위 프레임워크이다.

 

인증(Authentication)?

인증은 사용자의 신원을 입증하는 과정이다. 쉽게 말하면 우리가 흔히 어떤 사이트에 아이디와 비밀번호를 입력하고 로그인하는 과정이다.

 

인가(Authorization)?

인가는 사용자의 권한을 확인하는 작업이다. 이것도 쉽게 말하면 파일 공유 시스템에서 권한 별로 접근할 수 있는 폴더가 상이하다. 상위직책자는 들어갈 수 있고, 하위 직책자는 접근할 수 없는 경우 사용자의 권한을 확인해야 하는데 이 과정을 인가라고 한다.

 

그래서 스프링 시큐리티?

스프링 기반 애플리케이션의 보안을 담당하는 스프링 하위 프레임워크이다. 이름에 걸맞게 보안 관련 옵션을 많이 제공해 주며 복잡한 로직 없이 어노테이션으로 설정할 수 있다. 여러 보안 위협을 방어해 주고 요청 헤더도 보안 처리를 해준다. 그리고 기본적으로 스프링 시큐리티는세션 기반 인증을 제공한다.

 

반응형

 

스프링 시큐리티는 필터 기반으로 동작한다.

출처 : http://atin.tistory.com/590

엄청 다양한 필터로 구성되어있다. 각 필터에서 인증, 인가와 관련된 작업을 처리하며, SecurityContextPersistenceFilter부터 시작해서 아래로 내려가며 FilterSecurityInterceptor까지 순서대로 필터를 거친다.

어피치 스티커가 붙어있는 UsernamePasswordAuthenticationFilterSecurityInterceptor는 아이디와 패스워드가 넘어오면 인증 요청을 위임하는 인증 관리자 역할을 한다.

프로도 스티커가 붙어있는 FilterSecurityInterceptor는 권한 부여 처리를 위임해 접근 제어 결정을 쉽게 하는 접근 결정 관리자 역할을 한다.

필터명 설명
SecurityContextPersistenceFilter SecurityContextRepository에서 SecurityContext(접근 주체와 인증에 대한 정보를 담고 있는 객체)를 가져오거나 저장하는 역할
LogoutFilter 설정된 로그아웃 URL로 오는 요청을 확인해 해당 사용자를 로그아웃 처리 함
UsernamePasswordAuthenticationFilter 인증 관리자. 폼 기반 로그인 시 사용되는 필터로 아이디, 패스워드 데이터를 파싱하여 인증 요청을 위임. 인증이 성공하면 AuthenticationSuccessHandler, 실패하면 AuthenticationFailureHandler를 실행
DefaultLoginPageGeneratingFilter 사용자가 로그인 페이지를 따로 지정하지 않았을 때 기본으로 설정하는 로그인 페이지 관련 필터
BasicAuthenticationFilter 요청 헤더에 있는 아이디와 패스워드를 파싱해 인증 요청 위임. 인증이 성공하면 AuthenticationSuccessHandler를, 인증에 실패하면 AuthenticationFailureHandler 실행
RequestCacheAwareFilter 로그인 성공 후, 관련 있는 캐시 요청이 있는지 확인하고 캐시 요청 처리. 
SecurityContextHolderAwareRequestFilter HttpServletRequest 정보를 감싼다. 필터 체인 상의 다음 필터들에게 부가 정보를 제공함.
AnonymousAuthenticationFilter 필터가 호출되는 시점까지 인증되지 않았다면 익명 사용자 전용 객체인 AnonymousAuthentication을 만들어 SecurityContext에 넣어준다.
SessionManagementFilter 인증된 사용자와 관련된 세션 작업 진행. 세션 번조 방지 전략 설정, 유효하지 않은 세션에 대한 처리, 세션 생성 전략을 세우는 등의 작업 처리
ExceptionTranslationFilter 요청을 처리하는 중에 발생할 수 있는 예외를 위임하거나 전달
FilterSecurityInterceptor 접근 결정 관리자. AccessDecisionManager로 권한 부여 처리를 위임함으로써 접근 제어 결정을 쉽게 해준다. 이 과정에서 이미 사용자 인증이 되어있으므로 유효한 사용자인지도 알 수 있음. 인가 관련 설정 가능.

 

 

스프링 시큐리티 인증 처리 과정

  1. 사용자가 폼에 아이디, 패스워드를 입력하면 HTTPServletRequest에 아이디, 비밀번호 정보가 전달된다. 이때 AuthenticationFilter가 넘어온 아이디와 비밀번호의 유효성 검사를 실시한다.
  2. 유효성 검사 후 실제 구현체인 UsernamePasswordAuthenticationToken을 만들어 넘겨준다.
  3. 인증용 객체인 UsernamePasswordAuthenticationToken을 AuthenticationManager에게 전달한다.
  4. UsernamePasswordAuthenticationToken을 AuthenticationProvider에게 전달한다.
  5. 사용자 아이디를 UserDetailsService로 보낸다. UserDetailService는 사용자 아이디로 찾은 사용자의 정보를 UserDetails 객체로 만들어 AuthenticationProvider에게 전달한다.
  6. DB에 있는 사용자 정보를 가져온다.
  7. 입력 정보와 UserDetails의 정보를 비교해 실제 인증 처리를 진행한다.
  8. ~ 10까지 인증이 완료되면 SecurityContextHolder에 Authentication을 저장한다. 인증 성공 여부에 따라 성공 시 AuthenticationSuccessHandler, 실패 시 AuthenticationFailureHandler 핸들러를 실행한다.

 

 

스프링부트 3.X에 스프링 시큐리티 적용하기

의존성 추가하기

build.gradle dependencies에 스프링 시큐리티 관련 의존성을 추가한다.

// 스프링 시큐리티
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
testImplementation 'org.springframework.security:spring-security-test'

차례대로

  • 스프링 시큐리티를 사용하기 위한 스타터 추가
  • 타임리프에서 스프링 시큐리티를 사용하기 위한 의존성 추가
  • 스프링 시큐리티를 테스트하기 위한 의존성 추가

 

 

회원 도메인 생성

UserDetails 클래스를 상속하는 User 클래스 생성

@Table(name = "users")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
public class User implements UserDetails {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", updatable = false)
    private Long id;

    @Column(name = "email", nullable = false, unique = true)
    private String email;

    @Column(name = "password")
    private String password;

    @Builder
    public User(String email, String password, String auth) {
        this.email = email;
        this.password = password;
    }

    @Override // 권한 반환
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(new SimpleGrantedAuthority("user"));
    }

    // 사용자의 id 반환(고유한 값)
    @Override
    public String getUsername() {
        return email;
    }

    // 사용자의 패스워드 반환
    @Override
    public String getPassword() {
        return password;
    }

    // 계정 만료 여부 반환
    @Override
    public boolean isAccountNonExpired() {
        return true; // true : 만료 X
    }

    // 계정 잠금 여부 반환
    @Override
    public boolean isAccountNonLocked() {
        return true; // true : 잠금 X
    }

    // 패스워드 만료 여부 반환
    @Override
    public boolean isCredentialsNonExpired() {
        return true; // true : 만료 X
    }

    // 계정 사용 가능 여부 반환
    @Override
    public boolean isEnabled() {
        return true; // true : 사용 가능
    }
}

 

UserDetails 클래스는 스프링 시큐리티에서 사용자의 인증 정보를 담아 두는 인터페이스이므로 필수 오버라이드 메서드가 많다.

메서드 설명
getAuthorities() 사용자가 가지고 있는 권한 목록 반환.
getUsername() 사용자를 식별할 수 있는 사용자 이름 반환. 사용되는 값은 반드시 고유해야 함.
getPassword() 사용자의 비밀번호 반환. 반드시 암호화해서 저장
isAccountNonExpired() 계정이 만료되었는지 확인. 만료되지 않았을 시 true 반환
isAccountNonLoked() 계정이 잠금되었는지 확인. 잠금되지 않았을 시 true 반환
isCredentialsNonExpired() 비밀번호가 만료되었는지 확인. 만료되지 않았을 시 true 반환
isEnabled() 계정이 사용 가능한지 확인. 사용 가능할 시 true 반환

 

 

Repository 생성

스프링 시큐리티를 이용해 사용자 정보를 가져오기 위해서는 스프링 시큐리티가 이메일을 전달받아야 한다.

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email); // email로 사용자 정보 가져옴
}

 

 

Service 생성

UserDetailsService 인터페이스를 구현하고, loadUserByUsername() 메서드를 오버라이딩해서 사용자 정보를 가져오는 로직을 작성한다.

@RequiredArgsConstructor
@Service
public class UserDetailService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public User loadUserByUsername(String email) {
        return userRepository.findByEmail(email)
                .orElseThrow(() -> new IllegalArgumentException(email));
    }
}

 

반응형

 

WebSecurityConfig

@RequiredArgsConstructor
@EnableWebSecurity
@Configuration
public class WebSecurityConfig {

    private final UserDetailsService userService;

    // 스프링 시큐리티 기능 비활성화
    @Bean
    public WebSecurityCustomizer configure() {
        return (web) -> web.ignoring()
                .requestMatchers(toH2Console())
                .requestMatchers("/static/**");
    }

    // 특정 HTTP 요청에 대한 웹 기반 보안 구성
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                .authorizeHttpRequests()
                .requestMatchers("/login", "/signup", "/user").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin() // 폼 기반 로그인 설정
                .loginPage("/login")
                .defaultSuccessUrl("/home")
                .and()
                .logout() // 로그아웃 설정
                .logoutSuccessUrl("/login")
                .invalidateHttpSession(true)
                .and()
                .csrf().disable() //csrf 비활성화
                .build();
    }
	
    // 인증 관리자 관련 설정
    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider() throws Exception {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();

        daoAuthenticationProvider.setUserDetailsService(userService);
        daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder());

        return daoAuthenticationProvider;
    }

    // 패스워드 인코더로 사용할 빈 등록
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

 

  • configure() : 스프링 시큐리티의 모든 기능(인증, 인가)을 사용하지 않게 설정.
    • requestMatchers() : 특정 요청과 일치하는 url에 대한 액세스 설정
    • ignoring() : requestMatchres()에 적힌 url에 대해 인증, 인가 서비스를 적용하지 않음
  • filterChain() : 특정 HTTP 요청에 대해 웹 기반 보안 구성. 인증/인가 및 로그인, 로그아웃 설정
    • permitAll() : 누구나 접근 가능. requestMatchers()에 기재된 url은 인증, 인가 없이도 접근이 가능함
    • anyRequest() : 해당 코드 윗 줄에서 설정한 url 이외의 요청에 대해 설정
    • authenticated() : 인가는 필요하지 않지만 인증이 필요함
    • loginPage() : 로그인 페이지 설정
    • defaultSuccessUrl() : 로그인 성공 시 이동할 경로
    • logoutSuccessUrl() : 로그아웃 성공 시 이동할 경로
    • invalidateHttpSession() : 로그아웃 이후에 세션 전체 삭제 여부
    • csrf().disable() : CSRF 설정 비활성화. 원래는 CSRF 공격을 방지하기 위해 활성화하는 게 좋음
  • daoAuthenticationProvider() : 인증 관리자 설정. 사용자 정보를 가져올 서비스를 재정의하거나, 인증 방법 등을 설정
    • setUserDetailsService() : 사용자 정보를 가져올 서비스를 설정. 이때 설정하는 서비스 클래스는 반드시 UserDetailsService를 상속받은 클래스여야 함.
    • setPasswordEncoder() : 비밀번호 암호화를 위한 인코더 설정
  • bCryptPasswordEncoder() : 비밀번호 암호화를 위한 빈 등록

 

반응형

 

회원 가입 구현

DTO 생성

@Getter
@Setter
public class AddUserRequest {

    private String email;
    private String password;
    
}

 

 

Service 생성

패스워드를 BCryptPasswordEncoder를 사용해서 암호화한 후에 저장한다.

@RequiredArgsConstructor
@Service
public class UserService {

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    public Long save(AddUserRequest dto) {
        return userRepository.save(User.builder()
                .email(dto.getEmail())
                .password(bCryptPasswordEncoder.encode(dto.getPassword()))
                .build()).getId();
    }
}

 

 

Controller 생성

redirect: 접두사를 붙이면 회원 가입 처리가 끝난 후 무조건 /login URL로 이동한다.

@RequiredArgsConstructor
@Controller
public class UserApiController {

    private final UserService userService;

    @PostMapping("/user")
    public String signup(AddUserRequest request) {
        userService.save(request);
        return "redirect:/login";
    }
}

 

 

ViewController 생성

@Controller
public class UserViewController {

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

    @GetMapping("/login")
    public String login() {
        return "login";
    }
    
    @GetMapping("/signup")
    public String signup() {
        return "signup";
    }
}

 

 

로그아웃 구현

Controller 추가

@GetMapping("/logout")
public String logout(HttpServletRequest request, HttpServletResponse response) {
    new SecurityContextLogoutHandler().logout(request, response,
            SecurityContextHolder.getContext().getAuthentication());
    return "redirect:/login";
}

 

 

 

테스트

회원가입

http://localhost:8080/signup

회원가입 버튼을 누르면 자동으로 Login 페이지로 이동해야 함

 

반응형

 

로그인

 

로그인 성공 시 메인으로 이동해야 함

 

 

H2 DB 확인

반응형

 

 

로그아웃

로그아웃 버튼 클릭 시 login 화면으로 되돌아감

 

 

+ 추가) 메인 페이지에서 로그인한 사용자 정보 가져오기

메인페이지에서 로그인한 사용자의 아이디가 보였으면 좋겠어서 나름 수정해봤다. 올바른 코드인지는 잘 모르겠으니 주의!

엄청난 디자인이 담긴 메인 페이지에 email과 비밀번호가 출력되도록 추가했다.

<body>
<h1>로그인 성공!</h1>
<h3>놀랍지만 메인 페이지입니다.</h3>
<div>
    <span th:text="|${userInfo.email} 님 환영합니다.|"></span>
<br>
    <span th:if="${userInfo.password} != null"  
    	th:text="|${userInfo.password}  !!! 비번 유출 주의|"></span>
</div>
<button type="button" class="btn btn-secondary" onclick="location.href='/logout'">로그아웃</button>
</body>

 

 

ViewController home() 수정

@GetMapping("/home")
public String home(Model model, @AuthenticationPrincipal User userInfo) throws Exception {
    model.addAttribute("userInfo", userInfo);
    return "home";
}

뷰 쪽으로 넘겨주기 위해 Model 객체와, 로그인 한 사용자의 정보를 얻어오기 위해 @AuthenticationPrincipal과 User 객체를 매개변수로 받는다.

 

서버 재실행 후 메인 페이지로 접속해보면 너무 멋지게 아이디와 암호화 된 비밀번호가 눈에 보인다.

반응형

그래서 View 용 객체를 따로 만들어주었다.

 

UserResponse

@Getter
public class UserResponse {

    private String email;
    private String password = null;

    public UserResponse(User user){
        this.email = user.getEmail();
    }
}

 

 

ViewController home() 수정

@GetMapping("/home")
public String home(Model model, @AuthenticationPrincipal User userInfo) throws Exception {
    UserResponse userResponse = new UserResponse(userInfo);
    model.addAttribute("userInfo", userResponse);
    return "home";
}

User 객체를 UserResponse에 한 번 옮겨담고 그 객체를 뷰에 뿌려주었다.

 

성공!

 

 

다음 글

 

스프링부트 3.X 스프링 시큐리티 JWT 토큰 적용하기

Spring Boot 3.0.2 version 기준입니다. (Spring Boot 3.1.0까지 실행 확인) 이전 글 아래 포스팅과 이어지는 글입니다. 스프링부트 3.X 스프링 시큐리티 사용해서 회원가입, 로그인, 로그아웃 구현하기 스프

devhan.tistory.com

 

 


  • reference
    • 스프링부트3 백엔드 개발자 되기(자바편) 및 스택 오버플로우 및 여러 블로그
반응형