장쫄깃 기술블로그

[Spring Security] 2. Spring Security 적용하기 (Session) 본문

Spring Framework/Spring Security

[Spring Security] 2. Spring Security 적용하기 (Session)

장쫄깃 2022. 5. 9. 16:08
728x90

 


들어가며


스프링 시큐리티 사용시 기본적으로 Session을 사용한다. 스프링 시큐리티와 Session을 이용하여 로그인 및 권한을 체크하는 기능을 만들어보았다.

 

스프링 시큐리티에 대한 설명은 해당 글을 참고하면 된다.

링크 : https://jangjjolkit.tistory.com/24

 

[Spring Security] 1. Spring Security(스프링 시큐리티) 란?

Spring Security(스프링 시큐리티) 란? 스프링 시큐리티는 스프링 기반의 애플리케이션 보안(인증, 권한, 인가 등)을 담당하는 스프링 하위 프레임워크이다. 즉, 인증(Authenticate, 누구인지) 과 인가(A

jangjjolkit.tistory.com

 

 

1. Dependency 추가


<Maven>

<dependencies>
    <!-- ... other dependency elements ... -->
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-web</artifactId>
        <version>4.2.2.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-config</artifactId>
    <version>4.2.2.RELEASE</version>
    </dependency>
</dependencies>

<Gradle>

implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'

 

 

2. Java Configuration


WebSecurityConfigurerAdapter를 상속받은 config 클래스에 @EnableWebSecurity 어노테이션을 달면 SpringSecurityFilterChain이 자동으로 포함된다.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}

 

그 후에 configure 메소드를 오버라이딩하여 접근 권한을 설정한다.

 

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	// 비밀번호 암호화 로직
	@Autowired private BCryptPasswordEncoder passwordEncoder;
	// 권한이 없는 사용자 접근에 대한 handler
	@Autowired private WebAccessDeniedHandler webAccessDeniedHandler;
	// 인증되지 않은 사용자 접근에 대한 handler
	@Autowired private WebAuthenticationEntryPoint webAuthenticationEntryPoint;
	
	// 실제 인증을 담당하는 provider
	@Bean
	public CustomAuthenticationProvider customAuthenticationProvider() {
		return new CustomAuthenticationProvider(passwordEncoder);
	}
	
	// 스프링 시큐리티가 사용자를 인증하는 방법이 담긴 객체
	@Override
	public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) {
		authenticationManagerBuilder.authenticationProvider(customAuthenticationProvider());
	}
	
	/*
	* 스프링 시큐리티 룰을 무시할 URL 규칙 설정
	* 정적 자원에 대해서는 Security 설정을 적용하지 않음
	*/ 
	@Override
	public void configure(WebSecurity web) throws Exception {
		web.ignoring()
			.antMatchers("/resources/**")
			.antMatchers("/css/**")
			.antMatchers("/vendor/**")
			.antMatchers("/js/**")
			.antMatchers("/favicon*/**")
			.antMatchers("/img/**");
	}
	
	// 스프링 시큐리티 규칙
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.csrf().disable() // csrf 보안 설정 비활성화
			.antMatcher("/**").authorizeRequests() // 보호된 리소스 URI에 접근할 수 있는 권한 설정

			.antMatchers("/index").permitAll() // 전체 접근 허용
			.antMatchers("/main").authenticated() // 인증된 사용자만 접근 허용
			.antMatchers("/regist").annonymous() // 인증되지 않은 사용자만 접근 허용
			.antMatchers("/mypage").hasRole("ADMIN") // ROLE_ADMIN 권한을 가진 사용자만 접근 허용
			.antMatchers("/check").hasAnyRole("ADMIN", "USER") // ROLE_ADMIN 혹은 ROLE_USER 권한을 가진 사용자만 접근 허용
			
			// 그 외 항목 전부 인증 적용
			.anyRequest()
			.authenticated()
			.and()
		
        // exception 처리
		.exceptionHandling()
			.accessDeniedHandler(webAccessDeniedHandler) // 권한이 없는 사용자 접근 시
			.authenticationEntryPoint(webAuthenticationEntryPoint) // 인증되지 않은 사용자 접근 시
		
		.formLogin() // 로그인하는 경우에 대해 설정
			.loginPage("/user/loginView") // 로그인 페이지 URL을 설정
			.successForwardUrl("/hello") // 로그인 성공 후 이동할 URL 설정
			.failureForwardUrl("/user/loginView") // 로그인 실패 URL 설정
			.permitAll()
			.and()
		
		.logout() // 로그아웃 관련 처리
			.logoutUrl("/user/logout") // 로그아웃 URL 설정
			.logoutSuccessUrl("/user/loginView") // 로그아웃 성공 후 이동할 URL 설정
			.invalidateHttpSession(true) // 로그아웃 후 세션 초기화 설정
			.deleteCookies("JSESSIONID") // 로그아웃 후 쿠기 삭제 설정
			.and()
		
		// 사용자 인증 필터 적용
		.addFilterBefore(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
	}
    
	/*
	 * customLoginSuccessHandler를 CustomAuthenticationFilter의 인증 성공 핸들러로 추가
	 * 로그인 성공 시 /user/login 로그인 url을 체크하고 인증 토큰 발급 
	 */
	@Bean
	public UsrCustomAuthenticationFilter usrCustomAuthenticationFilter() throws Exception {
		UsrCustomAuthenticationFilter customAuthenticationFilter = new UsrCustomAuthenticationFilter(authenticationManager());
		customAuthenticationFilter.setFilterProcessesUrl("/user/login");
		customAuthenticationFilter.setAuthenticationSuccessHandler(usrCustomLoginSuccessHandler());
		customAuthenticationFilter.setAuthenticationFailureHandler(usrCustomLoginFailHandler());
		customAuthenticationFilter.afterPropertiesSet();
		return customAuthenticationFilter;
	}
	
	// 로그인 성공 시 실행될 handler bean 등록
	@Bean
	public UsrCustomLoginSuccessHandler usrCustomLoginSuccessHandler() {
		return new UsrCustomLoginSuccessHandler();
	}
	
	// 로그인 성공 시 실행될 handler bean 등록
	@Bean
	public UsrCustomLoginFailHandler usrCustomLoginFailHandler() {
		return new UsrCustomLoginFailHandler();
	}
}

 

