본문으로 바로가기

AuthenticationProvider 인터페이스는 화면에서 입력한 로그인 정보와 DB에서 가져온 사용자의 정보를 비교해주는 인터페이스이다. 해당 인터페이스에 오버라이드되는 authenticate() 메서드는 화면에서 사용자가 입력한 로그인 정보를 담고 있는 Authentication 객체를 가지고 있다. 그리고 DB에서 사용자의 정보를 가져오는 건 UserDetailsService 인터페이스에서 loadUserByUsername() 메서드로 구현했다. 따라서 authenticate() 메서드에서 loadUserByUsernmae() 메서드를 이용해 DB에서 사용자 정보를 가져와서 Authentication 객체에서 화면에서 가져온 로그인 정보와 비교하면 된다. AuthenticationProvider 인터페이스는 인증에 성공하면 인증된 Authentication 객체를 생성하여 리턴하기 때문에 비밀번호, 계정 활성화, 잠금 모든 부분에서 확인이 되었다면 리턴해주도록 하자.



1. AuthenticationProvider

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
public class CustomAuthenticationProvider implements AuthenticationProvider {
    
    @Autowired
    private UserDetailsService userDeSer;
 
    @SuppressWarnings("unchecked")
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        
        String username = (String) authentication.getPrincipal();
        String password = (String) authentication.getCredentials();
        
        CustomUserDetails user = (CustomUserDetails) userDeSer.loadUserByUsername(username);
        
        if(!matchPassword(password, user.getPassword())) {
            throw new BadCredentialsException(username);
        }
 
        if(!user.isEnabled()) {
            throw new BadCredentialsException(username);
        }
        
        return new UsernamePasswordAuthenticationToken(username, password, user.getAuthorities());
    }
 
    @Override
    public boolean supports(Class<?> authentication) {
        return true;
    }
    
    private boolean matchPassword(String loginPwd, String password) {
        return loginPwd.equals(password);
    }
 
}
cs

10행: 화면에서 입력한 아이디를 username에 담는다.

11행: 화면에서 입력한 비밀번호를 password에 담는다.

13행: 화면에서 입력한 아이디(username)로 DB에 있는 사용자의 정보를 UserDetails 형으로 가져와 user에 담는다.

15행: 화면에서 입력한 비밀번호와 DB에서 가져온 비밀번호를 비교하는 로직이다. 비밀번호가 맞지 않다면 예외를 던진다.

19행: 계정 활성화 여부를 확인하는 로직이다. AuthenticationProvider 인터페이스를 구현하게 되면 계정 잠금 여부나 활성화 여부등은 여기에서 확인해야 한다.

23행: 계정이 인증됐다면 UsernamePasswordAuthenticationToken 객체에 화면에서 입력한 정보와 DB에서 가져온 권한을 담아서 리턴한다.

27행: AuthenticationProvider 인터페이스가 지정된 Authentication 객체를 지원하는 경우에 true를 리턴한다.

31행: 비밀번호를 비교하는 메서드이다. 맞으면 true를 리턴한다.


계정 관련 체크 여부(19행)에 관해서 언급할 게 있다. 이 부분은 온전히 내 생각일 뿐이기 떄문에 정확한 건 아니다. 좀 더 찾아봐야 할 부분이다. 그리고 난 왜 UserDetailsService 인터페이스를 구현할 때 이걸 궁금해하지 않았는 지 의문이다(ㅋㅋ) UserDetailsService 인터페이스를 구현할 때 비활성화된 계정을 체크하는 로직이 없었다. 근데 비활성화된 계정은 에러로 예외가 던져졌다(!!!) 왜 이걸 궁금해하지 않고 그냥 넘어갔을 까 ㅋㅋㅋ 정확히는 모르지만 DB에서 가져온 사용자의 정보를 UserDetails 인터페이스에 담으면서 계정관련 체크 여부를 알아서 확인하고 예외를 던지는 것 같다.


