본문으로 바로가기

로그인에 실패했을 때 에러 메시지만 보여줘도 되지만 나는 추가적인 부가 작업을 해보려고 한다. 로그인에 실패했을 때 부가적인 작업을 할 수 있는 인터페이스를 Spring Security 에서 제공한다. 마찬가지로 로그인 실패 작업을 커스터마이징 한다고 생각하면 된다. 물론 필요가 없으면 사용하지 않아도 되지만, 대부분은 부가 작업을 하게 될 것이다. 나는 아래의 3개의 작업을 할 것이다.


1. 로그인 실패시 입력한 로그인 정보 띄우기

2. 세션을 이용하지 않고 에러 메시지 보여주기

3. 비밀번호를 3번 이상 틀릴시 계정 잠금 처리


Spring security 에서 제공하는 AuthenticationFailureHandler 인터페이스를 구현하게 되면 onAuthenticationFailure() 메서드를 오버라이드 받게 된다. 즉, 로그인에 실패하게 되면 해당 메서드가 실행이 되는 것이다. 해당 메서드에서 넘어오는 객체는 3가지가 있다. 해당 객체들을 이용해서 정보를 가져오고 추가작업들을 하면 된다.


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

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

AuthenticationException 객체: 로그인 실패 정보를 가지고 있는 객체


HttpServletReqeust 객체를 이용해서 웹에서 넘오는 로그인 정보들을 메서드(getParameter)를 이용해서 가져올 수 있다. 이렇게 가져온 값들을 그대로 다시 셋팅(setAttribute)해서 다시 웹으로 넘겨주면 된다. 또한 AuthenticationException 객체를 통해 실패 메시지를 가져와서 다시 셋팅하여 웹으로 보내면 된다. 이렇게 셋팅한 값들을 메서드(getRequestDispatcher)를 통해 보여줄 화면으로 forward 해주면, jstl을 이용해서 셋팅한 값들을 보여줄 수 있다.


먼저 4가지의 파라미터를 설정할 것이다.  우리가 필요한 정보들을 불러올 때 사용하는 파라미터라고 생각하면 된다. 다시말해, 이 파라미터들을 이용해서 각각의 값들을 불러오게 된다. 이렇게 불러온 값들을 샛팅해서 보여줄 페이지에 jstl을 이용해서 보여주면 된다. 


파라미터 설정

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
34
35
36
37
38
39
40
41
42
43
44
45
public class LoginFailureHandler implements AuthenticationFailureHandler {
    
    private String loginidname;
    private String loginpwdname;
    private String errormsgname;
    private String defaultFailureUrl;
 
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
            throws IOException, ServletException {
    }
 
    public String getLoginidname() {
        return loginidname;
    }
 
    public void setLoginidname(String loginidname) {
        this.loginidname = loginidname;
    }
 
    public String getLoginpwdname() {
        return loginpwdname;
    }
 
    public void setLoginpwdname(String loginpwdname) {
        this.loginpwdname = loginpwdname;
    }
 
    public String getErrormsgname() {
        return errormsgname;
    }
 
    public void setErrormsgname(String errormsgname) {
        this.errormsgname = errormsgname;
    }
 
    public String getDefaultFailureUrl() {
        return defaultFailureUrl;
    }
 
    public void setDefaultFailureUrl(String defaultFailureUrl) {
        this.defaultFailureUrl = defaultFailureUrl;
    }
 
}
cs

- 03행: HttpServletRequest 에서 로그인 아이디가 저장되어 있는 파라미터 이름. 아이디값이 들어오는 input 태그 name

- 04행: HttpServletRequest 에서 로그인 비밀번호가 저장되어 있는 파라미터 이름. 비밀번호값이 들어오는 input 태그 name

- 05행: 로그인 페이지에서 jstl을 이용하여 에러메시지를 가져올 때 사용할 변수 이름.

- 06행: 실패시 보여줄 화면 url 

- 13~43행: 파라미터들의 getter/setter이다. getter/setter가 존재하지 않으면 <bean> 등록시 <property> 태그에 오류가 발생한다.


security 설정

context-security.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<security:form-login
    username-parameter="loginId"
    password-parameter="loginPwd"
    login-processing-url="/login"
    login-page="/secu/loginPage"
    default-target-url="/"
    authentication-failure-handler-ref="loginFailureHandler"
/>
 
