티스토리 뷰
formLogin 인증처리 방식이나 Ajax 인증처리 방식은 크게 다르지 않고 동일하게 필터를 기반으로 처리를 진행한다.
필터가 사용하는 각각의 클래스들도 역할은 조금 차이나지만 전반적인 처리 과정은 거의 동일하다.
formLogin 인증은 동기적인 방식으로 인증처리하고 Ajax 인증은 비동기적인 방식으로 처리하는 점이 다르다.
동기 (Synchronous)와 비동기(Asynchronous)
동기는 직렬적으로 작업을 처리하는 방식으로 서버에 요청을 보낸 후 응답을 받아야만 다음 동작이 진행되는 방식이다.
A 작업이 모두 진행될 때까지 B작업은 대기하게 된다.
비동기는 병렬적으로 작업을 처리하는 방식으로 서버에 요청을 보낸 후 응답의 여부와 상관없이 다음 작업이 진행된다.
A 작업을 진행되면 B 작업이 실행된다. A 작업은 진행 완료후 결과값이 끝나게 되면 출력된다.
보통 계좌이체같은 경우 작업이 동기적인 방식으로 한 계좌에서 작업이 완전히 끝나야 다른 계좌로 작업이 진행되어야 금액이 틀릴 일이 없다.
웹 서버 같은 경우 한 번에 많은 요청이 들어오고 처리해야하기 때문에 비동기 방식이라 볼 수 있다.
formLogin 인증방식과 동일하게 AjaxAuthenticationFilter 가 사용자의 요청을 받고 AjaxAuthenticationToken 인증 객체에 요청온 정보를 담아 AuthenticationManager에게 보내면 AjaxAuthenticationProvider에게 실제 인증처리를 위임하게 된다.
AjaxAuthenticationSuccessHandler 객체는 인증 성공 후 처리를 담당하고 AjaxAuthenticationFailureHandler 객체는 인증 실패 시 처리를 담당한다.
인증 처리에 성공 하고 자원 접근 가능한 인가처리에 관련해선 FilterSecurityInterceptor가 담당하게 되고 처리중 인증 예외나 인가 예외가 발생할 시 ExceptionTranslationFilter가 담당하여 처리하게 되고 인증이 실패할 경우엔 AuthenticationException 예외가 발생하여 AjaxUrlAuthenticationEntryPoint 가 처리하고 , 인가가 실패할 경우엔 AccessDeniedException 예외가 발생하여 AjaxAccessDeniedHandler 가 처리하게 된다.
AjaxAuthenticationFilter
- AbstractAuthenticationProcessingFilter 상속
- 추상클래스로 formLogin 인증방식에서 사용하는 UsernamePasswordAuthenticationFilter도 AbstractAuthenticationProcessingFilter 를 상속받아 구현되었다.
대부분 인증처리의 기능을 가진 추상 클래스이다.
- 추상클래스로 formLogin 인증방식에서 사용하는 UsernamePasswordAuthenticationFilter도 AbstractAuthenticationProcessingFilter 를 상속받아 구현되었다.
- 필터 작동 조건
- AntPathRequestMatcher("/api/login")로 요청정보와 매칭하고 요청 방식이 Ajax이면 필터 작동
- 스프링 부트3 버전이라면 Request
- AjaxAuthenticaitionToken 생성하여 AuthenticationManager에게 전달하여 인증처리
- Filter 추가
- http.addFilterBefore(AjaxAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
AbstractAuthenticationProcessingFilter를 상속받아 AjaxLoginProcessingFilter를 만든다.
생성자에 AntPathRequestMatcher 객체를 사용하여 "/api/login" url로 요청이 들어오면 현재 필터가 요청을 처리할 수 있게 설정한다.
request header 정보에 담긴 X-Requested-With의 값이 XMLHttpRequest라면 Ajax 요청이라 판단하여 값이 틀리면 IllegalStateException 예외를 던진다.
Ajax 방식의 요청은 정보가 json 방식으로 넘어오기 때문에 다시 객체로 변환해 주기위해 ObjectMapper를 사용하여 요청 정보의 값을 맵핑하여 인증 객체로 만들고 중요 정보인 username 이나 password 값이 비어있다면 IllegalArgumentException 예외를 던진다.
Ajax 요청을 처리할 전용 토큰 객체를 만들어 username 정보와 password 를 셋팅해주고 AuthenticationManager에게 토큰 객체를 넘긴다.
AjaxAuthenticationToken 객체로 Ajax 처리에 필요한 전용 인증 토큰 객체이다.
AbstractAuthenticationToken 객체를 상속 받고 하위 객체인 UsernamePasswordAuthenticationToken을 참고하여 AjaxAuthenticationToken을 만든다.
생성자가 2개가 있는데 파라미터를 2개 받는 생성자는 인증 전 사용자가 입력한 username, password 정보로 인증 객체를 생성하고 파라미터 3개를 받는 생성자는 인증 이후 인증 객체를 생성하는 생성자이다.
이제 SecurityConfig에 만든 Filter를 등록한다.
AjaxLoginProcessingFilter를 Bean으로 등록한다.
인증 토큰을 넘겨줄 AuthenticationManager 객체도 등록할 때 같이 설정해줘야 한다.
AuthenticationManager 객체도 Bean으로 생성하기 위해 AuthenticationConfiguration 객체를 사용하여 Manager 객체를 생성한다.
addFilterBefore API 설정으로 UsernamePasswordAuthenticationFilter 지정된 필터보다 새로 Bean으로 등록한 AjaxLoginProcessingFilter가 먼저 실행이 된다. 마치 지정된 필터 대신 커스텀 필터를 추가하는 것 처럼 메소드가 동작할 것 같지만 실제 오버라이드 되지 않고 커스텀 필터가 실행되고 인증이 완료되었기 때문에 UsernamePasswordAuthenticationfilter가 후에 수행되면서 인증완료 상태면 인증 로직이 수행되지 않고 자연스럽게 통과하기 때문에 오버라이드 된 것처럼 보여진다.
csrf설정은 Post 방식의 요청이 올 경우 csrf 토큰도 같이 넘겨주어 안전한 rest API 요청이 맞는지 확인하지만 일단 이 기능은 꺼두고 테스트 한다.
인텔리제이에서 Ajax 방식으로 요청을 보내는 툴을 사용하여 Rest API 요청을 보낸다.
AjaxAuthenticationProvider
실제 보안 인증처리를 담당할 AuthenticationProvider 객체를 만든다.
AuthenticationProvider를 구현하고 실제 formLogin 인증 로직과 크게 다를 것이 없기 때문에 CustomAuthenticationProvider 로직을 그대로 가져온다.
AjaxAuthenticationProvider에서 사용할 인증 객체만 UsernamePasswordAuthenticationToken -> AjaxAuthenticationProvider 객체로 수정한다.
기존 SecurityConfig에 ajax와 관련된 구문을 모두 빼고 authenticationProvider 객체를 formLogin 전용으로 만든 CustomAuthenticationProvider 객체로 설정한다.
Provider 객체가 2개가 생성되니 명시적으로 설정해주는 것 같다. 설정을 안하면 DaoAuthenticationProvider객체 로직을 탄다.
설정 순서는 @Order(1) 로 지정하여 더 세부적인 설정인 AjexSecurityConfig 객체가 우선순위가 되게 한다.
SecurityConfig.java 전체 코드
@Configuration
@EnableWebSecurity
@Order(1)
public class SecurityConfig {
private final UserRepository userRepository;
public SecurityConfig(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService customUserDetailsService(){
return new CustomUserDetailsService(userRepository);
}
@Bean
public AuthenticationProvider customAuthenticationProvider(){
return new CustomAuthenticationProvider(customUserDetailsService(), passwordEncoder());
}
@Bean
public AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> formAuthenticationDetailsSource() {
return new FormAuthenticationDetailsSource();
}
@Bean
public AuthenticationSuccessHandler successHandler() {
return new CustomAuthenticationSuccessHandler();
}
@Bean
public AuthenticationFailureHandler failureHandler() {
return new CustomAuthenticationFailureHandler();
}
@Bean
public AccessDeniedHandler accessDeniedHandler() {
CustomAccessDeniedHandler accessDeniedHandler = new CustomAccessDeniedHandler();
accessDeniedHandler.setErrorPage("/denied");
return accessDeniedHandler;
}
@Bean
@Order(1)
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(requests -> {
requests
.requestMatchers("/", "/users", "/login*", "/css/**", "/js/**", "/images/**").permitAll()
.requestMatchers("/mypage").hasRole("USER")
.requestMatchers("/messages").hasRole("MANAGER")
.requestMatchers("/config").hasRole("ADMIN")
.anyRequest().authenticated();
})
.formLogin(form -> {
form
.loginPage("/login")
.loginProcessingUrl("/login_proc")
// .defaultSuccessUrl("/", true)
// .defaultSuccessUrl("/")
.authenticationDetailsSource(formAuthenticationDetailsSource())
.successHandler(successHandler())
.failureHandler(failureHandler())
.permitAll();
})
.exceptionHandling(exception -> {
exception
.accessDeniedHandler(accessDeniedHandler());
})
.authenticationProvider(customAuthenticationProvider())
.build();
}
}
AjaxSecurityConfig.java 파일을 생성하고 @Order(0) 우선순위값 지정 후에 SecurityFilterChain을 Bean으로 등록하여 설정한다.
securityMatcher("/api/**") 설정은 /api/ 하위 URL 요청이 들어오면 현재 보안 설정이 동작하게 한다.
AjaxLogin 실제 인증과정을 거칠 AjaxAuthenticationProvider를 사용하기위해 AuthenticationManager를 생성하며 AjaxAuthenticationProvider에게 보안인증을 위임하기 위해 Provider를 추가한다.
AjaxLoginProcessingFilter 생성할 때 사용할 AuthenticationManager를 지정해준다. AjaxAuthenticationProvider에게 위임을 맞긴 Manager 객체로 설정한다.
AjexSecurityConfig 전체 코드
@Configuration
@Order(0)
public class AjaxSecurityConfig {
private final UserRepository userRepository;
private final AuthenticationConfiguration authenticationConfiguration;
public AjaxSecurityConfig(UserRepository userRepository, AuthenticationConfiguration authenticationConfiguration) {
this.userRepository = userRepository;
this.authenticationConfiguration = authenticationConfiguration;
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService customUserDetailsService(){
return new CustomUserDetailsService(userRepository);
}
@Bean
public AjaxAuthenticationProvider ajaxAuthenticationProvider(){
return new AjaxAuthenticationProvider(customUserDetailsService(), passwordEncoder());
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration)
throws Exception {
ProviderManager authenticationManager = (ProviderManager)authenticationConfiguration.getAuthenticationManager();
authenticationManager.getProviders().add(ajaxAuthenticationProvider());
return authenticationManager;
}
@Bean
public AjaxLoginProcessingFilter ajaxLoginProcessingFilter() throws Exception {
AjaxLoginProcessingFilter ajaxLoginProcessingFilter = new AjaxLoginProcessingFilter();
ajaxLoginProcessingFilter.setAuthenticationManager(authenticationManager(authenticationConfiguration));
return ajaxLoginProcessingFilter;
}
@Bean
@Order(0)
public SecurityFilterChain ajaxSecurityFilterChain(HttpSecurity http) throws Exception {
return http
.securityMatcher(AntPathRequestMatcher.antMatcher("/api/**"))
.authorizeHttpRequests(requests -> {
requests
.anyRequest().authenticated();
})
.addFilterBefore(ajaxLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.csrf(csrf -> {
csrf
.ignoringRequestMatchers("/api/**");
})
.build();
}
}
AjaxSecurityConfig 설정파일을 따로 분리하려 했지만 버전이 달라서 그런지 잘 안되서 SecurityConfig 파일 내부에 SecurityFilterChain Bean을 하나 더 만들고 Order 값을 지정했다.
securityMatcher("/api/**") 설정은 /api/ 하위 URL 요청에 대한 보안 설정이 동작하게 한다.
기존 설정의 addFilterBefore API 설정을 ajaxSecurityFilterChain에 설정하고 AjaxLogin 실제 인증과정을 거칠 AjaxAuthenticationProvider를 사용하기위해 AuthenticationManager를 생성하며 AjaxAuthenticationProvider를 추가한다.
AjaxLoginProcessingFilter 생성할 때 사용할 AuthenticationManager를 지정
AjaxAuthenticationSuccessHandler, AjaxAuthenticationFailureHandler
formLogin에서 설정한 AuthenticationFailureHandler, AuthenticationSuccessHandler와 로직은 크게 다를 것이 없지만 성공, 실패 후 루트 페이지로 리다이렉트 되는 것이 아닌 비동기 방식으로 인증에 성공한 결과값을 objectMapper가 Json 형식으로 변환하여 responseBody에 담아 클라이언트에 응답하는 것이 차이점이다.
응답할때 상태코드를 설정하는 부분은 성공시 상태값 200번을 넘기고 ContentType은 "application/json"으로 설정하여 넘긴다. 실패할 경우 코드값은 401번으로 인증실패 코드를 넘긴다.
AjaxSecurityConfig 설정에서 Filter 생성 부분에 Bean으로 생성한 Ajax 전용 성공핸들러와 실패 핸들러를 설정한다.
성공시 요청 결과값과 200 상태코드를 확인할 수 있다.
요청 실패시 요청 실패 메세지와 401 상태코드를 확인할 수 있다.
AjaxLoginUrlAuthenticationEntryPoint, AjaxAccessDeniedHandler
인증을 받지 못한 사용자가 인증이 필요한 자원에 접근했을 경우 스프링 시큐리티는 사용자가 인증을 받을 수 있도록 처리하는 객체가 AjaxLoginUrlAuthenticationEntryPoint.
인증을 받았지만 자원에 접근할 권한이 아닌 경우 처리하는 객체가 AjaxAxxessDeniedHandler 객체이다.
인증예외, 인가예외를 처리하는 필터인 ExceptionTranslationFilter 에서 AccessDeniedException 예외가 발생했을 경우 익명의 사용자라면 AuthenticationEntryPoint에서 처리하고 인증된 사용자라면 AccessDeniedHandler 에서 처리하도록 분기처리가 되어있다. 각각의 클래스를 AjaxLogin 처리에 맞게 구현하는 클래스를 만든다.
익명의 사용자일 경우 인증 실패 코드와 에러 메세지를 응답에 보낸다.
인증된 사용자가 권한이 맞지 않는 경우 접근 거부 코드와 에러 메세지를 응답에 보낸다.
AjaxSecurityConfig에 exceptionHandling 설정에서 AjaxLoginAuthenticationEntryPoint 객체와 AjaxAccessDeniedHandler 객체가 작업을 처리하도록 설정한다.
인증, 인가 테스트를 위해 "/api/message" url 경로 접근 권한을 "MANAGER" 로 설정한다.
GET 방식의 테스트할 경로를 설정하고 Json 형석의 데이터를 보내기 위해 @ResponseBody 어노테이션을 달아준다.
DB에 사용자를 추가하고
테스트할 요청 url과 헤더정보, 데이터를 설정한다.
- 로그인 없이 "/api/message" 자원에 접근해보고
- ROLE_USER 권한으로 "/api/message" 자원에 접근해보고
- ROLE_MANAGER 권한으로 "/api/message" 자원에 접근하여 인증, 인가 테스트를 진행한다.
테스트를 진행하는데 저번 문제처럼 뭔가 /error 경로로 넘어가서 LoginUrlAuthenticationEntryPoint 로 넘어가서 /login 경로로 넘어가는 바람에
SecurityConfig에 '/error/**" 경로 permitAll() 설정해줬다.
스프링 시큐리티6에 들어오면서 뭔가 로직이 바뀌었는지 왜 /error 경로로 자꾸 잡히는지 모르겠다..
그리고 user나 manager 로 로그인을 한 후에 "/api/message" 경로에 접근하려고 해도 인증객체가 null 이라고 표시가 되서 몇 시간을 헤메었는데..
Ajax 인증시 인가코드가 발급 되지 않는 원인 문의 - 인프런 | 질문 & 답변
Spring Authorization 1.0,1 기반으로 개발을 하고 있습니다.인가코드를 발급 할떄 FormLogin 기본 설정을 사용하면 인가코드가 발급이 되는데 Ajax 로 로그인을 하면 인가코드가 발급되지 않고 있습니다.
www.inflearn.com
스프링 시큐리티가 버전업이 되면서 SecurityContext를 세션에 담는 역할을 커스터마이징할 때 해주지 않아 직접 저장 객체를 설정해야 한다.
위에는 AjaxSecurityConfig.java 설정
AjaxLoginProcessingFilter.java
전체 코드
@Configuration
@Order(0)
public class AjaxSecurityConfig {
private final UserRepository userRepository;
private final AuthenticationConfiguration authenticationConfiguration;
public AjaxSecurityConfig(UserRepository userRepository, AuthenticationConfiguration authenticationConfiguration) {
this.userRepository = userRepository;
this.authenticationConfiguration = authenticationConfiguration;
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService customUserDetailsService(){
return new CustomUserDetailsService(userRepository);
}
@Bean
public AjaxAuthenticationProvider ajaxAuthenticationProvider(){
return new AjaxAuthenticationProvider(customUserDetailsService(), passwordEncoder());
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration)
throws Exception {
ProviderManager authenticationManager = (ProviderManager)authenticationConfiguration.getAuthenticationManager();
authenticationManager.getProviders().add(ajaxAuthenticationProvider());
return authenticationManager;
}
@Bean
public AjaxLoginProcessingFilter ajaxLoginProcessingFilter(HttpSecurity http) throws Exception {
AjaxLoginProcessingFilter ajaxLoginProcessingFilter = new AjaxLoginProcessingFilter(http,
authenticationConfiguration.getAuthenticationManager());
ajaxLoginProcessingFilter.setAuthenticationManager(authenticationManager(authenticationConfiguration));
ajaxLoginProcessingFilter.setAuthenticationSuccessHandler(ajaxSuccessHandler());
ajaxLoginProcessingFilter.setAuthenticationFailureHandler(ajaxFailureHandler());
return ajaxLoginProcessingFilter;
}
@Bean
public AuthenticationSuccessHandler ajaxSuccessHandler() {
return new AjaxAuthenticationSuccessHandler();
}
@Bean
public AuthenticationFailureHandler ajaxFailureHandler() {
return new AjaxAuthenticationFailureHandler();
}
@Bean
public AuthenticationEntryPoint authenticationEntryPoint() {
return new AjaxLoginAuthenticationEntryPoint();
}
@Bean
public AccessDeniedHandler ajaxAccessDeniedHandler() {
return new AjaxAccessDeniedHandler();
}
@Bean
@Order(0)
public SecurityFilterChain ajaxSecurityFilterChain(HttpSecurity http) throws Exception {
return http
.securityMatcher(AntPathRequestMatcher.antMatcher("/api/**"))
.authorizeHttpRequests(requests -> {
requests
.requestMatchers("/api/messages").hasRole("MANAGER")
.anyRequest().authenticated();
})
.addFilterBefore(ajaxLoginProcessingFilter(http), UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(exception -> {
exception
.authenticationEntryPoint(authenticationEntryPoint())
.accessDeniedHandler(ajaxAccessDeniedHandler());
})
.csrf(csrf -> {
csrf
.ignoringRequestMatchers("/api/**");
})
.build();
}
}
public class AjaxLoginProcessingFilter extends AbstractAuthenticationProcessingFilter {
private final ObjectMapper objectMapper = new ObjectMapper();
public AjaxLoginProcessingFilter(HttpSecurity http, AuthenticationManager authenticationManager) {
super(new AntPathRequestMatcher("/api/login"));
setSecurityContextRepository(getSecurityContextRepository(http));
}
public AjaxLoginProcessingFilter(AuthenticationManager authenticationManager){
super(new AntPathRequestMatcher("/api/login"), authenticationManager);
}
SecurityContextRepository getSecurityContextRepository(HttpSecurity http) {
SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class);
if(securityContextRepository == null){
securityContextRepository = new DelegatingSecurityContextRepository(
new RequestAttributeSecurityContextRepository(), new HttpSessionSecurityContextRepository());
}
return securityContextRepository;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws
AuthenticationException, IOException, ServletException {
if(!isAjax(request)){
throw new IllegalStateException("Authentication is not supported");
}
AccountDto accountDto = objectMapper.readValue(request.getReader(), AccountDto.class);
if(StringUtils.isEmpty(accountDto.getUsername()) || StringUtils.isEmpty(accountDto.getPassword())){
throw new IllegalArgumentException("Username or Password is empty");
}
AjaxAuthenticationToken ajaxAuthenticationToken = new AjaxAuthenticationToken(accountDto.getUsername(),
accountDto.getPassword());
return getAuthenticationManager().authenticate(ajaxAuthenticationToken);
}
private boolean isAjax(HttpServletRequest request) {
return "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
}
}
익명의 사용자가 "/api/messages" 경로에 접근하면
ExceptionTranslationFilter에서 AccessDeniedException 예외가 발생하여 익명의 사용자일 경우 AuthenticationEntryPoint로 처리하는 로직으로 넘어간다.
AuthenticationEntryPoint는 직접 구현한 AjaxLoginAuthenticationEntry 객체가 불러와지는 것을 볼 수 있다.
응답 결과
유저로 로그인한 후 "/api/message" 경로에 접근하면
인증된 사용자로 AccessDeniedHandler가 처리를 진행하며 역시 직접 구현한 AjaxAccessDeniedHandler 객체에서 처리된다.
응답 결과
매니저로 로그인 후 "/api/message" 경로에 접근하면
자원에 접근 가능
응답 결과
DSL로 Config 설정
- Custom DSLs
- 도메인 전용 언어 (Domain-Specific Language, DSL)
- 특정 비지니스 도메인의 문제를 해결하려고 만든 언어
- 특정 비지니스 도메인을 인터페이스로 만든 API
- 도메인을 표현할 수 있는 클래스와 메서드 집합이 필요하다.
- DSL의 장점
- 간결함 : API는 비즈니스 로직을 간편하게 캡슐화하므로 반복을 피할 수 있고 코드를 간결하게 만들 수 있다.
- 가독성 : 도메인 영역의 용어를 사용하므로 비 도메인 전문가도 코드를 쉽게 이해할 수 있다. 다양한 조직 구성원 간에 코드와 도메인 영역이 공유될 수 있다.
- 유지보수 : 잘 설계된 DSL로 구현한 코드는 쉽게 유지 보수하고 바꿀 수 있다.
- 높은 수준의 추상화 : DSL은 도메인과 같은 추상화 수준에서 동작하므로 도메인의 문제와 직접적으로 관련되지 않은 세부 사항을 숨긴다.
- 집중 : 비즈니스 도메인의 규칙을 표현할 목적으로 설계된 언어이므로 프로그래머가 특정 코드에 집중할 수 있다.
- 관심사 분리(SoC) : 지정된 언어로 비즈니스 로직을 표현함으로 애플리케이션의 인프라구조와 관련된 문제와 독립적으로 비즈니스 관련된 코드에서 집중하기가 용이하다.
- DSL의 단점
- DSL 설계의 어려움 : 간결하게 제한적인 언어에 도메인 지식을 담는 것이 쉬운 작업은 아니다.
- 개발 비용 : 코드에 DSL을 추가하는 작업은 초기 프로젝트에 많은 비용과 시간이 소모된다. 또한 DSL 유지보수와 변경은 프로젝트에 부담을 주는 요소다.
- 추가 우회 계층 : DSL은 추가적인 계층으로 도메인 모델을 감싸며 이때 계층을 최대한 작게 만들어 성능 문제를 회피한다.
- 새로 배워야 하는 언어 : DSL을 프로젝트에 추가하면서 팀이 배워야 하는 언어가 한 개 더 늘어난다는 부담이 있다.
- 호스팅 언어 한계 : 일부 자바 같은 범용 프로그래밍 언어는 장황하고 엄격한 문법을 가졌다. 이런 언어로는 사용자 친화적 DSL을 만들기가 힘들다.
- AbstractHttpConfigurer
- 스프링 시큐리티 초기화 설정 클래스를 상속받아 Custom DSLs를 구현한다.
- 필터, 핸들러, 메서드, 속성 등을 한 곳에 정의하여 처리할 수 있는 편리함 제공
- public void init(H http) throws Exception - 초기화
- public void configure(H http) - 설정
- 도메인 전용 언어 (Domain-Specific Language, DSL)
- HttpSecurity의 apply(C configurer) 메서드 사용
https://ckddn9496.tistory.com/134
(모던 자바 인 액션) Chapter 10 람다를 이용한 도메인 전용 언어
도메인 전용 언어 (domain-specific language, DSL) 특정 비즈니스 도메인의 문제를 해결하려고 만든 언어 특정 비스니스 도메인을 인터페이스로 만든 API 도메인을 표현할 수 있는 클래스와 메서드 집합
ckddn9496.tistory.com
https://docs.spring.io/spring-security/reference/servlet/configuration/java.html#jc-custom-dsls
Java Configuration :: Spring Security
Spring Security’s Java configuration does not expose every property of every object that it configures. This simplifies the configuration for a majority of users. After all, if every property were exposed, users could use standard bean configuration. Whi
docs.spring.io
Custom DSLs 사용을 위한 AjaxLoginConfigurer 클래스
AbstractHttpConfigurer를 구현하는 AbstractAuthenticationFilterConfigurer를 상속받아 구현.
생성자에 AjaxLoginProcessingFilter를 생성하여 부모클래스에 넘겨준다.
configure 메서드에 HttpSecurity 객체가 등록되고 내부에 공유객체가 저장되고 가져오는 API를 제공해주는데 만약 AuthenticationManager가 null 이라면 공유객체에서 AuthenticationManager를 가져온다.
AjaxLogin 인증에 필요한 매니저와 핸들러들을 AjaxLoginProcessingFilter에 설정하고 세션설정이나 리멤버미 설정 후
공유객체에 AjaxLoginProcessingFilter 클래스를 저장하고 UsernamePasswordAuthenticationFilter 앞에 AjaxLoginProcessingFilter가 오는 설정해준다.
각 매니저와 핸들러를 등록하는 메서드를 만들고 LoginUrl 설정 메서드도 구현한다.
AjaxLoginConfigurer 전체 코드
public class AjaxLoginConfigurer<H extends HttpSecurityBuilder<H>> extends
AbstractAuthenticationFilterConfigurer<H, AjaxLoginConfigurer<H>, AjaxLoginProcessingFilter> {
private AuthenticationSuccessHandler successHandler;
private AuthenticationFailureHandler failureHandler;
private AuthenticationManager authenticationManager;
public AjaxLoginConfigurer(HttpSecurity http, AuthenticationManager authenticationManager) {
super(new AjaxLoginProcessingFilter(http, authenticationManager), null);
}
@Override
public void init(H http) throws Exception {
super.init(http);
}
@Override
public void configure(H http) {
if(authenticationManager == null){
authenticationManager = http.getSharedObject(AuthenticationManager.class);
}
getAuthenticationFilter().setAuthenticationManager(authenticationManager);
getAuthenticationFilter().setAuthenticationSuccessHandler(successHandler);
getAuthenticationFilter().setAuthenticationFailureHandler(failureHandler);
SessionAuthenticationStrategy sessionAuthenticationStrategy = http.getSharedObject(SessionAuthenticationStrategy.class);
if(sessionAuthenticationStrategy != null){
getAuthenticationFilter().setSessionAuthenticationStrategy(sessionAuthenticationStrategy);
}
RememberMeServices rememberMeServices = http.getSharedObject(RememberMeServices.class);
if(rememberMeServices != null){
getAuthenticationFilter().setRememberMeServices(rememberMeServices);
}
http.setSharedObject(AjaxLoginProcessingFilter.class, getAuthenticationFilter());
http.addFilterBefore(getAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
public AjaxLoginConfigurer<H> successHandlerAjax(AuthenticationSuccessHandler successHandler){
this.successHandler = successHandler;
return this;
}
public AjaxLoginConfigurer<H> failureHandlerAjax(AuthenticationFailureHandler authenticationFailureHandler){
this.failureHandler = authenticationFailureHandler;
return this;
}
public AjaxLoginConfigurer<H> setAuthenticationManager(AuthenticationManager authenticationManager){
this.authenticationManager = authenticationManager;
return this;
}
@Override
protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl) {
return new AntPathRequestMatcher(loginProcessingUrl, "POST");
}
}
AjaxSecurityConfig 클래스에 AjaxLoginConfigurer 설정
customConfigurerAjax 메서드를 만들어서 apply 설정에 AjaxLoginConfigurer 클래스를 넘겨주고 각 핸들러와 매니저, loginProcessingUrl 설정을 해주고 SecurityFilterChain에 customCongifurerAjax 메서드가 실행되게 선언하고 HttpSecurity 객체를 넘겨준다.
기존에 설정했는 구문을 주석처리 한다.
AjaxSecurityConfig 전체 코드
@Configuration
@Order(0)
public class AjaxSecurityConfig {
private final UserRepository userRepository;
private final AuthenticationConfiguration authenticationConfiguration;
public AjaxSecurityConfig(UserRepository userRepository, AuthenticationConfiguration authenticationConfiguration) {
this.userRepository = userRepository;
this.authenticationConfiguration = authenticationConfiguration;
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService customUserDetailsService(){
return new CustomUserDetailsService(userRepository);
}
@Bean
public AjaxAuthenticationProvider ajaxAuthenticationProvider(){
return new AjaxAuthenticationProvider(customUserDetailsService(), passwordEncoder());
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration)
throws Exception {
ProviderManager authenticationManager = (ProviderManager)authenticationConfiguration.getAuthenticationManager();
authenticationManager.getProviders().add(ajaxAuthenticationProvider());
return authenticationManager;
}
/*@Bean
public AjaxLoginProcessingFilter ajaxLoginProcessingFilter(HttpSecurity http) throws Exception {
AjaxLoginProcessingFilter ajaxLoginProcessingFilter = new AjaxLoginProcessingFilter(http,
authenticationConfiguration.getAuthenticationManager());
ajaxLoginProcessingFilter.setAuthenticationManager(authenticationManager(authenticationConfiguration));
ajaxLoginProcessingFilter.setAuthenticationSuccessHandler(ajaxSuccessHandler());
ajaxLoginProcessingFilter.setAuthenticationFailureHandler(ajaxFailureHandler());
return ajaxLoginProcessingFilter;
}*/
@Bean
public AuthenticationSuccessHandler ajaxSuccessHandler() {
return new AjaxAuthenticationSuccessHandler();
}
@Bean
public AuthenticationFailureHandler ajaxFailureHandler() {
return new AjaxAuthenticationFailureHandler();
}
@Bean
public AuthenticationEntryPoint authenticationEntryPoint() {
return new AjaxLoginAuthenticationEntryPoint();
}
@Bean
public AccessDeniedHandler ajaxAccessDeniedHandler() {
return new AjaxAccessDeniedHandler();
}
@Bean
@Order(0)
public SecurityFilterChain ajaxSecurityFilterChain(HttpSecurity http) throws Exception {
customConfigurerAjax(http);
return http
.securityMatcher(AntPathRequestMatcher.antMatcher("/api/**"))
.authorizeHttpRequests(requests -> {
requests
.requestMatchers("/api/messages").hasRole("MANAGER")
.anyRequest().authenticated();
})
// .addFilterBefore(ajaxLoginProcessingFilter(http), UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(exception -> {
exception
.authenticationEntryPoint(authenticationEntryPoint())
.accessDeniedHandler(ajaxAccessDeniedHandler());
})
.csrf(csrf -> {
csrf
.ignoringRequestMatchers("/api/**");
})
.build();
}
private void customConfigurerAjax(HttpSecurity http) throws Exception {
http
.apply(new AjaxLoginConfigurer<>(http, authenticationConfiguration.getAuthenticationManager()))
.successHandlerAjax(ajaxSuccessHandler())
.failureHandlerAjax(ajaxFailureHandler())
.setAuthenticationManager(authenticationManager(authenticationConfiguration))
.loginProcessingUrl("/api/login");
}
}
로그인 Ajax 구현 & CSRF
화면에서 비동기 방식으로 API 호출하기 위해 필요한 요청 헤더 설정을 한다.
- 헤더 설정
- 전송 방식이 Ajax 인지의 여부를 위한 헤더 설정
- AjaxLoginProcessingFilter 클래스에 isAjax 메서드로 특정 헤더값을 가져옴
- xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
- CSRF 헤더 설정
- Ajax 방식으로 인증할 때는 직접 csrf 값을 생성해서 전달해줘야 한다.
- <meta id="_csrf" name="_csrf" th:content="${_csrf.token}"/>
- <meta id="_csrf_header" name="_csrf_header" th:content="${_csrf.headerName}" />
- var csrfHeader = ${'meta[name="_csrf_header"]').attr('content');
- var csrfToken = $('meta[name="_csrf"]').attr('content');
- xhr.setRequestHeader(csrfHeader, scrfToken);
login.html 에 submit 방식의 로그인 대신 button 클릭시 formLogin 펑션이 실행되는 로그인 방식으로 변경하고
로그인 정보는 json형식으로 보내고 csrf 정보를 헤더에 담아 ajax 호출하고 실패한다면 에러 텍스트 노출, 성공하면 루트경로로 이동한다.
csrf 메타정보는 공통 header.html 레이아웃에 선언하였다.
top.html 메뉴 공통 레이아웃 html 에서 formLogin 방식의 경로가 아닌 api 로그인 경로로 변경한다.
home.html 파일도 메세지 경로에 접근 할 시 api 요청으로 변경하여 에러일 경우 인증에러와 인가에러 경로로 이동하고 성공시에만 messages 경로에 접근 할 수 있다.
LoginController 에서 api 경로 설정도 추가해 준다.
MessageController 에서 /api/messages 경로로 POST 방식으로 접근하면 HTTP 응답 상태코드 200과 바디에 ok라는 문자를 담은 응답객체를 클라이언트로 응답해준다.
AjaxSecurityConfig 설정에서는 우선 /error 경로로 시작되는 모든경로와 /api/login 접근을 모두 허용하고
csrf 기능을 사용할 수 있게 기존에 무시설정을 주석 처리한다.
서버 가동 후 메인페이지에서 개발자도구(F12)를 열어 브라우져 브레이크 포인트를 잡고
인증이 안된 사용자의 리소스 접근, 권한이 없는 사용자의 리소스 접근, 권한 있는 사용자의 리소스 접근을 확인해본다.
/api/messages 경로에 인증이 안된 사용자가 접근할 경우 csrfHeader 값과 csrfToken 값을 잘 받아오고
csrf 토큰 처리 잘 넘어가지만
ExceptionTranslationFilter 에서 인증 예외에 걸리고
AjaxLoginAuthenticationEntryPoint 에서 에러 값 설정 후 응답하면
브라우저 401 인증예외 에러 구문에 걸리고
로그인페이지로 이동하여 에러 메세지를 출력한다.
user로 로그인 하면
Ajax 요청인지 확인 후
SuccessHandler 인증 성공 핸들러를 호출하며 로그인 처리가 된다.
user 사용자가 다시 /api/messages 경로에 접근하면
인가 예외 핸들러를 호출하고
AjaxAccessDeniedHandler 에서 권한 예외 코드 설정과 에러 메세지를 담아 응답한다.
브라우저에서는 403 응답코드 에러로 확인하여 /api/denied 경로로 이동 시키고
로그인 컨트롤러에서 /api/denied 경로로 접근되어 인증객체 정보를 가져와서 모델에 담고 에러 메세지도 담고 denied 페이지로 넘긴다.
예외 메세지를 뿌린다.
manager로 로그인한 후에 메시지 페이지에 접근하게 되면
인증, 인가 모두 통과되며 성공 응답 코드와 ok 문자를 응답 객체에 담아 넘기고
브라우저에서 /messages 경로로 이동시킨다.
'자바 > 스프링 시큐리티' 카테고리의 다른 글
실전프로젝트 - 인증 프로세스 Form 인증 구현 (0) | 2023.07.07 |
---|---|
스프링 시큐리티 주요 아키텍쳐 이해 (0) | 2023.06.21 |
스프링 시큐리티 기본 API 및 Filter 이해 (0) | 2023.06.11 |