서블릿 필터
요구사항을 보면 로그인 한 사용자만 상품 관리 페이지에 들어갈 수 있어야 한다.
앞에서 로그인을 하지 않은 사용자에게는 상품 관리 버튼이 보이지 않기 때문에 문제가 없어 보인다.
그런데 문제는 로그인 하지 않은 사용자도 다음 URL을 직접 호출하면 상품 관리 화면에 들어갈 수 있다는 점이다.
상품 관리 컨트롤러에서 로그인 여부를 체크하는 로직을 하나하나 작성하면 되겠지만,
등록, 수정, 삭제, 조회 등등 상품관리의 모든 컨트롤러 로직에 공통으로 로그인 여부를 확인해야 한다.
더 큰 문제는 향후 로그인과 관련된 로직이 변경될 때 이다. 작성한 모든 로직을 다 수정해야 할 수 있다.
이렇게 애플리케이션 여러 로직에서 공통으로 관심이 있는 있는 것을 공통 관심사(cross-cutting concern)라고 한다.
여기서는 등록, 수정, 삭제, 조회 등등 여러 로직에서 공통으로 인증에 대해서 관심을 가지고 있다
이러한 공통 관심사는 스프링의 AOP로도 해결할 수 있지만,
웹과 관련된 공통 관심사는 지금부터 설명할 서블릿 필터 또는 스프링 인터셉터를 사용하는 것이 좋다.
웹과 관련된 공통 관심사를 처리할 때는 HTTP의 헤더나 URL의 정보들이 필요한데,
서블릿 필터나 스프링 인터셉터는 HttpServletRequest 를 제공한다
(필터는 서블릿이 제공하는 기능, 인터셉터는 스프링이 제공하는 기능)
서블릿 필터 소개
필터는 서블릿이 지원하는 수문장이다. 필터의 특성은 다음과 같다.
필터 흐름
http 요청이 오면 요청이 was(웹어플리케이션서버)로 들어가고 was가 필터를 호출한다.
그다음 서블릿이 호출되고, 컨트롤러 순이다. (필터 == 로직)
필터를 적용하면 필터가 호출 된 다음에 서블릿이 호출된다.
그래서 모든 고객의 요청 로그를 남기는 요구사항이 있다면 필터를 사용하면 된다.
(모든 http요청(request)이 들어오면서 필터를 먼저 만나니까)
참고로 필터는 특정 URL 패턴에 적용할 수 있다. /* 이라고 하면 모든 요청에 필터가 적용된다.
(/* 로 설정하면 모든 http요청에 필터가 적용된다.)
참고로 스프링을 사용하는 경우 여기서 말하는 서블릿은 스프링의 디스패처 서블릿으로 생각하면 된다.
(디스패처 서블릿 == 프론트컨트롤러)
필터 제한
로그인사용자는 필터에서 걸리지않아 서블릿,컨트롤러가 호출되는데
비 로그인 사용자는 필터에서 걸리기 때문에 서블릿이 호출되지않고, 서블릿이 호출되지않으므로 컨트롤러 또한 호출되지않는다.
필터에서 적절하지 않은 요청이라고 판단하면 거기에서 끝을 낼 수도 있다. 그래서 로그인 여부를 체크하기에 딱 좋다.
필터 체인
필터는 체인으로 구성되는데, 중간에 필터를 자유롭게 추가할 수 있다.
예를 들어서 로그를 남기는 필터를 먼저 적용하고, 그 다음에 로그인 여부를 체크하는 필터를 만들 수 있다.
(여러개의 필터를 순차적으로 적용할 수 있다)
필터 인터페이스
public interface Filter {
public default void init(FilterConfig filterConfig) throws ServletException {}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException;
public default void destroy() {}
}
필터 인터페이스를 구현하고 등록하면 서블릿 컨테이너가 필터를 싱글톤 객체로 생성하고, 관리한다.
was가 dofilter를 호출해서 필터들을 통과시킨다. 필터를 통과하면 서블릿,컨트롤러호출 ... 이런식
필터 초기화 메서드 => 서블릿 컨테이너가 생성될 때 호출된다.
init(FilterConfig filterConfig)
고객의 요청이 올 때 마다 해당 메서드가 호출된다. 필터의 로직을 구현하면 된다
doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException;
필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출된다.
destroy()
서블릿 필터 - 요청 로그
필터가 정말 수문장 역할을 잘 하는지 확인하기 위해 가장 단순한 필터인,
모든 요청을 로그로 남기는 필터를 개발하고 적용해보자.
LogFilter - 로그 필터
@Slf4j
public class LogFilter implements Filter { //servlet의 filter를 implements해야한다.
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("log filter init");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("log filter doFilter");
// ServletRequest 는 HttpServletRequest 의 부모이다. 기능이 별로 없어서 다운캐스팅해줘야한다.
// 서블릿이 http 뿐만아니라 다른 요청도 받을 수 있게끔 설계가 되어있어서 기본이 ServletRequest 인것이다.
HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestURI = httpRequest.getRequestURI();
String uuid = UUID.randomUUID().toString(); // 요청온것을 구분하기 위해 UUID생성
//요청(request)가 들어오면 로그로 요청정보를 찍어보고, dofilter()를 통해 다음 필터 호출 해준다, 다음 필터가 없다면 서블릿을 호출해준다.
try {
log.info("REQUEST [{}] [{}]", uuid, requestURI);
chain.doFilter(request,response); // 다음 필터 호출 해준다, 다음 필터가 없다면 서블릿을 호출해준다.
} catch (Exception e) {
throw e;
} finally { //예외가 발생했는지 안했는지 상관없이 반드시 실행되는 부분
log.info("RESPONSE [{}] [{}]", uuid, requestURI);
}
}
@Override
public void destroy() {
log.info("log filter destroy");
}
}
public class LogFilter implements Filter {
필터를 사용하려면 필터 인터페이스를 구현해야 한다.
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HTTP 요청이 오면 doFilter 가 호출된다.
ServletRequest request 는 HTTP 요청이 아닌 경우까지 고려해서 만든 인터페이스이다.
HTTP를 사용하면 HttpServletRequest httpRequest = (HttpServletRequest) request; 와 같이 다운 케스팅 하면 된다.
String uuid = UUID.randomUUID().toString();
HTTP 요청을 구분하기 위해 요청당 임의의 uuid 를 생성해둔다.
log.info("REQUEST [{}] [{}]", uuid, requestURI);
uuid 와 requestURI 를 출력한다.
chain.doFilter(request,response);
이 부분이 가장 중요하다.
다음 필터가 있으면 필터를 호출하고, 필터가 없으면 서블릿을 호출한다. (스프링이니까 디스패처 서블릿을 호출)
만약 이 로직을 호출하지 않으면 다음 단계로 진행되지 않는다.
WebConfig - 필터 설정
@Configuration
public class WebConfig {
// 직접 만든 필터를 등록해준다 자동 빈등록을 해주지않는 이유는 LogFilter 객체를 만들어서 스프링빈에 등록하려는게 아니고
// FilterRegistrationBean객체를 logFilter라는 스프링빈이름으로 등록하려고 하기 떄문인것같다.
// 스프링컨테이너에는 각 필터별 들이 스프링빈이름은 필터이름으로 가지고 , 스프링빈안에 빈객체는 FilterRegistrationBean를 가지며 그안에 필터객체,필터정보가 들어있는것이다.
//톰캣이 구동될때 스프링컨테이너에서 FilterRegistrationBean을 빈객체로 가지는 bean을 가져와서 FilterRegistrationBean안에 들어있는 필터를 서블릿 컨테이너에 추가하게된다.
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>(); //필터 등록용 빈을 만든다.
filterRegistrationBean.setFilter(new LogFilter()); // 내가만든 필터를 넣어주고
filterRegistrationBean.setOrder(1); // 필터의 순서도 넣어준다.
filterRegistrationBean.addUrlPatterns("/*"); // 어떤 url패턴에 적용할것인지 설정, /*는 모든 url를 의미한다.
return filterRegistrationBean;
}
}
https://keeeeeepgoing.tistory.com/104
필터를 등록하는 방법은 여러가지가 있지만,
스프링 부트를 사용한다면 FilterRegistrationBean 을 사용해서 등록하면 된다.
filterRegistrationBean.setFilter(new LogFilter());
등록할 필터를 지정한다.
filterRegistrationBean.setOrder(1);
필터는 체인으로 동작한다. 따라서 순서가 필요하다. 낮을 수록 먼저 동작한다.
filterRegistrationBean.addUrlPatterns("/*");
필터를 적용할 URL 패턴을 지정한다. 한번에 여러 패턴을 지정할 수 있다.
실행 테스트
localhost:8080
즉 홈화면에서 새로고침을 해보자.
필터를 등록할 때 urlPattern 을 /* 로 등록했기 때문에 모든 요청에 해당 필터가 적용된다.
localhost:8080로 url 호출을 했더니 uri가 / 로 나오는것을 확인할 수 있다.
밑의 css내용이 나오는 이유는 리소스에 대한 요청이라고 한다.
실행 테스트2
상품등록에서 아무값도 입력하지않고 저장을 눌러 에러를 발생시켜보자.
결과는 다음과 같다.
즉 log.info로 REQUEST관련 로그를 찍고 난후 (현재 필터인 logFilter()를 통과했고 == 로그를 찍었다는것에서 현재 필터는 통과라는것이다.)
그다음 필터로 이동하기위해 chain.doFilter()를 호출한다.
그렇게 모든 필터를 통과하고 난뒤에 서블릿,컨트롤러가 호출된다.
컨트롤러가 호출됬다는 증거로 비어있는 값들에 대한 필드에러 로그를 확인할 수 있다.
그리고 컨트롤러의 호출까지 마무리 됬으면 finally로 돌아와 RESPONSE의 로그를 호출하는것을 확인할 수 있다.
try {
log.info("REQUEST [{}] [{}]", uuid, requestURI);
chain.doFilter(request,response); // 다음 필터 호출 해준다, 다음 필터가 없다면 서블릿을 호출해준다.
} catch (Exception e) {
throw e;
} finally { //예외가 발생했는지 안했는지 상관없이 반드시 실행되는 부분
log.info("RESPONSE [{}] [{}]", uuid, requestURI);
}
참고
를 보면 request와 response 사이에 필드에러로그는 결국 해당 request에 대한 처리를하다가 생긴 에러로그이다.
그런데 UUID 식별자가 없으니 구분하기가 좀 힘들다. 같은 식별자를 남기는 방법은 logback mdc를 검색하자.
서블릿 필터 - 인증 체크
드디어 인증 체크 필터를 개발해보자.
로그인 되지 않은 사용자는 상품 관리 뿐만 아니라 미래에 개발될 페이지에도 접근하지 못하도록 하자.
LoginCheckFilter - 인증 체크 필터
@Slf4j
public class LoginCheckFilter implements Filter { //Filter인터페이스 내부를 보면 init()과 destroy() 메소드는 default로 되어있어서 구현을 굳이 안해도 된다.
// 로그인 체크필터가 동작하지 않을 url들을 미리 화이트리스트에 적어둔다. (로그인 없이 할수있는것들)
// css도 동작해야하므로 포함시킨다.
//아래 화이트리스트에 있는것빼고 모두 , 즉 나중에 추가될 url 들도 적용되게끔 , 적용되기 싫으면 화이트 리스트에 추가
private static final String[] whiteList = {"/", "/members/add", "/login", "/logout", "/css/*"};
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String requestURI = httpRequest.getRequestURI();
try {
log.info("인증 체크 필터 시작 {}", requestURI);
if (isLoginCheckPath(requestURI)) {
log.info("인증 체크 로직 실행 {}", requestURI);
HttpSession session = httpRequest.getSession(false);
if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
//세션이 널이거나, 세션안에 내가 찾는 키에 대한 value가 없거나
// session ==null이 앞에있어서 null인지 체크를 먼저하기에 뒤에 null인 session에 getAttribute 를 하는 오류는 신경안써도 된다.
log.info("미인증 사용자 요청 {}", requestURI); // 로그인 안한사용자이므로 로그인화면으로 이동시킨다.
//로그인화면으로 리다이렉트해준다.
httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
//로그인화면으로 보냈고, 로그인했으면 다시 원래화면으로 돌아오게끔 redirectURL를 파라미터로 추가해준다. (요즘 다 이렇게한다)
//이제 로그인 컨트롤러에서 로그인 성공시 redirectURL 파라미터를 가지고 리다이렉트해주는 부분은 개발해야한다.
return; //여기가 중요, 미인증 사용자는 다음으로 진행하지 않고 끝! (다음필터로 이동할거없이 여기서 종료,접근불가==진행불가==필터링에 걸림)
}
}
//위에서 이미 로그인체크하는 url인지 체크했고, 로그인한 사용자인지 체크하고, 로그인안한사람은 리다이렉트 시키고 ,return으로 더이상 진행되지않게 처리하였다.
//로그인에 대한 필터링이 끝났으니 다음 필터로 넘어간다. 다음필터가없으면 서블릿를 호출
chain.doFilter(request, response);
} catch (Exception e) {
throw e; // 예외 로깅 가능하지만, 톰캣까지 예외를 보내줘야하기때문에 throw를한다. 예외로깅해버리고 throw안하면 catch를 수행했으니 다음으로 진행된다.
}finally {
log.info("인증 체크 필터 종료 {} ", requestURI);
}
}
/**
* 화이트 리스트인 경우 인증 체크 X
*/
private boolean isLoginCheckPath(String requestURI) {
return !PatternMatchUtils.simpleMatch(whiteList, requestURI);
// whiteList안에 requestURL이 있는지 체크한다 없으면 false인데 !가 붙어있으므로 true를 반환
// 즉 화이트리스트에 없으면 체크대상이라는뜻이다.
}
}
Filter인터페이스 내부를 보면 init()과 destroy() 메소드는 default로 되어있어서 구현을 굳이 안해도 된다.
private static final String[] whiteList = {"/", "/members/add", "/login", "/logout", "/css/*"};
인증 필터를 적용해도 홈, 회원가입, 로그인 화면, css 같은 리소스에는 접근할 수 있어야 한다.
이렇게 화이트 리스트 경로는 인증과 무관하게 항상 허용한다.
화이트 리스트를 제외한 나머지 모든 경로에는 인증 체크 로직을 적용한다.
private boolean isLoginCheckPath(String requestURI) {
return !PatternMatchUtils.simpleMatch(whiteList, requestURI);
// whiteList안에 requestURI가 있는지 체크한다 없으면 false인데 !가 붙어있으므로 true를 반환
// 즉 화이트리스트에 없으면 체크대상이라는뜻이다.
}
화이트 리스트를 제외한 모든 경우에 인증 체크 로직을 적용한다.
httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
미인증 사용자는 로그인 화면으로 리다이렉트 한다.
그런데 로그인 이후에 다시 홈으로 이동해버리면, 원하는 경로를 다시 찾아가야 하는 불편함이 있다.
예를 들어서 상품 관리 화면을 보려고 들어갔다가 로그인 화면으로 이동하면, 로그인 이후에 다시 상품 관리 화면으로 들어가는 것이 좋다.
이런 부분이 개발자 입장에서는 좀 귀찮을 수 있어도 사용자 입장으로 보면 편리한 기능이다.
이러한 기능을 위해 현재 요청한 경로인 requestURI 를 /login 에 쿼리 파라미터로 함께 전달한다.
물론 /login 컨트롤러에서 로그인 성공시 해당 경로로 이동하는 기능은 추가로 개발해야 한다.
return; 여기가 중요하다.
필터를 더는 진행하지 않는다. ( 이미 로그인 필터에서 실패해서 걸러졌기 때문에 다음 필터로 보내지않고 종료해야한다.)
이후 필터는 물론 서블릿, 컨트롤러가 더는 호출되지 않는다.
앞서 redirect 를 사용했기 때문에 redirect 가 응답으로 적용되고 요청이 끝난다.
LoginCheckFilter를 스프링빈으로 만들고 싶다면
내가 만든 필터를 스프링빈으로 등록하고, 필요한것들은 주입받고 그렇게 사용하고 싶다면 @Component로 빈 등록하고, 주입받을거 생성자주입으로 주입받으면 된다.
필터 등록할때 WebConfig에서도 LoginCheckFilter를 생성자 주입받고 filterRegistrationBean.setFilter()할때
주입받은 LoginCheckFilter객체를 넣어주면 된다.
방금 만든 LoginCheckFilter 필터로 등록하기
아까 필터를 등록하기 위해 만든 WebConfig에 똑같이 등록해준다.
@Configuration
public class WebConfig {
// 직접 만든 필터를 등록해준다 자동 빈등록을 해주지않는 이유는 LogFilter 객체를 만들어서 스프링빈에 등록하려는게 아니고
// FilterRegistrationBean객체를 logFilter라는 스프링빈이름으로 등록하려고 하기 떄문인것같다.
// 스프링컨테이너에는 각 필터별 들이 스프링빈이름은 필터이름으로 가지고 , 스프링빈안에 빈객체는 FilterRegistrationBean를 가지며 그안에 필터객체,필터정보가 들어있는것이다.
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>(); //필터 등록용 빈을 만든다.
filterRegistrationBean.setFilter(new LogFilter()); // 내가만든 필터를 넣어주고
filterRegistrationBean.setOrder(1); // 필터의 순서도 넣어준다.
filterRegistrationBean.addUrlPatterns("/*"); // 어떤 url패턴에 적용할것인지 설정, /*는 모든 url를 의미한다.
return filterRegistrationBean;
}
//로그인체크 필터 등록
@Bean
public FilterRegistrationBean loginCheckFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LoginCheckFilter());
filterRegistrationBean.setOrder(2);
filterRegistrationBean.addUrlPatterns("/*"); //어차피 화이티리스트를 넣어놨기때문에 모든 url에 적용해도 알아서 걸러진다.
return filterRegistrationBean;
}
}
filterRegistrationBean.setFilter(new LoginCheckFilter());
로그인 필터를 등록한다.
filterRegistrationBean.setOrder(2);
순서를 2번으로 잡았다. 로그 필터 다음에 로그인 필터가 적용된다
filterRegistrationBean.addUrlPatterns("/*"); //어차피 화이티리스트를 넣어놨기때문에 모든 url에 적용해도 알아서 걸러진다.
실행해보기
아까 처럼 로그인하지않고, url를 직접 입력해서 아이템목록화면으로 가보자.
redirectURL 파라미터를 받고, 로그인화면으로 리다이렉트 되버린다.
로그찍은걸 살펴보면
"/' 홈화면에서 로그 필터에 걸려서 로그가 찍히고, 인증 필터는 시작하자마자 화이트리스트에 걸려서 종료된다.
그리고 "/items"로 url 요청하니까 인증필터가 동작하기 시작하고, 인증에 실패하여서
"/login" 으로 리다이렉트 된다.
"/login"도 화이트리스트이므로 인증 체크필터가 바로 종료된다.
( 로그 필터는 모든 url에 동작하게 해놨으니 모든 http요청마다 동작한다.)
그리고 필터는 doFilter()메소드로 인해 하나의 필터가 끝나지않고 다음 필터로 넘어간다 (재귀처럼)
그래서 위의 로그를 보면 LogFilter가 Request~라는 로그를 찍고, LoginCheckFilter로 넘어간후
LoginCheckFilter가 마지막 필터니까 그다음 서블릿,컨트롤러로 넘어가 동작한뒤 다시 LoginCheckFilter로 돌아와
doFilter()아래의 동작들을하고 마무리되고, 그다음 LoginCheckFilter로 돌아와서 doFilter()아래의 동작을 하고 마무리된것이다.
필터 1 -> 필터 2 -> 서블릿,컨트롤러 -> 필터 2 -> 필터 1
RedirectURL 파라미터를 받은 로그인컨트롤러 처리
위에서 로그인하지않은 사용자는 로그인화면으로 리다이렉트 되면서 현재 요청url를 리다이렉트URL의 파라미터값으로 가지고 넘어갔다.
httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
로그인화면에서 로그인 성공시 가지고온 redirectURL 파라미터를 이용해서 원래있었던 url로 리다이렉트 시켜주자.
loginV4
@PostMapping("/login")
public String loginV4(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult,
HttpServletRequest request,
@RequestParam(defaultValue = "/")String redirectURL) { // @RequestParam으로 파라미터를 받는다. 변수의 이름이 같으면 파라미터명을 생략할수 있다. 해당 파라미터가 없다면 기본값으로 설정된다.
if (bindingResult.hasErrors()) {
return "login/loginForm";
}
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
if (loginMember == null) { // login의 반환이 null이라는것은 로그인 아이디에 맞는 멤버가 없거나, 비밀번호가 틀렸거나이다.
//이럴때는 필드에러가 아닌 복합적인 검증실패이므로 오브젝트에러(글로벌오류)를 생성해서 bindingResult에 담는 reject() 메소드를 사용한다.
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
//에러코드는 에러코드메시지파일에 추가해주면 defaultMessage를 null로 주면 되고, 추가하지않고 바로 사용하려면 이렇게 기본메시지를 넣어주면된다.
return "login/loginForm";
}
//로그인 성공처리
//세션이 있으면 있는 세션을 반환, 없으면 신규 세션을 생성 (유저마다 하나씩 세션이 부여되므로)
//신규 세션 생성이라면 , 일단 세션을 생성하고, 세션들을 저장하는 세션저장소에 (세션ID(UUID,Jsessionid),세션객체) 를 저장한다. 그리고 세션객체 반환
//원래 세션이 있다면(http요청헤더 쿠키로 세션id가 넘어온경우?) 해당 세션객체를 반환한다.
HttpSession session = request.getSession(); // HttpServletRequest.getSession()을 이용해서 HttpSession을 받아온다.
//세션에 로그인 회원정보를 보관, 내 세션에다가 저장할값의이름, 저장할값 을 저장한다.
session.setAttribute(SessionConst.LOGIN_MEMBER,loginMember); // 받은 나의 세션에다가 키 : "loginMember" 값 : 멤버객체를 저장한다.
//그리고 톰캣이 응답메시지에 내 세션의 ID(uuid,Jsessionid)를 쿠키에 담아 보내준다.
//추가된부분 : redirectURL이라는 파라미터가 존재하면 값을 받아서 없으면 기본값인 "/"로 갈것이고 있으면 해당 url로 리다이렉트 된다.
return "redirect:" + redirectURL;
}
로그인 체크 필터에서, 미인증 사용자는 요청 경로를 포함해서 /login 에 redirectURL 요청 파라미터를 추가해서 요청했다. 이 값을 사용해서 로그인 성공시 해당 경로로 고객을 redirect 한다
로그인하면
있던곳으로 다시 리다이렉트 되는것을 확인할 수 있다
정리
서블릿 필터를 잘 사용한 덕분에 로그인 하지 않은 사용자는 나머지 경로에 들어갈 수 없게 되었다.
공통 관심사를 서블릿 필터를 사용해서 해결한 덕분에 향후 로그인 관련 정책이 변경되어도 이 부분만 변경하면 된다.
(필터부분만 손을 대면된다.)
'인프런 > 스프링 MVC 2편' 카테고리의 다른 글
15) 예외 처리와 오류 페이지 (0) | 2023.02.09 |
---|---|
14) 로그인 처리2 - 인터셉터 , ArgumentResolver 활용 (0) | 2023.02.08 |
12) 로그인처리 - 쿠키, 세션 (실전) (0) | 2023.02.08 |
11) 로그인처리 - 쿠키, 세션 직접 개발,적용해보기(개념단계) (0) | 2023.02.06 |
10) Bean Validation, @Validated(@Valid) ,프로젝트 (V3~V4) (0) | 2023.02.05 |
댓글