[Spring] Spring Security를 이용한 로그인 구현 (스프링부트 3.X 버전) [1] - 동작 원리 및 Config 설정

Paek·2024년 1월 25일
10

Spring Security

목록 보기
2/7
post-thumbnail

이 포스팅에서는 스프링 부트 3.2.2 버전을 사용하고, 스프링 시큐리티 6.2.1 버전을 사용합니다.

이번에 유엠씨 프로젝트를 진행하며 회원가입/로그인 파트를 진행하게 되었습니다. 이번 기회에 스프링 시큐리티에 대해 낱낱히 파헤쳐보고 앞으로 자유자재로 사용할 수 있도록 해보려고 합니다.

긴 시리즈가 될거같은 기분이 벌써 듭니다. 내용이 어렵다 보니 마음의 준비를 하고 보시는걸 추천합니다.

먼저, 기본 개념에 대해 정리해둔 글이 있습니다.여기에 제가 간단하게 개념을 정리해두었습니다. 먼저 보고 오면 이해가 더 잘될 것입니다.

공식 문서 : https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/index.html#publish-authentication-manager-bean

서론

직접 구현해보기에 앞서, 개념을 다시 한번 간단하게 복습해보겠습니다.

스프링 시큐리티란?

스프링 시큐리티는 스프링 기반의 애플리케이션 보안(인증, 인가, 권한)을 담당하는 스프링 하위 프레임워크입니다. 보안 옵션을 많이 제공해주고 복잡한 로직 없이도 어노테이션으로도 설정이 가능합니다. 여러 보안 위협 방어 및 요청 헤더도 보안처리를 해줍니다. 기본적으로 스프링 시큐리티는 세션 기반 인증을 제공합니다.

인증(Authentication)

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

인가(Authorization)

'권한부여'나 '허가'와 같은 의미로 사용됩니다. 즉, 어떤 대상이 특정 목적을 실현하도록 허용(Access) 하는 것을 의미합니다. 예를 들면, 파일 공유 시스템에서 권한별로 접근할 수 있는 폴더가 상이합니다. 관리자는 접속이 가능하지만 일반 사용자는 접속할 수 없는 경우에서 사용자의 권한을 확인하게 되는데, 이 과정을 인가라고 합니다.

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

인증 처리 과정

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

구현 시작

위에 언급하였듯이 스프링 부트 3.2.2 버전을 사용하고, 스프링 시큐리티 6.2.1 버전을 사용합니다. 그래서 다른 분들이 해놓은 글이 많이 없어 공식 문서를 참고하여 해보았습니다. 그러다 보니 틀린 부분이 있을 수 있는데, 있다면 적극적인 피드백 부탁드립니다!!

의존성 추가

스프링 시큐리티를 사용하기 위해 필요한 의존성입니다. build.gradle에 추가하시면 됩니다.

//== 스프링 시큐리티 ==//
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
testImplementation 'org.springframework.security:spring-security-test'
  • 스프링 시큐리티를 사용하기 위한 스타터
  • 타임리프에서 스프링 시큐리티를 사용하기 위한 의존성
  • 스프링 시큐리티를 테스트 하기 위한 의존성

유저 엔티티

유저 엔티티를 먼저 구현하였습니다.

@Entity
@Getter
@Builder
@AllArgsConstructor
@Table(name = "USERS")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Users {

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

	@Column(nullable = false)
	private String name;

	@Column(nullable = false, unique = true, length = 30)
	private String email;

	@Column(nullable = false)
	private String password;

	@Column(nullable = false)
	private String phoneNum;

	private String imgUrl;

	@Enumerated(EnumType.STRING)
	@JsonIgnore
	private PublicStatus publicStatus;

	@JsonIgnore
	@Enumerated(EnumType.STRING)
	private ShareStatus shareStatus;

	private LocalDate createdAt;

//	@JsonIgnore
//	@OneToMany(mappedBy = "users")
//	private List<SharedAlbum> sharedAlbums = new ArrayList<>();

}

위와 같이 기본적인 필드로 구성을 하였고, 아직 다른 엔티티가 구현이 완료된 상태가 아니라서 연관관계 매핑 부분은 주석처리를 해놓았습니다.

앞으로 인증을 진행할때, 아이디는 이메일로 진행할 예정입니다.
email과 password의 조합입니다.

스프링 부트 3.0 이상 - 시큐리티 설정

먼저 config 패키지에 SecurityConfig라는 시큐리티 설정 파일을 만들어 줍니다. 필요한 @bean들을 추가해 사용할 수 있습니다.

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

	private final UserDetailsServiceImpl userDetailsService;
	private final ObjectMapper objectMapper;

	 // 스프링 시큐리티 기능 비활성화 (H2 DB 접근을 위해)
//	@Bean
//	public WebSecurityCustomizer configure() {
//		return (web -> web.ignoring()
//				.requestMatchers(toH2Console())
//				.requestMatchers("/h2-console/**")
//		);
//	}

	// 특정 HTTP 요청에 대한 웹 기반 보안 구성
	@Bean
	public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
		http	.csrf(AbstractHttpConfigurer::disable)
				.httpBasic(AbstractHttpConfigurer::disable)
				.formLogin(AbstractHttpConfigurer::disable)
				.authorizeHttpRequests((authorize) -> authorize
						.requestMatchers("/signup", "/", "/login").permitAll()
						.anyRequest().authenticated())
				// 폼 로그인은 현재 사용하지 않음         
