본문으로 바로가기

로그인에 성공했을 때의 부가작업도 해보려고 한다. 마찬가지로 Spring Security에서 제공하는 AuthenticationSuccessHandler 인터페이스를 구현할 것이다. 따라서 실패 핸들러를 구현할 때와 비슷하다고 생각하며 된다. 그리고 해당 인터페이스에서 오버라이드하는 메서드에서도 3가지의 객체가 넘어오는데, 실패 핸들러와는 조금의 차이가 있을 뿐이다.


HttpServletRequest 객체: 웹에서 넘어온 Request 값을 가지고 있는 객체

HttpServletResponse 객체: 출력을 정의할 수 있는 객체

Authentication 객체: 인증에 성공한 사용자의 정보를 가지고 있는 객체


AuthenticationFailureHandler 인터페이스에는 로그인 실패 정보를 가지고 있었다면, AuthenticationSuccessHandler 인터페이스에는 로그인 성공 정보를 가지고 있다. 차이가 많이 나는 것이 아니기 때문에 큰 어려움은 없을 것이다. 다만, 성공 핸들러를 구현할때에는 기본적으로 해야하는 것이 있다. 로그인에 성공 후 이동할 URL을 지정하여 지정한 URL로 이동해야 한다. 로그인에 성공했을 때 이동할 곳은 다 다르다. 로그인 버튼을 눌러 직접 로그인화면으로 갔다면 성공 후엔 메인 화면으로 가야 할 것이고, 권한이 필요한 곳에 접근하여 강제로 로그인 화면으로 이동했다면 성공 후엔 그 권한이 필요했던 곳으로 가야한다. 그리고 혹시나 있을 에러 세션을 지우는 작업도 해줘야 한다. 기본적으로 2가지를 작업하고 다른 추가적인 부가 작업을 해주려고 한다.


1. 로그인 성공시, 어떤 URL로 Redirect 할 지 결정

2. 로그인 실패 에러 세션 지우기

3. 로그인 성공시, 실패 카운터 초기화


그리고 나는 여기서 추가적으로 마지막 접속 날짜를 갱신하는 로직을 추가했다. 생략한 이유는 실패 카운터 초기화 작업과 다를 게 없다. 그렇기 때문에 방문 카운터를 증가시키거나 방문자 수를 업데이트 하는 등 실패 카운터 초기화 작업의 과정을 보면서 자신이 원하는 부가 작업을 추가하면 된다.


파라미터 설정

LoginSuccessHandler.java

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
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
    
    private String loginidname;
    private String defaultUrl;
 
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws IOException, ServletException {
    }
 
    public String getLoginidname() {
        return loginidname;
    }
 
    public void setLoginidname(String loginidname) {
        this.loginidname = loginidname;
    }
 
    public String getDefaultUrl() {
        return defaultUrl;
    }
 
    public void setDefaultUrl(String defaultUrl) {
        this.defaultUrl = defaultUrl;
    }
 
}
cs


Security 설정

context-security.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
<security:form-login
    username-parameter="loginId"
    password-parameter="loginPwd"
    login-processing-url="/login"
    login-page="/secu/loginPage"
    authentication-failure-handler-ref="loginFailureHandler"
    authentication-success-handler-ref="loginSuccessHandler"
/>
 
<bean id="loginSuccessHandler" class="tody.common.handler.LoginSuccessHandler">
    <property name="loginidname" value="loginId"/>
    <property name="defaultUrl" value="/"/>
</bean>
cs

- 07행: authentication-success-handler-ref 속성을 추가해준다. 해당 핸들러를 통해 default url이 설정되기 때문에 default-target-url 속성은 필요가 없어진다.

- 10행: AuthenticationSuccessHandler 인터페이스를 구현할 클래스를 <bean> 등록해준다.

- 11, 12행: 사용할 파라미터 값들을 설정해준다.



1. 로그인 성공시, 어떤 URL로 Redirect 할 지 결정

