일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- MYSQL
- network
- java
- git
- aspect
- RestControllerAdvice
- request
- OOP
- 스프링 시큐리티
- spring boot
- 인터셉터
- 트랜잭션
- http
- Spring Security
- 객체지향프로그래밍
- response
- 스프링
- mybatis
- Interceptor
- 스프링부트
- 디자인패턴
- 자바
- Redis
- Filter
- proxy pattern
- exception
- 관점지향프로그래밍
- SQL
- Spring
- aop
- Today
- Total
장쫄깃 기술블로그
[Spring Security] 2. Spring Security 적용하기 (Session) 본문
[Spring Security] 2. Spring Security 적용하기 (Session)
장쫄깃 2022. 5. 9. 16:08
들어가며
스프링 시큐리티 사용시 기본적으로 Session을 사용한다. 스프링 시큐리티와 Session을 이용하여 로그인 및 권한을 체크하는 기능을 만들어보았다.
스프링 시큐리티에 대한 설명은 해당 글을 참고하면 된다.
링크 : https://jangjjolkit.tistory.com/24
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”으로 표기한다.
다음은 AccessDenied와 EntryPoint의 차이점에 대한 항목이다.
- 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
스프링 시큐리티 6.1 이후 버전 Session 로그인 구현은 해당 글을 참고하면 된다.
링크 : https://jangjjolkit.tistory.com/71
'Spring Framework > Spring Security' 카테고리의 다른 글
[Spring Security] Spring Security (Session) - Spring Security 6.1 이후 (0) | 2024.08.02 |
---|---|
[Spring Security] 5. Spring Security + Thymeleaf 권한에 따른 메뉴 동적으로 출력하기 (0) | 2022.05.13 |
[Spring Security] 4. Spring Security URL 권한 동적으로 체크하기 (2) | 2022.05.13 |
[Spring Security] 3. Spring Security 적용하기 (JWT, Access Token, Refresh Token) (2) | 2022.05.10 |
[Spring Security] 1. Spring Security(스프링 시큐리티) 란? (2) | 2022.04.26 |