<bean id="loginFailureHandler" class="tody.common.handler.LoginFailureHandler">
    <property name="loginidname" value="loginId"/>
    <property name="loginpwdname" value="loginPwd"/>
    <property name="errormsgname" value="ERRORMSG"/>
    <property name="defaultFailureUrl" value="/secu/loginPage?error"/>
</bean>
cs

- 07행: authentication-failure-handler-ref 속성에 <bean>태그로 등록한 값을 설정한다. authenticaton-failure-url 속성을 사용해서 로그인에 실패했을 때 호출할 url을 설정했다면, authentication-failure-url 속성이 필요가 없어진다. 우리가 앞서 설정한 파라미터 중 defaultFailureUrl 이 해당 역할을 해줄 것이다.

- 10행: AuthenticationFailureHandler 인터페이스를 구현한 클래스를 <bean> 태그를 이용해 등록한다.

- 11~14행: 4가지의 파라미터 값들을 셋팅해주었다. 이것은 웹에서 넘어온 input 태그의 이름, 혹은 지정한 url 페이지이다.



1. 로그인 실패시 입력한 로그인 정보 띄우기

HttpServletRequest 객체의 getParameter 메서드를 이용하여 사용자가 입력한 로그인 아이디와 비밀번호를 가져오면 된다. 아이디와 비밀번호 input 태그의 name을 이용해거 가져오면 된다. 앞서 정의한 파라미터들을 이용하면 된다.


LoginFailureHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
            throws IOException, ServletException {
        
        String username = request.getParameter(loginidname);
        String password = request.getParameter(loginpwdname);
        
        request.setAttribute(loginidname, username);
        request.setAttribute(loginpwdname, password);
 
        request.getRequestDispatcher(defaultFailureUrl).forward(request, response);
    }
cs

- 05, 06행: 아이디와 비밀번호를 getParameter 메서드를 이용해 가져온다.

- 08, 09행: input 태그의 name에 가져온 아이디와 비밀번호 정보를 HttpServletRequest 에서 제공하는 메서드(setAttribute)로 셋팅해준다.

- 11행: HttpServletRequest 의 getRequestDispatcher 메서드를 이용해서 보여줄 화면으로 forward 해준다. forward를 해줘야만 jstl을 이용해서 값들을 가져올 수 있다. 여기서 보여줄 화면은 로그인 실패 시 이동할 페이지(defaultFailureUrl) 이라고 생각하면 된다.



2. 세션을 이용하지 않고 에러 메시지 보여주기

지금까지의 에러메시지는 웹에서 세션에 접근해서 메시지를 보여주었다. 에러 메세지를 세션에 저장하는 것은 spring security 에서 자동으로 하는 것이다. 하지만 이렇게 웹에서 세션을 통해 에러 메시지를 가져오는 건 좋지 않다. 그래서 request의 setAttribute를 이용해서 에러 메시지를 보여줄 것이다. 헷갈릴 수도 있어서 더 설명하자면, 웹에서 세션을 통해 에러메시지를 가져오는 걸 막기 위해서 세션을 통해 가져온 에러메시지를 Attribute를 이용해 저장한 다음, 웹에서는 jstl로 보여주는 작업을 하는 것이다.


LoginFailureHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
            throws IOException, ServletException {
        
        String username = request.getParameter(loginidname);
        String password = request.getParameter(loginpwdname);
        String errormsg = exception.getMessage();
        
        request.setAttribute(loginidname, username);
        request.setAttribute(loginpwdname, password);
        request.setAttribute(errormsgname, errormsg);
 
        request.getRequestDispatcher(defaultFailureUrl).forward(request, response);
    }
cs

- 07행: 세션을 통해 에러메시지를 가져온다.

- 11행: 에러 메시지를 셋팅해준다.


사실 세션을 통해 에러메시지를 가져와서 다른 값에 셋팅해주어 보여주기만 한다면 이렇게 간단하다. 이렇게 해도 별 문제는 없다. 하지만 나는 추가 작업이 더 필요하다. 내가 쉬운 방법을 찾지 못한건지 모르겠지만, 이 다음 작업인 비밀번호가 틀렸을 때 해주어야할 작업이 남아있는데, 이 작업을 위해서는 아이디가 틀린 경우, 비밀번호가 틀린 경우를 구분해줘야 한다. (사실 이 부분부터는 내가 생각한 방식이여서 정확한 방법이 아닐 수도 있다.) 