로그인에 성공하고 난 후, 가야 할 이동 경로는 어떻게 로그인 화면에 접근했느냐에 따라 다르다. 또한 어떻게 설정해주냐에 따라 달라질 수도 있다. 로그인에 성공하면 무조건 메인 화면으로 간다던지, 로그인 성공 화면을 띄워준다 던지 등등 어떤 경우에 어떤 url로 보낼 지 결정하고 만드는 게 좋다. 나는 2가지의 경우로 나누어 redirect 시킬 것이다.


- 인증 권한이 필요한 페이지에 접근했을 때

- 직접 로그인 페이지로 접근했을 때


먼저 인증 권한이 필요한 페이지에 접근하게 되면, 로그인 화면을 띄우기 전에 필요한 정보들을 세션에 저장하게 된다. spring security에서 제공하는 사용자의 요청을 저장하고 꺼낼 수 있는 RequestCache 인터페이스를 이용해, 사용자 요청 정보들이 들어 있는 SavedRequest 클래스 객체를 세션에 저장하게 된다. 그럼 우리는 RequestCache 객체를 생성해 SavedRequest 객체를 가져와서 로그인 화면을 보기 전에 방문했던 URL 정보를 가져오면 된다.


다음으로는 직접 로그인 페이지로 접근했을 때 즉, 인증 권한을 위해 로그인 화면을 자동으로 띄워주는 것이 아니라 직접 로그인 url을 통해 이동했을 경우에는 세션에 저장되지 않기 때문에 SavedRequest 객체로는 URL 정보를 가져올 수 없다. 이런 경우에는 HttpServletRequest 객체의 getHeader 메소드를 이용해서 REFERER 헤더 값을 읽어 올 수 있다. REFERER 헤더 값은 이전 페이지 URL을 가져오는 데 가장 보편적인 방법이다. 하지만 로그인에 관한 작업을 할 때에는 REFERER 헤더 값을 이용하는 걸 추천하지 않는다.


로그인 화면에서 <form> 태그로 action을 지정했었다. 로그인 버튼을 누르게 되면 해당 action 을 통해 로그인 처리 과정이 이루어질 것이다. 여기서 문제가 발생한다. 이렇게 로그인에 성공하게 되면 REFERER 헤더 값은 어떤 url이 저장되어 있을까? 바로 로그인 화면 url이 저장되어 있다. 따라서 직접 로그인 했을 때 redirect 로 REFERER 헤더 값을 지정한다면, 로그인에 성공하게 되도 다시 로그인 화면을 보여주게 된다. 그렇기 때문에 REFERER 헤더 값을 사용하는 것은 로그인 처리에서는 별로 좋지 못하다. 따라서 우리는 지정된 화면으로 이동하게 만들어 줄 것이다. security 설정에서 defalut-target-url 을 설정한 적이 있다. 로그인 성공시 이동할 url을 설정했는데, 이 url로 이동하게 해주면 된다.


LoginSuccessHandler.java

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
32
33
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
        
    private RequestCache requestCache = new HttpSessionRequestCache();
    private RedirectStrategy redirectStratgy = new DefaultRedirectStrategy();
    
    ...
 
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws IOException, ServletException {
        
        ...
        
        resultRedirectStrategy(request, response, authentication);
        
    }
 
    ...
    
    protected void resultRedirectStrategy(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws IOException, ServletException {
        
        SavedRequest savedRequest = requestCache.getRequest(request, response);
        
        if(savedRequest!=null) {
            String targetUrl = savedRequest.getRedirectUrl();
            redirectStratgy.sendRedirect(request, response, targetUrl);
        } else {
            redirectStratgy.sendRedirect(request, response, defaultUrl);
        }
        
    }
}
cs

- 03행: RequestCache 객체를 생성한다.

- 04행: RedirectStratgy 객체를 생성한다.

- 14행: redirect url 작업을 위한 메서드를 호출한다.

- 23행: RequestCache 객체를 통해 SavedRequest 객체를 가져온다. 

- 25행: 세션에 이동할 url의 정보가 담겨져 있을때 즉, 인증 권한이 필요한 페이지에 접근했을 경우를 뜻한다.