근데 AuthenticationProvider 인터페이스를 구현하게 됐을 때 문제가 발생했었다. 비활성화된 계정이 예외를 던지지않고 그냥 로그인에 성공해버리는 문제가 생겼었다ㅠㅠ.. AuthenticationProvider 인터페이스를 구현하니까 UserDetailsService 인터페이스에서 계정 관련 체크를 하지 않았다ㅠㅠ AuthenticationProvider 인터페이스를 구현하게 되면서 UserDetailsService 인터페이스는 DB에서 사용자의 정보를 가져오는 역할만 하게 되버린 것 같다. 그래서 UserDetailsService 인터페이스에서 계정 활성화여부를 체크해보려고 했는데 적절한 예외가 없어서 에러 메시지가 제대로 나오지 않았다. 근데 AuthenticationProvider 인터페이스에는 적절한 에러 메시지를 던지는 예외가 있는 걸 확인했다! 유레카! 그래서 내 생각에 AuthenticationProvider 인터페이스와 UserDetailsService 인터페이스를 구현하게 되면 UserDetailsServcie 인터페이스는 DB에서 사용자의 정보를 가져오는 역할을 하고, AuthenticationProvider 인터페이스에서 비밀번호 체크 및 계정과 관련된 각종 체크여부를 확인해야 되는 것 같다. 대부분 계정 체크 부분을 true로 하고 만들어서 그런지 자료가 없었다ㅠㅠ 그냥 내 생각일 뿐이니.. 얼른 정확한 자료를 찾아봐야 겠다ㅠㅠ



2. Security 설정

context-security

1
2
3
4
5
6
7
8
<security:authentication-manager>
    <security:authentication-provider ref="userAuthProvider"/>
    <security:authentication-provider user-service-ref="userService">
    </security:authentication-provider>
</security:authentication-manager>
        
<bean id="userService" class="tody.common.service.CustomUserDetailsService"/>
<bean id="userAuthProvider" class="tody.common.service.CustomAuthenticationProvider"/>
cs

02행과 08행이 추가되었다. AuthenticationProvider 인터페이스를 구현한 CustomAuthenticationProvider 클래스를 등록하는 부분이다.


<authentication-provider> 태그는 여러 요소를 사용할 수 있는데, 여러 요소를 사용할 경우 순서대로 진행된다. 사실 어떤걸 먼저 선언하든 로그인 결과에는 영향을 주지는 않는다. 하지만 UserDetailsService 인터페이스가 먼저 선언되서 사용될 경우 에러 메시지가 제대로 나오지 않는다. 정확히는 에러 메시지 프로퍼티를 사용할 수 없다. 이 부분에 대해선 정확히 잘 몰라서 설명하지 못하지만(ㅠㅠ) UserDetailsService 인터페이스가 먼저 실행되면 AuthenticationProvider 인터페이스를 구현할 이유가 없지 않을까? 싶다..ㅋㅋㅋㅋ



3. 실행

비밀번호가 틀릴 경우, 비밀번호가 맞지 않다는 에러 문구가 뜨고 로그인에 실패할 것이다. 물론 비밀번호를 맞게 한다면 당연히 로그인에 성공해야 한다. 이렇게 비밀번호 비교 확인이 완벽하게 이루어진다면 AuthenticationProvider 인터페이스도 완벽하게 구현된 것이다! 휴! 뭔가 어려워보였지만 인터페이스에 맞는 기능들을 생각하고 구현한다면 그리 어렵지만은 않다.


GitHub 

그리고 지금까지 개인적인 생각으로는 AuthenticationProvider 인터페이스까진 구현하지 않아도 충분히 사용할 수 있을 거 같다 ㅋㅋ