먼저 메세지 프로퍼티 사용하기 위해서 Message Source Accessor를 등록해줘야한다. 별로 많은 내용이 있지는 않지만 따로 분리해서 설명을 하려고 한다. 메세지 프로퍼티를 사용할 준비가 됐다면 에러를 분리해보자. 내가 찾은 spring security의 예외 세션들이다.


 Exception

 설명

 BadCredentialException

 비밀번호가 일치하지 않을 때 던지는 예외

 InternalAuthenticationServiceException

 존재하지 않는 아이디일 때 던지는 예외

 AuthenticationCredentialNotFoundException

 인증 요구가 거부됐을 때 던지는 예외

 LockedException

 인증 거부 - 잠긴 계정

 DisabledException

 인증 거부 - 계정 비활성화

 AccountExpiredException

 인증 거부 - 계정 유효기간 만료

 CredentialExpiredException

 인증 거부 - 비밀번호 유효기간 만료


여기서 알아둬야 할 것은, 비밀번호와 아이디가 일치하지 않는 예외들의 메세지를 분리해서는 안된다. 물론 분리해서 아이디가 틀렸습니다, 혹은 비밀번호가 틀렸습니다와 같은 메세지를 보여줘도 된다. 하지만 이러면 보안성이 낮아진다. 조금이라도 더 보안성을 높이려면 두 개의 예외를 하나의 메세지로 처리하는 것이 좋다.


예외 분리

1
2
3
4
5
6
7
8
9
//AuthenticationProvider 인터페이스를 구현한 클래스의 Authentication()
if(!user.isEnabled() || !user.isCredentialsNonExpired()) {
    throw new AuthenticationCredentialsNotFoundException(username);
}
 
//UserDetailsService 인터페이스를 구현한 클래스의 loadUserByUsername() 메서드
if(user==null) {
    throw new InternalAuthenticationServiceException(username);
}
cs


계정 인증 관련 예외들을 BadCredentialException 예외로 던지면 세션에서 알아서 세분류 되어 에러 메시지를 보여주었다. 마찬가지로 좀 더 세분화하기 위해 인증과 관련된 예외중 상위 예외인 AuthenticationCredentialNotFoundException 예외를 사용했다. 해당 예외로 던지면 핸들러에 오는 에러 세션는 좀 더 세분화 된 예외로 들어오게 된다. 그렇기에 or 조건을 사용하여 인증 관련 예외들은 하나로 묶은 뒤 해당 예외를 던져주었다.


계정이 존재하지 않을 때 UserNotFoundException 예외를 던지게 되면, BadCredentialException 예외로 받게 된다. 그러면 아이디 예외와 비밀번호 예외가 구분되지 않아서, 찾다가 찾은 예외가 InternalAuthenticationServcieException 예외이다. 해당 예외는 UserDetailsService 인터페이스에서 아무런 값이 리턴되지 않을 때 발생하는 예외이다. 리턴값이 아무것도 없다는 것은 아이디가 존재하지 않다는 뜻이라 생각해서 해당 예외를 사용하게 되었다.


LoginFailureHandler.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
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
            throws IOException, ServletException {
        
        String username = request.getParameter(loginidname);
        String password = request.getParameter(loginpwdname);
        String errormsg = null;
        
        if(exception instanceof BadCredentialsException) {
            errormsg = MessageUtils.getMessage("error.BadCredentials");
        } else if(exception instanceof InternalAuthenticationServiceException) {
            errormsg = MessageUtils.getMessage("error.BadCredentials");
        } else if(exception instanceof DisabledException) {
            errormsg = MessageUtils.getMessage("error.Disaled");
        } else if(exception instanceof CredentialsExpiredException) {
            errormsg = MessageUtils.getMessage("error.CredentialsExpired");
        }
        
        request.setAttribute(loginidname, username);
        request.setAttribute(loginpwdname, password);
        request.setAttribute(errormsgname, errormsg);
 
        request.getRequestDispatcher(defaultFailureUrl).forward(request, response);
    }
cs

- 07행: 에러 메시지를 담을 변수를 지정한다.

- 09~17행: AuthenticationException 에서 오는 예외의 타입을 체크해서 알맞은 에러 메시지를 메세지 프로퍼티를 이용해 변수에 담아준다. 비밀번호가 틀릴 경우와 아이디가 없을 경우의 예외가 서로 다르지만, 같은 에러 메시지를 뿌려준다. 

- 21행: 저장된 에러 메시지를 셋팅해준다.


