진행 순서
- 스프링 시큐리티를 연동하기 위한 의존성 추가
- 스프링 시큐리티 연동을 위한 타임리프 확장 플러그인
- +) 시큐리티의 자세한 로그 파악을 위해 낮은 로그 설정
- 스프링 시큐리티 동작 이해를 위한 핵심 개념의 이해
- SecurityConfig를 통한 프로젝트의 시큐리티 설정 관리
- 필터와 인증/인가 관리 객체를 이용한 스프링 시큐리티 동작
- 스프링 시큐리티의 인증
- 필터 체인 구조와 인증 매니저
- UserNamePasswordAuthenticationToken 토큰을 이용해 인증매니저가 AuthenticationProvider를 통해 인증
- AuthenticationProvider가 인증 확인 후, UserDetailService를 통해 실제 인증을 위한 데이터 전달
- 스프링 시큐리티의 인가
- Authentication의 Roles 정보를 통해 Access-control
- Access-control를 통해 원하는 목적지에 접근 제한을 걸고, 이에 맞는 인증을 처리
- 인증과 인가를 위한 스프링 시큐리티 커스터마이징 설정
- InMemoryUserDetailsManager 객체 이용한 인증 매니저 설정
- SecurityFilterChain 객체 이용한 자원의 접근권한 설정
의존성 추가
- Spring Boot DevTools
- Lombok
- Spring Data JPA
- Oracle Driver
- Spring Web
- Thymeleaf
- Spring Security
- OAuth2 Client
추가 Thymeleaf 확장 플러그인
<!-- 타임리프 날짜시간의존성-->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-java8time</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>
<!-- 타임리프 시큐리티의존성-->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
추가 로그 설정
logging.level.org.springframework.security.web=trace
logging.level.com.club=debug
#시큐리티 관련 부분의 로그 레벨을 낮게 설정해 자세한 로그를 확인할 수 있도록 설정
스프링 시큐리티 사용시 임시 비밀번호가 생성된다.
-> 기본으로 생성되는 user 계정의 패스워드
Using generated security password: 25e3e6cc-dc8c-4326-a735-d90*****58b7
This generated password is for development use only. Your security configuration must be updated before running your application in production.
브라우저에서 강제 로그아웃을 원할 땐,
브라우저 내의 개발자도구에서 쿠키 중 톰캣이 사용하는 JSESSIONID를 삭제하면 로그아웃 처리가 된다.
시큐리티 설정 클래스
- 스프링 부트의 자동 설정 기능으로 연동처리가 수행된다.
- 스프링 시큐리티를 사용한다면, 해당 프로젝트에 맞는 설정을 추가해주는 것이 바람직하다.
- 프로젝트 내에 config 패키지를 추가해, SecurityConfig 클래스를 작성
- 이전의 WebSecurityConfigurerAdaptor의 deprecated로 빈등록을 통해 설정을 관리한다.
시큐리티 관련 기능의 쉬운 설정을 위해 WebSecurityConfigurerAdaptor 클래스를 상속으로 처리override를 통해 여러 설정을 조정한다.
SecurityConfig
package com.club.boot5.config;
import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@Log4j2
public class SecurityConfig {
}
권한에 따른 접근 단계를 구분
-
로그인을 하지 않은 사용자도 접근할 수 있는 ‘/sample/all’
-
로그인한 사용자만이 접근할 수 있는 ‘/sample/member’
-
관리자(admin) 권한이 있는 사용자만이 접근할 수 있는 ‘/sample/admin’
각자 접근 가능한 페이지 작성
all.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>For ALL</h1>
</body>
</html>
member.html
<!DOCTYPE html>
<html xmlns:sec="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>For Member ....................</h1>
<div sec:authorize="hasRole('USER')">Has USER ROLE</div>
<div sec:authorize="hasRole('MANAGER')">Has MANAGER ROLE</div>
<div sec:authorize="hasRole('ADMIN')">Has ADMIN ROLE</div>
<div sec:authorize="isAuthenticated()">
Only Authenticated user can see this Text
</div>
Authenticated username:
<div sec:authentication="name"></div>
Authenticated user roles:
<div sec:authentication="principal.authorities">
</div>
</div>
</body>
</html>
admin.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>For Admin</h1>
</body>
</html>
스프링 시큐리티의 동작 원리 이해
:'/sample/all' 경로 호출시 서버 로그 중심 확인
'/sample/all' 호출시 여러 개의 필터가 동작
2022-10-19 12:30:37.046 DEBUG 25408 --- [nio-8080-exec-3] o.s.s.w.s.HttpSessionRequestCache : Saved request http://localhost:8080/sample/all to session
2022-10-19 12:30:37.047 DEBUG 25408 --- [nio-8080-exec-3] s.w.a.DelegatingAuthenticationEntryPoint : Trying to match using And [Not [RequestHeaderRequestMatcher [expectedHeaderName=X-Requested-With, expectedHeaderValue=XMLHttpRequest]], MediaTypeRequestMatcher [contentNegotiationStrategy=org.springframework.web.accept.HeaderContentNegotiationStrategy@108eb536, matchingMediaTypes=[application/xhtml+xml, image/*, text/html, text/plain], useEquals=false, ignoredMediaTypes=[*/*]]]
2022-10-19 12:30:37.048 DEBUG 25408 --- [nio-8080-exec-3] s.w.a.DelegatingAuthenticationEntryPoint : Match found! Executing org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint@ec2f3a5
2022-10-19 12:30:37.048 DEBUG 25408 --- [nio-8080-exec-3] o.s.s.web.DefaultRedirectStrategy : Redirecting to http://localhost:8080/login
2022-10-19 12:30:37.048 TRACE 25408 --- [nio-8080-exec-3] o.s.s.w.header.writers.HstsHeaderWriter : Not injecting HSTS header since it did not match request to [Is Secure]
2022-10-19 12:30:37.048 DEBUG 25408 --- [nio-8080-exec-3] w.c.HttpSessionSecurityContextRepository : Did not store empty SecurityContext
2022-10-19 12:30:37.049 DEBUG 25408 --- [nio-8080-exec-3] w.c.HttpSessionSecurityContextRepository : Did not store empty SecurityContext
2022-10-19 12:30:37.049 DEBUG 25408 --- [nio-8080-exec-3] s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request
2022-10-19 12:30:37.057 TRACE 25408 --- [nio-8080-exec-4] o.s.security.web.FilterChainProxy : Trying to match request against DefaultSecurityFilterChain [RequestMatcher=any request, Filters=[org.springframework.security.web.session.DisableEncodeUrlFilter@2c2a0092, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@391bcb2a, org.springframework.security.web.context.SecurityContextPersistenceFilter@3769ae23, org.springframework.security.web.header.HeaderWriterFilter@51201289, org.springframework.security.web.csrf.CsrfFilter@3c82494a, org.springframework.security.web.authentication.logout.LogoutFilter@f84b383, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@77a8a715, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@12c59e15, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@5e2184ec, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@c46ebf0, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7141d341, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@fcad42c, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@11d26668, org.springframework.security.web.session.SessionManagementFilter@12e0cc6c, org.springframework.security.web.access.ExceptionTranslationFilter@41004dfe, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@42a13c94]] (1/1)
2022-10-19 12:30:37.057 DEBUG 25408 --- [nio-8080-exec-4] o.s.security.web.FilterChainProxy : Securing GET /login
2022-10-19 12:30:37.058 TRACE 25408 --- [nio-8080-exec-4] o.s.security.web.FilterChainProxy : Invoking DisableEncodeUrlFilter (1/16)
2022-10-19 12:30:37.058 TRACE 25408 --- [nio-8080-exec-4] o.s.security.web.FilterChainProxy : Invoking WebAsyncManagerIntegrationFilter (2/16)
2022-10-19 12:30:37.058 TRACE 25408 --- [nio-8080-exec-4] o.s.security.web.FilterChainProxy : Invoking SecurityContextPersistenceFilter (3/16)
2022-10-19 12:30:37.058 TRACE 25408 --- [nio-8080-exec-4] w.c.HttpSessionSecurityContextRepository : Did not find SecurityContext in HttpSession DC3B3206E2410D7707E2B8FE9BCFDA10 using the SPRING_SECURITY_CONTEXT session attribute
2022-10-19 12:30:37.058 TRACE 25408 --- [nio-8080-exec-4] w.c.HttpSessionSecurityContextRepository : Created SecurityContextImpl [Null authentication]
2022-10-19 12:30:37.058 DEBUG 25408 --- [nio-8080-exec-4] s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext
2022-10-19 12:30:37.058 TRACE 25408 --- [nio-8080-exec-4] o.s.security.web.FilterChainProxy : Invoking HeaderWriterFilter (4/16)
2022-10-19 12:30:37.058 TRACE 25408 --- [nio-8080-exec-4] o.s.security.web.FilterChainProxy : Invoking CsrfFilter (5/16)
2022-10-19 12:30:37.058 TRACE 25408 --- [nio-8080-exec-4] o.s.security.web.csrf.CsrfFilter : Did not protect against CSRF since request did not match CsrfNotRequired [TRACE, HEAD, GET, OPTIONS]
2022-10-19 12:30:37.058 TRACE 25408 --- [nio-8080-exec-4] o.s.security.web.FilterChainProxy : Invoking LogoutFilter (6/16)
2022-10-19 12:30:37.058 TRACE 25408 --- [nio-8080-exec-4] o.s.s.w.a.logout.LogoutFilter : Did not match request to Ant [pattern='/logout', POST]
2022-10-19 12:30:37.058 TRACE 25408 --- [nio-8080-exec-4] o.s.security.web.FilterChainProxy : Invoking UsernamePasswordAuthenticationFilter (7/16)
2022-10-19 12:30:37.058 TRACE 25408 --- [nio-8080-exec-4] w.a.UsernamePasswordAuthenticationFilter : Did not match request to Ant [pattern='/login', POST]
2022-10-19 12:30:37.058 TRACE 25408 --- [nio-8080-exec-4] o.s.security.web.FilterChainProxy : Invoking DefaultLoginPageGeneratingFilter (8/16)
2022-10-19 12:30:37.059 TRACE 25408 --- [nio-8080-exec-4] o.s.s.w.header.writers.HstsHeaderWriter : Not injecting HSTS header since it did not match request to [Is Secure]
2022-10-19 12:30:37.059 DEBUG 25408 --- [nio-8080-exec-4] w.c.HttpSessionSecurityContextRepository : Did not store empty SecurityContext
2022-10-19 12:30:37.060 DEBUG 25408 --- [nio-8080-exec-4] w.c.HttpSessionSecurityContextRepository : Did not store empty SecurityContext
2022-10-19 12:30:37.060 DEBUG 25408 --- [nio-8080-exec-4] s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request
스프링 시큐리티의 필터 동작
: 스프링 시큐리티 동작시 여러 개의 객체가 서로 데이터를 주고 받으면서 이루어진다.
-> 필터와 AuthenticationManager 등의 객체 이용
핵심 동작은 Authentication Manager (인증 매니저)를 통해 이루어지는데
Authentication Provider가 인증 매니저가 어떻게 동작하는지 결정하고, 실제 인증은 UserDetailsService에 의해 이루어진다.
스프링 시큐리티의 인증과 인가
: 사용자가 은행에 있는 금고의 내용을 열어보는 과정과 유사
- 사용자는 은행에 가서 신분증으로 자신을 증명한다. -인증
- 은행에서 신분증을 통해 사용자의 신분을 확인한다.
- 은행에서 신분증을 통해 해당 금고의 접근 권한 확인 -인가
- 접근권한이 있을 경우 금고를 열어준다.
필터와 필터 체이닝
: 서블릿이나 JSP에서 사용하는 필터와 유사하지만, 스프링의 빈과 연동할 수 있는 스프링 시큐리티의 필터
-> 스프링 시큐리티와 다르게 일반적인 필터는 스프링의 빈을 사용하지 못해 별도 클래스를 상속하는 형태로 이루어짐
스프링 시큐리티 내부에서 여러 개의 필터가 필터 체인 구조로 요청을 처리한다.
-> 개발 시에 필터를 확장하고 설정시, 다양한 형태의 로그인 처리가 가능하다.
인증을 위한 AuthenticationManager가 가진 인증 처리 메서드
: 파라미터도 Authentication, 리턴 타입도 Authentication
-> 로그인 과정에서 실제 사용자 검증 행위가 이루어지는 곳
UserNamePasswordAuthenticationToken
: 실제 동작에서 전달되는 파라미터로 인증 관련된 정보를 토큰이라는 객체로 만들어서 전달
- request를 통해 사용자 정보를 받아 UserNamePasswordAuthenticationToken 객체 생성
- AuthenticationManager의 authenticate()에 파라미터로 전달
- 인증매니저는 다양한 방식의 인증처리 방법을 제공한다.
- DB 이용
- 메모리상 정보 이용
- 인증매니저는 해당 인증을 AuthenticationProvider로 처리한다.
- AuthenticationProvider가 토큰 타입 처리를 확인 후, authenticate() 수행
- AuthenticationProvider가 UserDetailService를 이용해 실제 인증 위한 데이터를 가져온다.
- JPA로 리포지토리 제작시 UserDetailsService를 활용해 사용자 인증 정보를 처리한다.
인가의 권한과 접근 제한
: 인증 단계 이후 사용자의 권한이 적절한가? 확인 후 인가 처리
- 인증 처리 단계 후, 필터에서 호출하는 인증매니저의 authenticate() 메서드가 리턴하는 Authentication 인증정보의 Roles 속성의 권한 정보를 통해 접근 제한 처리를 수행한다.
: Access-control - 원하는 목적지에 접근 제한을 걸고, 스프링 시큐리티를 통해 인증을 처리한다.
인증과 인가 진행 순서
- 사용자가 원하는 URL 접근
- 요청한 URL에 알맞는 권한을 가진 사용자인지 인증 절차 수행 [로그인]
- 사용자가 계정정보를 통해 정보 전달
- 인증매니저가 전달된 정보를 통해 적절한 AuthenticationProvider를 찾아 인증
- AuthenticationProvider가 UserDetailsService를 구현한 객체로 처리하는데,
올바른 사용자라고 인증되면 사용자의 정보를 Authentication 타입으로 전달하는 것으로 인증 처리 - 인증 처리가 완료되면 전달받은 객체를 이용해 해당 URL에 인증된 정보에 접근 권한을 가지고 있는지 확인하는 인가 처리
스프링 시큐리티 커스터마이징
: SecurityConfig를 통해 적절한 접근 권한 관리
-> 빈 등록을 통해 동작을 제어한다.
사용할 커스터마이징
- PasswordEncoder
- 패스워드 암호화 수행
- bcrypt 해시 함수를 이용한 암호화를 수행하는
BCryptPasswordEncoder를 구현해서 사용
PasswordEncoder로 BCryptPasswordEncoder를 구현해서 비밀번호 암호화를 수행
- BCryptPasswordEncoder
- bcrypt 해시 함수를 이용한 암호화
- 암호화된 패스워드는 복호화 불가능 매번 암호화된 값이 다르다
- 값은 다르지만 길이는 똑같다.
- 원본 내용을 볼 수 없고, 특정 암호화된 문자열의 결과값인지만 확인 가능
package com.club.boot5.config;
import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@Log4j2
public class SecurityConfig {
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
BCryptPasswordEncoder 테스트
: 암호화를 문자열로 확인 불가능하므로, 어떤 값들을 사용할 수 있는지 확인하는 테스트
package com.club.boot5.security;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.password.PasswordEncoder;
@SpringBootTest
public class PasswordTests {
@Autowired
private PasswordEncoder passwordEncoder;
@Test
public void testEncode(){
String password="1111";
String enPw = passwordEncoder.encode(password);
System.out.println("enPw : "+enPw);
boolean matchResult = passwordEncoder.matches(password, enPw);
System.out.println("matchResult : "+matchResult);
}
}
결과
enPw : $2a$10$wO8xgibV2LVtJScXhtFLtOXkM4jppH9/gjPIsVjsHWFkQ6ukIzw4.
matchResult : true
- matchResult를 통해 암호화한 비밀번호가 실제 비밀번호와 같은지 확인
- 매번 암호화된 비밀번호 결과가 다르기 때문에 확인하는 matches메서드를 통해 확인 처리가 필요하다.
- 암호화된 패스워드를 이용해 로그인 과정에서 사용한다.
AuthenticationManager 설정
: 빈을 통해 InMemoryUserDetailsManager를 반환하는 userDetailsService() 메서드를 등록시켜서 직접 인증 매니저를 설정한다.
@Bean
public InMemoryUserDetailsManager userDetailsService() {
UserDetails user = User.builder()
.username("user1")
.password(passwordEncoder().encode("1111"))
.roles("USER")
.build();
log.info("userDetailsService............................");
log.info(user);
return new InMemoryUserDetailsManager(user);
}
-> 최소한의 코드로 로그인을 확인하는 InMemoryUserDetailsManager로 사용자를 생성해서 테스트를 진행한다.
생성한 사용자가 가지는 정보
- username
- password
- roles
시큐리티 필터 체인을 반환하는 필터 체인 메서드를 이용해 필터를 설정
//시큐리티 필터체인을 반환형으로
//HttpSecurity 객체인 http의 authorizeHttpRequests 메서드로 인가 절차를 수행하는데,
//앤트 스타일 패턴으로 원하는 자원을 선택 -> AuthenticationManagerBuilder 객체인 auth를 이용해 선택
//antMatchers() : 원하는 자원을 선택할 수 있다. -> 접근 제한 페이지 관리
//hasRole() : 권한에 따라 접근 제한을 선택할 수 있다.
//permitAll() : 권한에 관계없이 모든 접근을 허용한다.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((auth) -> {
auth.antMatchers("/sample/all").permitAll();
auth.antMatchers("/sample/member").hasRole("USER");
});
//인가/인증 절차에서 문제 발생시 권한 획득을 유도하는 페이지 리턴
http.formLogin();
return http.build();
}
로그인 폼을 이용한다면, 별도의 디자인 적용을 위한 추가 설정
- loginPage() - 별도의 로그인 페이지 이용하기 위한 메서드
- loginProcessUrl()
- defaultSuccessUrl()
- failureUrl()
인증된 사용자를 의미하는 ROLE_USER 상수
- 권한을 설정할 때 USER 권한을 요구하도록 지정하는데, 인증된 사용자를 의미한다
- 따라서 로그인 성공시 사용자는 ROLE_USER 권한을 갖도록 지정된다.
'Server Programming > Spring Boot Backend Programming' 카테고리의 다른 글
[Spring 부트 - 운동 클럽 프로젝트] 2. 소셜 로그인 연동 (0) | 2022.10.19 |
---|---|
[Spring 부트 - 운동 클럽 프로젝트] 1. 스프링 시큐리티 연동 (2) CSRF 와 접근 제한 설정 (0) | 2022.10.19 |
[Spring 부트 - 영화 리뷰 프로젝트] 6. Ajax로 영화 리뷰 처리 (2) 리뷰 등록 / 수정 /삭제 (0) | 2022.10.18 |
[Spring 부트 - 영화 리뷰 프로젝트] 6. Ajax로 영화 리뷰 처리 (1) 리뷰 구성 (0) | 2022.10.18 |
[Spring 부트 - 영화 리뷰 프로젝트] 5. 영화 조회 처리 (0) | 2022.10.18 |