//				.formLogin(formLogin -> formLogin
//						.loginPage("/login")
//						.defaultSuccessUrl("/home"))
				.logout((logout) -> logout
						.logoutSuccessUrl("/login")
						.invalidateHttpSession(true))
				.sessionManagement(session -> session
					.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
		);
		return http.build();
	}

	
	@Bean
	public BCryptPasswordEncoder bCryptPasswordEncoder() {
		return new BCryptPasswordEncoder();
	}

}

코드 설명

  • Configure() : 스프링 시큐리티의 모든 기능(인증, 인가 등)을 사용하지 않게 설정

    • requestMatchers() : 특정 요청과 일치하는 url에 대한 액세스 설정
    • ignoring() : requestMatchers에 적힌 url에 대해 인증, 인가를 하지 않음.
  • filterChain() : 특정 Http 요청에 대해 웹 기반 보안 구성. 인증/인가 및 로그아웃을 설정한다.

    • csrf(Cross site Request forgery) : 공격자가 인증된 브라우저에 저장된 쿠키의 세션 정보를 활용하여 웹 서버에 사용자가 의도하지 않은 요청을 전달하는 것. 즉, 정상적인 사용자가 의도치 않은 위조요청을 보내는 것을 의미한다.

      • 저는 REST API를 이용한 개발을 진행 할 예정입니다.Rest Api 환경에서는 Session 기반 인증과 다르기 때문에 서버에 인증정보를 보관하지 않고, 권한 요청시 필요한 인증정보(OAuth2, Jwt토큰 등)요청을 포함하기 때문에 굳이 불필요한 csrf 보안을 활성화할 필요가 없습니다.
      • 따라서 csrf는 disable 처리 하였습니다.
    • HttpBasic(), FormLogin() : Json을 통해 로그인을 진행하는데, 로그인 이후 refresh 토큰이 만료되기 전까지 토큰을 통한 인증을 진행할것 이기 때문에 비활성화 하였습니다.

      • HttpBasic() : Http basic Auth 기반으로 로그인 인증창이 뜬다.
    • authorizeHttpRequests() : 인증, 인가가 필요한 URL 지정

      • anyRequest() : requestMatchers에서 지정된 URL외의 요청에 대한 설정
      • authenticated() : 해당 URL에 진입하기 위해서는 인증이 필요함
      • requestMatchers("Url").permitAll() : requestMatchers에서 지정된 url은 인증, 인가 없이도 접근 허용
      • Url에 /**/ 와 같이 ** 사용 : ** 위치에 어떤 값이 들어와도 적용 (와일드 카드)
      • hasAuthority() : 해당 URL에 진입하기 위해서 Authorization(인가, 예를 들면 ADMIN만 진입 가능)이 필요함
        • .hasAuthority(UserRole.ADMIN.name()) 와 같이 사용 가능
    • formLogin() : Form Login 방식 적용

      • loginPage() : 로그인 페이지 URL
      • defaultSuccessURL() : 로그인 성공시 이동할 URL
      • failureURL() : 로그인 실패시 이동할 URL
    • logout() : 로그아웃에 대한 정보

      • invalidateHttpSession() : 로그아웃 이후 전체 세션 삭제 여부
    • sessionManagement() : 세션 생성 및 사용여부에 대한 정책 설정

      • SessionCreationPolicy() : 정책을 설정합니다.
      • SessionCreationPolicy.Stateless : 4가지 정책 중 하나로, 스프링 시큐리티가 생성하지 않고 존재해도 사용하지 않습니다. (JWT와 같이 세션을 사용하지 않는 경우에 사용합니다)

로그인(/login), 회원가입 (/signup), 메인 페이지(/)에 대해서는 인증 없이도 접근할 수 있게 만들었습니다.


Configure 작성 문법 바뀐 부분

기존의 많은 분들은 스프링 2.X 버전을 사용중이실텐데요, 스프링 3.0 이상의 버전부터는 스프링 시큐리티 버전도 바뀌어서 기존의 Configuration과는 다르게 작성해야합니다.

WebSecurity, HttpSecurity 모두 큰 변화를 맞이 했는데, 그중 하나가 lambdas 형식의 작성법인거 같습니다.

위에 Http 설정의 구성을 보면 알 수 있듯이, 비활성화를 위해 csrf().disable()같은 형식으로 사용 했었던 과거와 비교하여, 파라미터로 함수를 전달(.csrf(AbstractHttpConfigurer::disable))하는 것을 볼 수있습니다.

formLogin()와 같은 설정의 경우도 마찬가지로 기존에 formLogin().loginPage() 형식으로 사용했다면 이제는 람다식을 파라미터로 전달하여 아래와 같이 사용합니다.

.formLogin(formLogin -> formLogin
						.loginPage("/login")
						.defaultSuccessUrl("/home"))

마무리

Spring Security 간단한 복습과 설정만 봤는데도 내용이 너무 길어졌습니다. 다음 포스팅에 이어서 비밀번호 암호화 및 동작 원리에 대해 쭉 알아보도록 하겠습니다.

글 솜씨가 없어 두서 없이 작성한 긴 글 보시느라 고생 많으셨고, 감사합니다.

참고 사이트

profile
티스토리로 이전했다가 다시 돌아왔습니다... https://100cblog.tistory.com/

1개의 댓글

comment-user-thumbnail
2024년 9월 4일

formlogin 말고 json을 통한 로그인 과정에 대한 내용이 궁금했는데 덕분에 잘 읽어보겠습니다!

답글 달기