security_message.properties

1
2
3
4
5
error.BadCredentials=아이디나 비밀번호가 맞지 않습니다. 다시 확인해주세요.
error.Disaled=계정이 비활성화되었습니다. 관리자에게 문의하세요.
error.CredentialsExpired=비밀번호 유효기간이 만료 되었습니다. 관리자에게 문의하세요.
error.Locked=계정이 잠겨있습니다. 관리자에게 문의하세요.
error.AccountExpired=계정이 만료되었습니다. 관리자에게 문의하세요.
cs

에러에 맞게끔 예외 메세지를 작성해준다. 내가 사용하고 싶은 key값들로 바뀌었을 뿐 메세지에는 크게 변화는 없다.



3. 비밀번호를 3번 이상 틀릴 시 계정 잠금 처리

비밀번호가 일치하지 않는 예외일 때 해당 로직을 작성해주면 된다. 우리가 예외를 분리한 이유이다. 비밀번호 틀린 횟수를 어떻게 가지고 있어야 할까? DB를 이용해서 틀린 횟수를 체크하려고 한다. DB에 실패 카운터 컬럼을 생성해서 비밀번호가 틀릴 경우 실패 카운터 컬럼을 1씩 증가시킨다면 틀린 횟수를 쉽게 알 수 있게 된다. 


1. 비밀번호 실패 카운터 1 증가

2. 비밀번호 실패 횟수 가져오기

3. 만약 3번 틀렸다면 계정 잠금 처리


비밀번호가 틀렸으면 먼저 실패 카운터를 1 증가시킨 후, 실패 횟수를 가져온다. 실패 횟수가 3번이면 계정 잠금 처리를 하고 아니면 그냥 넘어간다. 이렇게 하면 3번째에 틀렸을 경우 계정이 잠금이 되고, 4번째에 비밀번호가 맞든 틀리든 계정이 잠금되었다는 에러 메시지가 나갈 것이다.


LoginFailureHandler.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
public class LoginFailureHandler implements AuthenticationFailureHandler {
 
    @Resource(name="userSer")
    private UserService userSer;
 
    ...
 
    @Override
     public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                    AuthenticationException exception) throws IOException, ServletException {    
         ...
 
        if(exception instanceof BadCredentialsException) {
            loginFailureCount(username);
            errormsg = MessageUtils.getMessage("error.BadCredentials");
        }
        ...
    }
 
    protected void loginFailureCount(String username) {
        userSer.countFailure(username);
        int cnt = userSer.checkFailureCount(username);
        if(cnt==3) {
            userSer.disabledUsername(username);
        }
    }
    
    ...
}
cs

- 14행: 비밀번호가 틀렸을 경우 실패 카운터와 관련된 메서드를 실행시킨다.

- 20행: 실패 카운터 메서드이다. 로그인 실패 핸들러 안에 존재한다.

- 21행: 실패 카운터를 증가시킨다.

- 22행: 실패 횟수를 가져온다.

- 23~25행: 실패 횟수가 3회일때 계정을 잠금 처리한다.


Service 영역

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
public interface UserService {
 
    void countFailure(String username);
 
    int checkFailureCount(String username);
 
    void disabledUsername(String username);
 
}
 
@Service("userSer")
public class UserServiceImpl implements UserService {
    
    @Resource(name="userAuthDAO")
    private UserAuthDAO userDAO;
 
    @Override
    public void countFailure(String username) {
        userDAO.updateFailureCount(username);
    }
 
    @Override
    public int checkFailureCount(String username) {
        return userDAO.checkFailureCount(username);
    }
 
    @Override
    public void disabledUsername(String username) {
        userDAO.updateDisabled(username);
    }
 
}
cs


DAO 영역

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Repository("userAuthDAO")
public class UserAuthDAO {
    
    @Autowired
    private SqlSessionTemplate sqlSession;
 
    public CustomUserDetails getUserById(String username) {
        return sqlSession.selectOne("user.selectUserById", username);
    }
 
    public void updateFailureCount(String username) {
        sqlSession.update("user.updateFailureCount", username);
    }
    
    public int checkFailureCount(String username) {
        return sqlSession.selectOne("user.checkFailureCount", username);
    }
 
}
cs