다음은 andMatchers() 로 지정할 수 있는 항목들이다.

 

  • hasRole() 혹은 hasAnyRole()
    • 특정한 역할을 가지는 사용자만 접근 가능
    • ADMIN, USER 등
  • hasAuthority() 혹은 hasAnyAuthority()
    • 특정한 권한을 가지는 사용자만 접근 가능
    • ROLE_ADMIN, ROLE_USER 등
  • permitAll()
    • 전체 접근 허용
  • denyAll()
    • 전체 접근 제한
  • rememberMe()
    • 리멤버 기능으로 로그인한 사용자만 접근 가능
  • anonymous()
    • 인증되지 않은 사용자만 접근 가능
  • authenticated()
    • 인증된 사용자만 접근 가능

 

Role은 역할이고 Authority는 권한이지만 사실은 표현의 차이이다.
Role은 “ADMIN”으로 표현하고 Authority는 “ROLE_ADMIN”으로 표기한다.

 

다음은 AccessDeniedEntryPoint의 차이점에 대한 항목이다.

 

  • AccessDeniedHandler
    • 서버에 요청을 할 때 접근이 가능한지 권한을 체크한 후 접근할 수 없는 경우 동작
  • AuthenticationEntryPoint
    • 인증이 되지 않은 유저가 요청을 한 경우 동작

 

 

3. AuthenticationProvider 구현


사용자 인증 로직을 구현하기 위해 AuthenticationProvider를 상속받는 Provider를 구현한다.

@RequiredArgsConstructor
public class CustomAuthenticationProvider implements AuthenticationProvider {
	private final BCryptPasswordEncoder passwordEncoder;
	
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
		