- 26행: savedRequest 객체를 통해 getRedirectUrl (로그인화면을 보기 전에 갔던 url)을 가져온다.

- 28행: 직접 로그인 화면으로 이동했을 경우를 뜻한다.

- 27, 29행: 화면 이동을 위해 redirectStratgy 의 sendRedirect 를 재정의해준다. 각각에 알맞은 url 정보를 넣어주면 된다.


spring security에서 자동적으로 해주던 부분을 커스터마이징 해주면서 우리가 직접 경로를 설정하게 된 것이다. 만약에 권한에 따라 서로 다른 페이지로 이동하게 하고 싶으면 인증에 성공한 사용자의 권한을 이용해서 원하는 url로 보내주면 된다. 어떨 때 어디로 보낼 지 먼저 정해놓고 시작한다면 어렵지 않게 원하는대로 커스터마이징할 수 있을 것이다.



2. 로그인 실패 에러 세션 지우기

로그인을 하는 과정에서 한번만에 로그인에 성공할 수도 있지만, 실패를 한 후 로그인에 성공하는 경우도 있다. 이처럼 로그인에 실패하는 상황이 한번이라도 발생한다면, 에러가 세션에 저장되어 남아있게 된다. 로그인에 성공했다고 하지만 이렇게 세션에 에러가 남겨진 채로 넘어갈 수는 없다. 따라서 로그인 성공 핸들러에서 에러 세션을 지우는 작업을 해줘야 한다.


LoginSuccessHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
    Authentication authentication) throws IOException, ServletException {
 
        clearAuthenticationAttributes(request);
 
    }
 
    protected void clearAuthenticationAttributes(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if(session==nullreturn;
        session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
    }
 
}
cs

- 05행: 에러 세션을 지우는 메서드를 실헹한다.

- 10행: 세션을 받아온다.

- 11행: 세션이 null 즉, 세션에 에러가 없다면 그냥 return 된다.

- 12행: WebAttributes.AUTHENTICATION_EXCEPTION 이름 값으로 정의된 세션을 지운다.



3. 로그인 성공시, 실패 카운터 초기화

로그인 실패 핸들러에서 로그인에 실패 시, 실패 카운터를 증가시켰다. 그런데 실패 카운터가 증가된 상태로 로그인에 성공하게 되면, 다음번에 로그인할 때는 더 적은 실패 기회를 얻게 될 것이다. 따라서 로그인을 시도할 때마다 같은 시도 횟수를 주려고 한다면, 로그인에 성공할 때 실패 카운터를 초기화해야 한다. 


LoginSuccessHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Resource(name="userSer")
private UserService userSer;
 
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
    Authentication authentication) throws IOException, ServletException {
        
    String username = request.getParameter(loginidname);
        
    userSer.resetFailureCnt(username);
 
    ...
        
}
cs

Service 영역

1
2
3
4
5
6
7
8
//UserService
void resetFailureCnt(String username);
 
//UserServiceImpl
@Override
public void resetFailureCnt(String username) {
    userDAO.updateFailureCountReset(username);
}
cs

UserDAO.java

1
2
3
public void updateFailureCountReset(String username) {
    sqlSession.update("user.updateFailureCountReset", username);
}
cs

user_sql.xml

1
2
3
4
5
6
7
8
9
10
<update id="updateFailureCountReset">
    <![CDATA[
        UPDATE
            user
        SET
            FAILURE_CNT = 0
        WHERE
            ID = #{ loginId}
    ]]>
</update>
cs


이런 로직 작업은 정말 껌일 것이다. 그래서 따로 설명하지 않고 로직이 어디에 들어가야 하는 지만 잘 알아두면 언제든지 하고 싶은대로 커스텀 할 수 있다. 프로젝트를 실행시켜서 테스트를 해보자. 실패 카운터도 초기화 되고, url도 생각한 것처럼 이동되면 완성된 것이다!


GitHub

참고 : 사랑이 고픈 프로그래머..