SQL 영역

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
34
35
36
<mapper namespace="user">
 
     <select id="selectUserById" resultType="tody.common.vo.CustomUserDetails">
        <![CDATA[
            SELECT
                *
            FROM
                user
            WHERE
                ID=#{ loginId}
        ]]>
    </select>
    
    <update id="updateFailureCount">
        <![CDATA[
            UPDATE
                user
            SET
                FAILURE_CNT = FAILURE_CNT + 1
            WHERE
                ID = #{ loginId}
        ]]>
    </update>
    
        <select id="checkFailureCount" resultType="Integer">
        <![CDATA[
            SELECT
                FAILURE_CNT
            FROM
                user
            WHERE
                ID=#{ loginId}
        ]]>
    </select>
    
 </mapper>
cs



4. jstl 적용

LoginFailureHandler 클래스에 로그인 정보, 에러 메시지 등을 셋팅하고 로그인 화면으로 forward 해줬다면 이제 jstl을 이용해서 셋팅한 값들을 그대로 보여주면 된다. 주의해야 할 점은 jstl 파라미터 이름을 정확하게 입력해주어야 한다. 그거만 조심하면 문제없이 나올 것이다.


loginPage.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<form action="/login" method="post">
    ...
        <input type="text" name="loginId" placeholder="example" value="${loginId }">
    ...
        <input type="password" name="loginPwd" placeholder="Password" value="${loginPwd }">
    ...
    <c:if test="${not empty ERRORMSG}">
        <font color="red">
        <p>Your login attempt was not successful due to <br/>
        ${ERRORMSG }</p>
        </font>
    </c:if>
    ...
</form>
cs

- 03, 05행: 사용자가 입력한 로그인 정보를 셋팅하여 jstl을 이용해서 보여준다.

- 07, 10행: 이젠 세션에서 에러메시지를 가져오는 것이 아닌 jstl을 이용해서 에러 메세지를 보여준다.

그리고 에러메시지를 더이상 세션에서 가져오는 것이 아니기 때문에 <c:remove> 태그를 이용해 세션을 지우던 로직은 삭제하였다.



로그인 성공 후 부가 작업부터 하지 않고 실패 핸들러 작업부터 한 이유는, 비밀번호를 2번 틀리고 로그인에 성공했을 때 로그인 횟수가 남아있다면 다음번에 로그인을 할 때 한번이라도 틀리면 계정이 잠금된다. 그걸 방지하기 위해 로그인 성공시 로그인 실패 카운터를 초기화 해주는 작업을 해야 하는데 로그인 성공 핸들러부터 한다면 뭔가 거꾸로 가는 기분이라서 실패 핸들러부터 작성했다.


GitHub

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