		// AuthenticaionFilter에서 생성된 토큰으로부터 아이디와 비밀번호를 조회함
		String userId = token.getName();
		String userPw = (String) token.getCredentials();
		
		UserDTO uDTO = new UserDTO();
		// ... DB에서 아이디로 사용자 조회
		
		// 비밀번호 일치 여부 체크
		if (!passwordEncoder.matches(userPw, uDTO.getUsrPw())) {
			throw new BadCredentialsException(uDTO.getUsrId() + " Invalid password");
		}
		
		// principal(접근대상 정보), credential(비밀번호), authorities(권한 목록)를 token에 담아 반환
		return new UsernamePasswordAuthenticationToken(uDTO, userPw, uDTO.getAuthorities());
	}
	
	@Override
	public boolean supports(Class<?> authentication) {
		return authentication.equals(UsernamePasswordAuthenticationToken.class);
	}
}

 

 

4. LoginSuccessHandler 구현


Form Login에서 로그인에 성공한 경우 동작하기 위해 SavedRequestAwareAuthenticationSuccessHandler를 상속받는 핸들러를 구현한다.

로그인에 성공한 경우 인증 정보를 Spring Context Holder에 저장 후 지정된 페이지로 리다이렉트 하는 역할을 한다.

public class UsrCustomLoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
		// Spring Context Holder에 인증 정보 저장
		SecurityContextHolder.getContext().setAuthentication(authentication);
		
		// ...
		
		// 로그인 후 페이지 이동 시 해당 코드 적용
		// response.sendRedirect("/hello");
	}
}

 

 

5. LoginFailHandler 구현


Form Login에 실패할 경우 동작하기 위해 AuthenticationFailureHandler를 상속받는 핸들러를 구현한다.

로그인 실패 시 로직 실행 후 로그인 페이지로 리다이렉트하는 역할을 한다.

public class UserCustomLoginFailHandler implements AuthenticationFailureHandler {
	private final Logger log = LoggerFactory.getLogger(this.getClass());

	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
		// ...
		
		// 로그인 실패 후 페이지 이동 시 해당 코드 적용
		response.sendRedirect("/user/loginView");
	}
}

 

 

6. AccessDeniedHandler 구현


서버에 요청을 할 때 접근이 가능한지 권한을 체크 후 접근할 수 없는 요청을 했을 시 동작하기 위해 AccessDeniedHandler를 상속받는 핸들러를 구현한다.

오류 페이지로 이동시키거나 에러코드를 반환하는 역할을 한다.

@Component
public class WebAccessDeniedHandler implements AccessDeniedHandler{
	@Override
	public void handle(HttpServletRequest request, HttpServletResponse response,
			AccessDeniedException accessDeniedException) throws IOException, ServletException {
		// 권한이 없는 경우 페이지 이동 시 사용
		response.sendRedirect("/error/error403");
		// 권한이 없는 경우 에러코드 반환 시 사용
		response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
	}
}

 

 

7. AuthenticationEntryPointHandler 구현


인증이 되지 않은 사용자가 요청을 한 경우 동작하기 위해 AuthenticationEntryPoint를 상속받는 핸들러를 구현한다.

오류 페이지로 이동시키거나 에러코드를 반환하는 역할을 한다.

@Component
public class WebAuthenticationEntryPoint implements AuthenticationEntryPoint{
	@Override
	public void commence(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException authException) throws IOException, ServletException {
		// 인증되지 않은 경우 페이지 이동 시 사용
		response.sendRedirect("error/error403.html");
		// 인증되지 않은 경우 에러코드 반환 시 사용
		response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
	}
}

 

 

8. AuthenticationFilter 구현


UsernamePasswordAuthenticationFilter를 상속받아 요청 시 가장 앞단에서 요청을 받는 역할을 하는 Filter를 구현한다.