댓글을 달아 주세요

  1. 초보자 2019.02.20 19:13

    혹시 로그인 성공 작업을 할 때
    시큐리티가 제공하는 세션에 추가 정보를 입력 할 수도 있을 까요?

    [Spring Security - 인증 절차 인터페이스 구현 (1) UserDetailsService, UserDetails]
    게시물 보면서 DB에 있는 여러가지 칼럼들의 겟터/셋터를 만들면

    세션에 자동으로 해당 정보들도 저장 되는 줄 알았는데 그게 아니더라고요 ㅠㅠ

    이 포스팅 글 보면서 공부하다가 생각해보니, 로그인에 성공했을 때 시큐리티가 생성하는 세션에 로그인 성공한 사용자 정보 몇가지 더 담아서

    화면단에서 활용해보고 싶은데.. 가능할까요 ? 혹시나 해서 구글 검색해봤는데 원하는 정보가 잘 나오지 않아 이렇게 또 질문을 해봅니다 ㅠ

    • BlogIcon #에게 2019.02.20 20:22 신고

      로그인한 사용자 정보를 보여주는 포스팅이 있답니다! https://to-dy.tistory.com/m/82 한번 확인해보세요! 이메일 같은 정보도 가져올 수 있어요

    • 초보자 2019.02.20 20:53

      해당 포스팅을 보고 로그인에 성공하면 메인 화면에 사용자 이름 표기하는 것 까지 성공 하였습니다.

      String name = "";
      if(principal != null) {
      name = auth.getName();
      }

      이 부분에서 auth.get 하고 컨트롤 스페이스를 하면 네임 외에 사용 할 수 있는 메서드 중에

      제가 [인증 절차 인터페이스 구현(1)] 에서 추가한 getter 들은 안나오더라고요.. ㅠㅠ

    • BlogIcon #에게 2019.02.20 21:22 신고

      그거말고 조금 더 내려보시면 +) 부분에 새로운 방법이 있어요! 그거 쓰는게 더 편하실거에욜

    • 초보자 2019.02.20 21:41

      엇 !!!
      저런 꿀팁을 미쳐 못보고 지나쳤네요 ㅠㅠ

      학원에서 작업을 해둬서 ... 내일 가자마자 바로 확인해봐야 겠네요

      정말 감사합니다 !!!


      아참, 혹시 <sec:authentication property="principal.[여기]"

      저기서 '여기'에 들어가는 것은 getter의 이름 인가요 ?

      추가로 저렇게 값을 가져오고 해당 값을 컨트롤러로 바로 쏠 수 있을까요 ?

    • BlogIcon #에게 2019.02.20 22:01 신고

      거기에 userDetails 에서 선언한 변수명을 쓰면 됩니당 그리고 <sec:authentication property=“principal.username” var=“id”> 이렇게 해주시면 ${id} 형태로 input value든 url이든 값을 컨트롤러에 보내 줄 수 있을 거에요!

    • 초보자 2019.02.20 22:09

      답변 정말 감사합니다 ㅠㅠ

    • BlogIcon #에게 2019.02.20 22:22 신고

      열공 화이팅!

  2. 초보자 2019.02.21 10:36

    연습하고 있는데 로그인 성공 후 작업 이후에 몇 가지 문제점이 생겨서 고쳐보다가 질문 남깁니다

    01.
    DB에 존재하지 않는 아이디를 입력해도 로그인 실패 핸들러에서
    if(exception instanceof BadCredentialsException) 쪽으로 요청이 들어갑니다.
    제 기억 상에는 이전 포스팅 글 보면서 테스트 해 볼때는 이러지 않았던 것 같은데 어디서 꼬인지 모르겠네요 ;

    일단은 DB에서 userId로 정보 가져오는 서비스 하나 만들어서 조건문에 추가 하긴 했는데..

    if(userService.selectByUserId(userId) != null &&exception instanceof BadCredentialsException)

    이렇게 해서 오류가 나오는 상황은 방지 했는데

    근본적인 원인을 알고싶습니다 ㅠㅠ

    02.
    <sec:authorize access="isAuthenticated()"> 과
    <sec:authorize access="isAnonymous()"> 을 이용해서

    비로그인 (시큐리티 인증X) 상태이면, 로그인 화면 상단에 문구가 나오고
    로그인 하면 문구가 나오지 않게 설정했는데
    갑자기 로그인 페이지에서 로그인 실패 (존재하는 ID로 비밀번호 틀리기) 해도 문구가 사라져버리네요..

    혹시 해당 포스팅 내용 추가하면 컨트롤러 단에서 추가적으로 작업해주어야 하는 것이 있나요 ?


    • 초보자 2019.02.21 10:58

      1번은 일단 별도의 service를 만들어서 강제(?)로 해결 한 상태이고

      2번은 실패핸들러의 디폴트url을 컨트롤러에서 받으면 리다이렉트 해서 화면 요청 해보니까 1차적으로 해결은 되었습니다 ..

      ㅠㅠ

      문제는 왠지 다른 곳에서도 문제가 발생 할 것 같아서 걱정이네요

    • BlogIcon #에게 2019.02.21 11:15 신고

      01)
      존재하지 않는 아이디일 경우 BadCredentialsException 예외로 나오는 게 맞습니당! 그래서 존재하지 않는 아이디를 따로 표시하고 싶으면 에러를 던질때 InternalAuthenticationServiceException 에러로 던진다면 구분이 될거에요! 이거랑 관련된 얘기는 로그인 실패 부가작업에 가면 "예외분리" 에 설명되어 있어요!

      02)
      이건 소스를 직접 봐야 할 거 같아요. 말씀하신 설정대로라면 문구가 나오는 게 정상적인건데... 다시 한번 찬찬히 확인해보셔야 할 거 같아요

  3. Heedo Kim 2019.06.25 15:41

    꼼꼼한 포스팅 잘 봤습니다. 로그인 핸들러 구현에 큰 도움이 되었습니다. 감사합니다.

  4. 띠로리 2020.01.09 15:11

    꼼꼼하게 잘 정리된 글 덕분에 시큐리티를 학습하고 적용하는데 큰도움이되었습니다.

    그런데 마지막으로 좀 궁금한부분이있어서 이렇게 질문드립니다.

    제가 따로 회원가입및 비밀번호 암호화하는 부분을 추가하고있는데요, 회원가입 jsp에서 폼태그안에 회원가입 폼구성을하고 나서 폼액션을 예를들어 /cmm/signUP.do 로 경로설정하고 보냈는데, 접근제한 페이지가 호출되고있어 애를먹고있습니다.. 제가알기론 security-context에서 security:intercept-url pattern="/**"access="permitAll" 설정을하면 그 위에 url-pattern형식에 해당되지않는경로들은 모두 접근이 되는거 아닌가요...? ㅠㅠ

    • BlogIcon #에게 2020.01.10 13:17 신고

      말씀하신 부분은 맞아요! 그리고 폼액션의 경로가 아니라 페이지의 경로의 접근을 막는거에요. 컨트롤러에서 뷰로 갈때 페이지경로를 확인해보셔야할 거 같구요.. 태그 설정 경로가 꼬여있을 수도 있어요 /** 경로가 태그들의 맨 아래에 설정되어있는 지도 확인해야 할 거 같아요

  5. 소쿠리 2020.09.03 18:27

    정말정말 감사합니다
    이걸로 스프링 시큐리티 프로젝트 겨우 구현했네용!
    설명도 너무 잘되어있어서 따라하기도 좋고
    문서에서 발췌한 내용들이 많이있어서 도움이 정말 되었습니다
    최고입니다.. 흥하세요

  6. BlogIcon 금융경제학 2020.10.07 10:18 신고

    지금까지 로그인 성공시 무조건 홈으로 보내고 있었는데...ㅋㅋㅋ;;;
    덕분에 요청했던 주소로 보내게 금방 처리가 됐네요~
    정말 감사합니다 ^^