댓글을 달아 주세요

  1. 눈팅 2019.01.07 09:37

    덕분에 좋은글 잘보고있습니다. 나중에 인프런같은곳에 강의 찍어서 올려주시면 유료강의여도 듣고싶네요
    19년도 행복하세요!

  2. 어글어글 2019.03.14 10:23

    유용한 글 잘 보고 있습니다. 감사합니당! 질문 하나만 하겠습니다. 디비에는 그럼 COUNT컬럼 하나만 있어도 되는건가요?

    • BlogIcon #에게 2019.03.14 12:37 신고

      네네~! 하나만 있으면 됩니당

    • 어글어글 2019.03.14 15:59

      아 감사합니다 질문 하나만 더 드릴게여 ;;
      잠금이 되면 잠금을 푸는건 어찌 해결을 해야할지 알고싶습니다. 시간 설정을 안하면 영구잠금 될텐데 잠기고 난 이후 로그인에 성공하게 되면 cnt값을 초기화 시켜야하는지...

    • BlogIcon #에게 2019.03.14 16:03 신고

      저는 계정이 잠기면 관리자에게 요청을 해야 풀어진다고 생각하고 만든거에요 메일로 다시 비밀번호를 찾는다던지 등등.. 잠긴 계정을 푸는 건 원하는 방법에 따라 달라질거라 생각합니당..!

    • 어글어글 2019.03.14 16:28

      아 넵 감사합니다!앞으로도 번성하세용!

    • BlogIcon #에게 2019.03.14 16:45 신고

      네 감사합니당~~~!! 행복한 하루 되세용

  3. 도와주세요 2019.03.19 17:05

    로그인 실패시 에러 메시지가 sysout해서 콘솔에는 찍히는데 화면단에는 나오지 않는 이유가 뭘까요..
    스프링 시큐리티 설정 파일에서

    <property name="defaultFailureUrl" value="/accounts/loginForm?error" />

    이렇게 적어두었는데, 저 요청에 관한 컨트롤러는 따로 만들지 않아도 되는 것이 맞나요 ?
    현재 비밀번호를 고의로 틀리면, 주소창이 loginForm에서 login으로 바뀌고 아무런 변화가 없습니다

    • BlogIcon #에게 2019.03.19 17:23 신고

      음 주소창이 login 으로 바뀌는 건 맞아요! 아무런 변화가 없는것도 맞구요. 에러 메세지가 보여져야하는데 보여지지 않는게 문제네요.. 그러면 jsp 부분에서 에러 메세지를 보여주는 부분(jstl사용한 부분)을 다시 확인해보시겠어요? setAttribute할때 선언한 이름과 jsp에서 선언한 이름이 다르거나..? 음 혹시 message source accessor 가 등록이 안되어 있으신건가..? 코드를 더 확인해보셔야 할 거 같아요ㅠㅠㅜ

  4. 초보 2019.03.24 20:17

    잘보고 갑니다. 질문이있는데 userService를 resource로 주입시키려고하니까

    Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'userService 인터페이스 경로' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
    에러가 발생하네요.

    • 초보 2019.03.24 20:20

      참고로 context-security.xml 파일에 loginFailHandler bean도 주입했습니다.

    • 초보 2019.03.24 20:26

      UserServiceImpl 코드 입니다.

      @Service("userService";)
      public class UserServiceImpl implements UserService{

      @Autowired
      private UserDAO userDao;

      @Override
      public List<Map<String, Object>> listUser() {

      return userDao.listUser();
      }

      @Override
      public int updateUser(Map<String, Object> map) {
      return 0;
      }
      }

    • 초보 2019.03.24 20:27

      LoginFailHandler 코드 내용입니다.
      UserService를 resource로선언하나 Autowired로 선언하나 에러 내용은 똑같습니다.

      @Autowired
      private UserService userService;

      @Override
      public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
      AuthenticationException exception) throws IOException, ServletException {
      String userId = request.getParameter("userId";);
      String errorMsg = null;
      MessageUtils.setResources();
      if (exception instanceof BadCredentialsException) {
      updateFailCnt(userId);
      errorMsg = MessageUtils.getMessage("error.BadCredentials";);
      } else if (exception instanceof InternalAuthenticationServiceException) {
      errorMsg = MessageUtils.getMessage("error.BadCredentials";);
      } else if (exception instanceof DisabledException) {
      errorMsg = MessageUtils.getMessage("error.Disaled";);
      } else if (exception instanceof CredentialsExpiredException) {
      errorMsg = MessageUtils.getMessage("error.CredentialsExpired";);
      }
      request.setAttribute("userId", userId);
      request.setAttribute("msg", errorMsg);
      request.getRequestDispatcher("/?fail=true";).forward(request, response);

      }


      private void updateFailCnt(String userId) {
      Map<String, Object> paramMap = new HashMap<String, Object>();
      paramMap.put("userId", userId);
      paramMap.put("updtCnt", true);
      userService.updateUser(paramMap);
      // CustomUserDetails userInfo = (CustomUserDetails) userDetailServiceImpl.loadUserByUsername(userId);
      // if (userInfo != null) {
      // int failCnt = userInfo.getFail_cnt();
      // if (failCnt >= 3) {
      // paramMap.remove("updtCnt";);
      // paramMap.put("enabled", 0);
      // userService.updateUser(paramMap);
      // }
      // }
      }

    • 초보 2019.03.24 20:33

      문제 해결했네요.

      해당 클래스가 context-security.xml의 component-scan의 범위가. 안들어가서였네요..좀더 넓히니까 잘됩니다.

    • BlogIcon #에게 2019.04.09 11:37 신고

      아 답변이 늦어서 정말 죄송합니다 ㅠㅠ 해결하셨다니 정말 다행이네요 ㅠㅠㅠ

  5. 2019.08.02 17:58

    비밀댓글입니다

    • BlogIcon #에게 2019.08.06 10:54 신고

      음.. 로그인에 실패하게 되면 getDispatcher로 호출한 login?error=true url을 호출하게 되고, login-page에 설정한 로그인 화면 url로 갑니다. login-procrssing-url은 form태그에 사용하는 속성인데.. 혹시 login-page랑 헷갈리신거 아닐까요?? 제 답변이 원하는 답변이 아니거나, 이해가 아직 안되신다면 다시 질문해주세요ㅠㅠ

  6. 2019.10.07 18:08

    비밀댓글입니다

    • BlogIcon #에게 2019.10.07 18:20 신고

      안녕하세요. 어디까지 따라오신지를 잘 모르겠는데.. 로그인에 실패했을 때 AuthenticationFailureHandler 커스텀 인터페이스에는 들어오는 지 먼저 확인해보셔야할 것 같아요! 아무 에러도 안났다면 security 설정부분에서 오타나 문제가 있을 수도 있구요!

  7. 2019.10.07 18:51

    비밀댓글입니다

  8. 김진희 2020.01.21 17:39

    안녕하세요? Spring Security 이해가 잘 안됐었는데 저의 궁금증 대로 쭉 풀어주셔서 덕분에 학습에 도움이 많이 되었습니다.
    다만 이번 포스트에서 궁금한 점이 있어 댓글을 답니다.

    LoginFailureHandler 클래스에서 로그인 정보 (아이디. 비밀번호), 에러 메세지 항목을 프로퍼티로 선언하여
    security-context.xml 설정 파일에서 데이터를 받도록 bean 등록을 하셨더라구요.

    그런데 또 추가적으로 onAuthenticationFailure 메소드에서 request 객체로부터 request.getParameter를 통해 데이터를 또 받아오시잖아요.

    제 생각엔 중복처리 인 것 같아서요. 차이가 있다면 알려주실 수 있나요?

    • BlogIcon #에게 2020.01.21 18:01 신고

      저도 스프링을 완벽히 마스터한 게 아니라서 정확한 답변은 아니지만 제가 이해한 것은, loginfailurehandler를 bean등록하면서 웹에서 넘어오는 파라미터값을 프로퍼티로 지정(혹은 선언)해주고, handler 클래스에서 그 등록한 프로퍼티의 name을 사용한거에요. request 객체로부터 값을 받아오는데, 그 받아오는 것의 이름을 bean등록시 프로퍼티로 지정한거죠.. 이게 아닐 수도있어요ㅠ 틀린 부분이 있다면 지적은 달게 받겠습니당ㅠㅠ

    • 김진희 2020.01.22 09:35

      ㅎㅎ 제가 다르게 생각하였어요
      받아 올 아이디 이름을 따로 저장해두는 것이였군요~

      저는 request.getParameter("loginId";) 이렇게 바로 사용하면 되는 데 따로 프로퍼티로 생성하였는지 궁금했었어요.

      감사합니다^_^

    • BlogIcon #에게 2020.01.22 12:44 신고

      아하! 좋은 하루 되세요~!

  9. 백수개발자 2020.03.21 22:11

    안녕하세요. 글에 오류가 있어 댓글을 남김니다.
    InternalAuthenticationServiceException은 아이디가 존재하지 않을때 뿐만아니라
    인증요청 대한 처리가 이루어질때 발생하는 모든 시스템 에러에 대해 발생하는 예외입니다.
    많은 사람들이 작성자님의 글을 복붙한 결과 구글에 많은 결과들이
    InternalAuthenticationServiceException은 마치 아이디가 존재하지 않을때만 나타나는 예외처럼
    정보가 변질이 되었네요.
    https://docs.spring.io/spring-security/site/docs/4.2.13.RELEASE/apidocs/org/springframework/security/authentication/InternalAuthenticationServiceException.html
    스프링 도큐멘터리에서 참고한 자료입니다.
    포스트를 수정하여 보다 많은 사람들이 올바른 정보를 읽을 수 있도록 해주세요^^

  10. 호신 2020.07.21 10:33

    disabled에 대한 쿼리는 왜 없나요요?

  11. 시큐리티처음 2021.04.09 13:56

    로그인 페이지에서 새로고침 할 경우 카운트가 증가하는것을 방지하는 방법은 해보셨나요?

    • BlogIcon #에게 2021.10.29 16:41 신고

      로그인 실패 후에 주소가 /login 으로 변하기 때문에 새로고침을 하게 되면, 기존 작업을 반복할 수 있다는 경고문이 뜹니다. 경고를 무시하고 다시 진행한다면, form의 /login 작업을 다시 하게 되기 때문에 틀린 비밀번호라면 카운트가 증가하겠죠?

      sign in을 누를때만 form /login 이 동작하게끔 작업하면 될 거 같은데, 딱히 해보지는 않았습니다.