요청을 하면 AuthenticationFilter로 요청이 먼저 들어오게 되고, 사용자가 보낸 아이디와 패스워드를 인터셉트한다.

로그인 시 적절한 유효성 체크를 통과하면 UserPasswordAuthenticationToken을 발급한다.

 

HttpServletRequest에서 꺼내온 사용자 아이디와 패스워드를 진짜 인증을 담당할 AuthenticationManager 인터페이스(구현체 - ProviderManager)에게 인증용 객체(UsernamePasswordAuthenticationToken)로 만들어줘서 위임한다.

public class UsrCustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
	public UsrCustomAuthenticationFilter(AuthenticationManager authenticationManager) {
		super.setAuthenticationManager(authenticationManager);
	}

	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(request.getParameter("usrId"), request.getParameter("usrPw"));
		
		// ...
		
		setDetails(request, authRequest);
		return this.getAuthenticationManager().authenticate(authRequest);
	}
}

 

 

9. Login 페이지 구현


Login 페이지를 구현한다. 로그인 시 Form Login을 기본으로 하며 method는 POST로 한다.

<!DOCTYPE html>
<html lang="ko">
<head>
	<meta charset="utf-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
	<title>develop</title>
	<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1, maximum-scale=1, minimal-ui">

	<style>
		* {box-sizing: border-box; -webkit-box-sizing: border-box;}
		.login-wrap {position: fixed; left:0; top:0; width:100%; height:100%; display: flex; justify-content: center; align-items: center; padding-bottom:100px;}
		.login-box {max-width: 400px; padding:50px 30px; background:#f5f5f5; border:1px solid #eee;}
		.login-box .tit {text-align: center; margin-bottom:30px;}
		.login-box .inp {width: 100%; height:40px; line-height: 40px; margin-bottom:10px; padding:0 10px; border:1px solid #eee;}
		.login-box .btn {display: block; width:100%; height:40px; line-height: 40px; background:#000; color:#fff; box-shadow: none; border:none; cursor: pointer;}
	</style>
</head>
<body>
	<form id="loginFrm" name="loginFrm" method="post" action="/user/login">
		<section class="login-wrap">
			<div class="login-box">
				<h1 class="tit">로그인</h1>
					<input type="text" title="아이디 입력" placeholder="아이디" class="inp" id="usrId" name="usrId">
					<input type="password" title="비밀번호 입력" placeholder="비밀번호" class="inp" id="usrPw" name="usrPw">
					<button type="button" class="btn" onclick="login_user();">로그인</button>
			</div>
		</section>
	</form>
	
	<script src="https://code.jquery.com/jquery-1.12.4.min.js"></script>
	<script>
		function login_user() {
			$("#loginFrm").submit();
		}
	</script>
</body>
</html>

 

 

정리하며


스프링 시큐리티는 사용자를 인증하고, 로그인 후 애플리케이션의 각 기능들에 대한 권한을 부여하는 기능을 구현하는 데 사용되는 프레임워크로, 각 핸들러, 필터들을 거쳐 동작한다는 것이 포인트다.

 

관련 소스 코드는 깃허브를 참고하면 된다.

링크 : https://github.com/JangDaeHyeok/Spring-Security

 

GitHub - JangDaeHyeok/Spring-Security: Spring Security 세션, JWT 방식 구현

Spring Security 세션, JWT 방식 구현. Contribute to JangDaeHyeok/Spring-Security development by creating an account on GitHub.

github.com

 

스프링 시큐리티 6.1 이후 버전 Session 로그인 구현은 해당 글을 참고하면 된다.

링크 : https://jangjjolkit.tistory.com/71

 

[Spring Security] Spring Security (Session) - Spring Security 6.1 이후

들어가며Spring Security 6.1부터 기존에 사용하던 and()와 non-Lambda DSL Method가 Deprecated 되고, 필수적으로 Lambda DSL을 사용하도록 변경되었다. 변경된 내용으로 스프링 시큐리티 세션 로그인을 구현해보

jangjjolkit.tistory.com

 

728x90