▸Spring Security

스프링 Security_Remember-me 커스터마이징(커스텀 필터) [3/3]

코데방 2020. 4. 22.
728x90
- Develop OS : Windows10 Ent, 64bit
- WEB/WAS Server : Tomcat v9.0
- DBMS : MySQL 5.7.29 for Linux (Docker)
- Language : JAVA 1.8 (JDK 1.8)
- Framwork : Spring 3.1.1 Release
- Build Tool : Maven 3.6.3
- ORM : Mybatis 3.2.8

 

이전 글에서는 Remember-me 구현체를 커스터마이징 완료했습니다. 이제 남은 로직은 인증메일에서 "인증하기"를 눌렀을 때 DB 테이블에서 해당 series의 certified 컬럼값을 'true'로 업데이트 해주기만 하면 됩니다.

 

링크 주소에 암호화된 "series:token" 값을 파라미터로 보내주는수밖에 없는데, 이 때 고민거리가 하나 생길 수밖에 없습니다. URI를 통한 요청이므로 MVC패턴 안에서 컨트롤러가 받아서 처리해주는 방법이 가장 간단하지만 Spring Security의 인증 로직 일부를 MVC에 넘기는 것이 아무래도 꽤나 찝찝합니다. 

 

 

 

따라서 이 로직 또한 Spring Security 안에 담기 위해서 새로운 커스텀 필터를 하나 만들어서 넣어줍니다. 다행히 Spring Security는 쉽게 커스텀 필터를 삽입할 수 있도록 허용해주고 있습니다.

 

 


 

 

 

1. 필터 클래스 생성

 

Spring Security의 모든 필터는 'GenericFilterBean' 추상 클래스를 상속받습니다. 필터기능에 필요한 여러 기능을 구현해주고 있어 상속받은 뒤 doFilter() 메소드만 오버라이딩해주면 됩니다. 

 

다른 필터들을 보면 추상 클래스의 다른 메소드들도 필요에 따라 오버라이딩하는 경우가 있지만, 가장 간단한 필터인 LogoutFilter를 살펴보면 doFilter() 메소드 하나만 오버라이딩하고 있는 것으로 보아 다른 메소드는 확실하게 선택사항임을 알 수 있습니다.

 

여러 보안적인 부분은 앞의 필터들이 모두 처리할 것이고, 인증 메일에 관한 부분만 처리해줄 간단한 필터를 가장 마지막 순서에 동작시킬 것이므로 아래와 같이 간단하게 doFilter() 메소드만 작성해주기로 합니다.

 

package hs.spring.hsweb.service.user;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.stereotype.Service;
import org.springframework.web.filter.GenericFilterBean;

import hs.spring.hsweb.mapper.user.UserRememberMeMapper;
import hs.spring.hsweb.mapper.vo.user.UserRememberMeVO;

@Service
/* 인증 메일에서 '인증하기' 누를 경우 해당 토큰을 인증해주는 필터 */
public class UserLoginRememberMeCertifyingFilter extends GenericFilterBean {

	@Autowired
	/* 인증 메일 객체 (디코딩 메소드 사용) */
	// 인증 필터와 메일은 한 세트이므로 의존 관계로 엮어줌
	UserLoginRememberMeCertifyingEmail emailSender;

	@Autowired
	/* DB 작업을 위한 mapper */
	UserRememberMeMapper mapper;

	@Override
	public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {
		
        
        
		// 로직 작성
	
		
		chain.doFilter(request, response);
	}
}

 

 

필터 클래스의 끝에는 항상 필터체인의 doFilter() 메소드를 실행해줘야 다음 단계로 넘어갈 수 있습니다.

 

[Spring Security] - 스프링 필터와 스프링 시큐리티(Spring Security)의 동작 구조

 

 


 

2. 로직 작성

 

이 부분은 수행하고 싶은 로직을 작성해주면 됩니다. 저는 인증메일에서 요청한 URI가 있다면 파라미터 값을 디코딩해 series와 token값을 확인한 뒤 모두 일치하는 값이 있다면 해당 데이터의 certified 컬럼값을 'true'로 업데이트 시켜주도록 했습니다. 

 

package hs.spring.hsweb.service.user;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.stereotype.Service;
import org.springframework.web.filter.GenericFilterBean;