댓글을 달아 주세요

  1. 눈팅 2018.12.03 17:55

    스프링시큐리티 파악이 많이 힘들었는데 매우 도움이 되네요
    혹시 어느걸로 공부하시는지 여쭤봐도될까요??

    • BlogIcon #에게 2018.12.03 18:13 신고

      안녕하세요! 이거 비공개로 올린 줄 알았는데ㅠㅠㅋㅋㅋ 우선 저는 시큐리티는 인터넷으로 다 찾아보고 공부한 거에요! 이론 부분은 https://zgundam.tistory.com/ 이 분 글 보면서 많이 이해했어요!! 4 버전은 잘 없어서 https://docs.spring.io/spring-security/site/docs/4.2.0.RELEASE/reference/htmlsingle/#jc-httpsecurity 많이 참고했어요!

  2. 눈팅족 2019.03.21 10:45

    회원가입시 시큐리티의 bcryptPasswordEncoder을 이용해서 비밀번호를 저장 한다고 하면
    그것을 비교하는 작업을 해당 포스트의 인터페이스에서 해주면 되는 건가요 ???

    • 눈팅족 2019.03.21 11:53

      context-security 설정을 해주었는데도 CustomAuthenticationProvider로 요청이 들어오지 않습니다 ㅠ
      sysout 하나 찍어뒀는데 뜨지도 찍히지 않고 오류 또한 없네요 ...;
      혹시 어디를 체크해보면 도움이 될 까요

      이 전 포스트인 UserDetails은 sysout 찍어보면 요청 잘 받고 있는 것 같습니다.

      혹시나 해서 시큐리티 설정 파일에서 UserDetails을 제거하고 userAuthProvider만 등록해 보아도 요청을 타지 않네요 ..

    • BlogIcon #에게 2019.04.09 11:35 신고

      네! 해당 인터페이스에 해주시며 됩니다.

      security 설정 부분에서 문제가 있는 거 같아요. provider 를 타고, details service 를 타는 거라서.. 등록에 문제가 있는 거 같아요.

      답변이 늦어서 죄송합니다 ㅠㅠ

  3. 감사 2019.05.06 13:51

    감사합니다 3일내내 막혔다가 구현성공했네요

  4. 2019.05.31 10:23

    비밀댓글입니다

    • BlogIcon #에게 2019.05.31 10:35 신고

      오와.. 좋은 정보 감사합니다...!!!! 좋은 하루 되셔요!!

    • BlogIcon 나시아 2019.05.31 10:42 신고

      DaoAuthenticationProvider를 찾아주는부분은 저의 경우에는 WebSecurityConfigurerAdapter 상속받아 configuration 해주었는데,
      이 WebSecurityConfigurerAdapter에서 default 처리를 해주는것 같습니다.

  5. 싯큐릿티 2019.09.28 20:23

    안녕하세요~
    현재 인증 절차 인터페이스 구현 1단계까지 하고, 2단계에 들어가려는데 궁금한점이 있어서 글 남깁니다.
    1단계 구현 후 테스트 해본 결과 아이디와 비밀번호가 일치하지 않거나, 계정이 비활성이면
    messages.properties를 통해 멘트를 날려주는데,,,
    왜 꼭 AuthenticationProvider 인터페이스까지 구현해 줘야되는지 이해가 잘 안가서 글 남깁니다..ㅠㅠ

    • BlogIcon #에게 2019.09.28 20:52 신고

      구현하지 않아도 됩니다! 스프링시큐리티의 구조를 모두 커스텀 인터페이스로 구현할 수 있다는 것을 보여주기 위해서 구현한거에요! 구현하지 않아도 아무 문제없는 인터페이스에요^^

  6. 방가 2019.12.23 04:42

    자꾸 [java.lang.string] username을 찾을수 없다고 나와서... 무엇의 문제인가 다찾아봤는데..
    <security:authentication-provider ref="userAuthProvider"/>
    <security:authentication-provider user-service-ref="userService">
    요놈들 순서를 바꾸니.. 정상작동 와우..

  7. 2020.03.02 09:35

    비밀댓글입니다

    • BlogIcon #에게 2020.03.02 10:49 신고

      안녕하세요. 저도 provider 를 설계하면서 이부분에 대해서 많이 찾아보기도 하고 고민도 했었습니다ㅠㅠ

      우선 security 설정에서 provider와 service를 선언했을 경우, 선언한 순서대로 실행되면서 예상하신 것과 같이 동작되는 게 맞습니다.

      정확한 답변을 찾진 못했지만 설계를 하면서 여러가지 테스트 해봤을 때, 두 가지 모두 구현했을 경우 service는 DB에서 사용자의 정보만을 가져오는 역할을 하게 되고, provider는 로그인 체크의 역할을 하는 것으로 확인되었습니다.

      그래서 저는, 사용자의 정보를 가져오는 것과 로그인을 검증하는 것을 분리해서 관리하고 싶다면 두가지 모두를 구현하면 되고, 굳이 그러고 싶지 않다면 둘 중 하나만 구현해도 충분하다는 생각이 듭니다.ㅎㅎ 제 생각에 조금 더 확신을 주는 댓글을 남겨주신 것 같아서 감사드려요ㅎㅎ.. 좋은하루 되세요

    • BlogIcon Pawer0223 2020.03.02 12:48 신고

      아..! 친절한 답변 감사드립니다 ㅎ :)

  8. 호롤롤로 2020.03.03 19:48

    AuthenticationProvider 16번째 줄에 exception던지면서 아이디를 넣었는데
    로그인 실패하면 에러메세지가 기존에 설정해놓은 properties의 문구로 나오지않고
    아이디로 나오는데 properties 문구로 나오게 하려면 어떻게 해야할까요..??

    • 홀롤로로 2020.03.03 19:58

      AuthenticationProvider없이 로그인실패했을 경우에는 에러메세지는 properties 설정갑으로 가져오는데.. AuthenticationProvider로 하게되면 exception던질때 설정한 아이디로 나오네요ㅠㅠ

    • BlogIcon #에게 2020.03.03 20:37 신고

      안녕하세요 failureHandler를 이용해서 프로퍼티 메세지 처리를 했어요!! 다음 글을 참고하시면 될 거 같아요

  9. 홀롤로로 2020.03.04 19:55

    CustomAuthenTicationProvider에 Override된 anthenticate return 하면 어디로가나여..??

    • 호롤롤로 2020.03.04 20:05

      로그인 실패시 로그를 찍어보고 과정을 보니까
      메인 -> 로그인 화면 -> 로그인시도 -> loadUserByUsername -> query로 로그인한 user정보를 객체로 리턴 (여기 리턴도 어디로 가는지 모르겠어요)

      그이후에 누가호출했는지를 몰라도..
      authenticate -> loadUserByUsername 에서 query로 로그인한 user정보를 객체로 리턴 (다시 반복)
      ->로그인 페이지

      이렇게 흘러가는데...
      이해가 안되네요ㅠㅠ

      설명해주실수있나요?

    • BlogIcon #에게 2020.03.04 21:37 신고

      provider 와 service 2가지 모두를 구현했을 경우, security설정에서 선언한 순서대로 구현한 인터페이스를 타게 됩니다.

      그렇기 때문에, 먼저 선언한 provider 인터페이스에서 service의 loadUserByUsername을 호출해서 db정보들을 가져와서 로직을 검사하고 객체(new usernamepassword어쩌구,,)를 리턴합니다. 그 다음 선언된 service인터페이스가 실행되어 loadUserByUsername 을 또 타게 되어 또 db 정보를 가지고 오게 되는 거죠..ㅎ

      선언한 순서대로 인터페이스 하나 호출 후, 다음 인터페이스가 호출되는 방식입니다.. 그래서 저는 둘 중 하나만 구현해도 무방하다고 보는 입장입니다ㅎㅎ..

  10. 눈팅44 2020.06.23 20:54

    안녕하세요. 로그인 페이지로 이동하는 것에는 성공했는데, 로그인을 요청하면, login-processing-url을 모르는 것인지 Provider를 못부르는 건지 계속 access-deined-hanlder에 걸리는데 무엇이 잘못된걸까요?ㅠㅠㅠ

    • BlogIcon #에게 2020.09.27 11:56 신고

      access-deined-hanlder 에 가는 건 로그인 한 회원이 로그인 성공 후 가는 페이지에 권한이 없는 경우 같아요. 접근 권한 설정을 확인해보셔야 할 듯 싶어요!

  11. 스프링초보 2020.12.03 10:45

    안녕하세요! 시큐리티 첫 게시글부터 잘 보고 따라왔던 학생입니다!ㅎㅎ 먼저 이렇게 유익한 게시글 작성해 주셔서 너무 감사해요! 근데 위 게시글의 계정 체크 관련 여부에 대해서 의문이 생겨서 의견을 나누고자 댓글 남깁니다! 저도 같은 생각으로 CustomUserDetailsService에서 enabled를 체크하는건 어떨까 해서 한 번 작성해 보았는데요! if(!user.isEnabled()) {
    throw new DisabledException(username);
    }
    이런식으로 예외를 던지면 저기에 걸리긴 하더라구요! 처음에 위 예외는 사용할 수 없었는데
    import org.springframework.security.authentication.DisabledException; 를 하니 사용할 수 있게 되었습니당!

    • BlogIcon 스프링초보 2020.12.03 11:28

      제가 여러 경우를 해봤는데 확실히 isEnabled는 provider에서 하는게 맞는거 같아요 그런데 provider랑 detailsService에 둘 다 isEnabled를 검사하는 로직이 있고 throw를 던지면, provider에서의 exception이 나오는게 아니고 provider가 호출했던 detailsService로 가서 거기에서의 isEnabled이 false일 때의 exception을 던지는 거 같아요!

  12. BlogIcon 페페로니피자 2021.03.25 23:49 신고

    JSP 페이지에서 form 태그로 데이터를 보내시는것 같던데, 컨트롤러에서는 어떻게 처리하시나요?

    따라해서 메소드를 다 만들었는데, 컨트롤러에서는 어떻게 써야 하는지 모르겠습니다ㅠㅠ

    • BlogIcon #에게 2021.10.29 15:44 신고

      form 태그로 보낸 데이트를 컨트롤러에서 어떻게 사용하냐는 질문이신걸까요?

      화면에서 사용자가 입력한 정보들은 Authentication 객체가 가지고 있어요.
      security 설정에서 username-parameter 와 password-parameter 로 설정한 ID 가 Authentication 객체에 담겨져요.
      그래서 AuthenticationProvider 에서 authentication.getPrincipal(), authentication.getCredentials 이런 형태로 아이디와 비밀번호를 가져온답니다.

  13. aaa 2021.11.27 23:56

    nullPointException에러 납니다.