import hs.spring.hsweb.mapper.user.UserRememberMeMapper;
import hs.spring.hsweb.mapper.vo.user.UserRememberMeVO;

@Service
/* 인증 메일에서 '인증하기' 누를 경우 해당 토큰을 인증해주는 필터 */
public class UserLoginRememberMeCertifyingFilter extends GenericFilterBean {

	@Autowired
	/* 인증 메일 객체 (디코딩 메소드 사용) */
	// 인증 필터와 메일은 한 세트이므로 의존 관계로 엮어줌
	UserLoginRememberMeCertifyingEmail emailSender;

	@Autowired
	/* DB 작업을 위한 mapper */
	UserRememberMeMapper mapper;

	@Override
	public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {

		HttpServletRequest req = (HttpServletRequest) request;
		String uri = req.getRequestURI();

		// 이메일에서 링크 클릭했을 경우
		if (uri.equals("/rememberMeCertifying") && request.getParameter("key") != null) {

			// seriesm token 값 추출
			String[] decodedSeries = emailSender.decodeValues(request.getParameter("key"));

			// 정상적인 값일 경우에만 적용
			if (decodedSeries != null && decodedSeries.length == 2) {

				String series = decodedSeries[0];
				String token = decodedSeries[1];

				// token 정보 확인
				UserRememberMeVO rememberVO = mapper.selectUserToken(series);
				if (token.equals(rememberVO.getToken())) {

					// 인증 상태값 업데이트
					try {
						mapper.updateUserCertifying(series);
					} catch (DataAccessException e) {
						e.printStackTrace();
					}
				}
			}
		}

		chain.doFilter(request, response);
	}
}

 

 

 


 

 

 

3. 컨텍스트 설정 파일에서 커스텀 필터 등록

 

커스텀 필터는 아래와 같이 등록해주면 됩니다. 옵션은 아래와 같고 필터들의 Alias 이름은 Docs에 나와 있습니다. 

 

* after : 해당 필터 뒤 순서에 동작

* before : 해당 필터 앞 순서에 동작

* position : 해당 필터를 대체

 

 

position의 경우 아래 세 개의 필터는 대체할 수 없습니다. 

 

- SecurityContextPersistenceFilter

- ExceptionTranslationFilter

- FilterSecurityInterceptor

 

 

저는 인터셉터 필터 다음에 실행되도록 했습니다.

<!-- 자동 로그인(커스텀) -->
<s:remember-me services-ref="userLoginRememberMeService" key="codevang"
	authentication-success-handler-ref="userLoginSuccessHandler" />

<!-- 인증 메일의 인증하기 요청 처리 커스텀 필터 -->
<s:custom-filter after="FILTER_SECURITY_INTERCEPTOR"
	ref="userLoginRememberMeCertifyingFilter" />

 

 


 

 

4. 컨트롤러에서 URI 처리

 

로그인 인터셉터나 로그아웃같이 해당 URI를 리다이렉트 시켜주는 로직이 없으므로 처리된 요청은 그대로 서블릿으로 전달됩니다. 따라서 컨트롤러에서 해당 인증 요청에 대한 URI를 리다이렉트 시켜주면 됩니다. 저는 간단히 메인화면으로 리다이렉트 시켰습니다. 인증 성공 페이지 등을 만들면 그 페이지로 리다이렉트 시켜줘도 됩니다.

 

메일 인증은 요청한 기기 외 다른 기기에서 할 수 있으므로 로그인이 필수인 페이지로 이동시키는 것은 좋지 않을 것 같습니다.

	/* 메일 인증 리다이렉트 */
	@RequestMapping("/rememberMeCertifying")
	public String rememberMeCertifying() {
		
		return "redirect:/";
	}

 

 


 

 

이것으로 Remember-me의 모든 커스터마이징이 완료되었습니다. 추상 클래스 레벨을 대체할 수 있는 클래스를 만드는 것도 약간 번거롭긴 하지만 어렵지는 않으니 필요에 따라 기존 코드를 참조해서 작성하면 됩니다. 기존 클래스의 구조만 잘 파악할 수 있다면 어렵지는 않습니다.  

728x90

댓글

💲 추천 글