본문으로 바로가기

UserDetailsService 인터페이스는 DB에서 유저 정보를 가져오는 역할을 한다. 해당 인터페이스의 메소드에서 DB의 유저 정보를 가져와서 AuthenticationProvider 인터페이스로 유저 정보를 리턴하면, 그 곳에서 사용자가 입력한 정보와 DB에 있는 유저 정보를 비교한다. 지금 우리가 할 것은 유저 정보를 가져오는 인터페이스를 구현하는 것이다. 사용자가 입력한 정보와 비교하는 작업은 이 글에서는 없다. DB에서 유저 정보를 가져오는 작업만 하기 때문에, 여기에서 필요한 인터페이스는 UserDetails 인터페이스와 UserDetailsService 인터페이스이다. 그럼 시작한다!


1. UserDetails

Spring Security에서 사용자의 정보를 담는 인터페이스는 UserDetails 인터페이스이다. 우리가 이 인터페이스를 구현하게 되면 Spring Security에서 구현한 클래스를 사용자 정보로 인식하고 인증 작업을 한다. 쉽게 말하면 UserDetails 인터페이스는 VO 역할을 한다고 보면 된다. 그래서 우리는 사용자의 정보를 모두 담아두는 클래스를 구현할 것이다.


UserDetails 인터페이스를 구현하게 되면 오버라이드되는 메소드들이 있다. 이 메소드들에 대해 파악을 해야 된다. 그리고 회원 정보에 관한 다른 정보(이름, 나이, 생년월일, ...)도 추가해도 된다. 오버라이드되는 메소드들만 Spring Security에서 알아서 이용하기 때문에 따로 클래스를 만들지 않고 멤버변수를 추가해서 같이 사용해도 무방하다. 만든 멤버변 수들은 getter, setter를 만들어서 사용하면 된다.


 메소드 명

리턴 타입

설명 

 getAuthorities()

 Collection<? extends   GrantedAuthority>

 계정이 갖고있는 권한 목록을 리턴한다.

 getPassword()

 String

 계정의 비밀번호를 리턴한다.

 getUsername()

 String

 계정의 이름을 리턴한다.

 isAccountNonExpired()

 boolean

 계정이 만료되지 않았는 지 리턴한다. (true: 만료안됨)

 isAccountNonLocked()

 boolean

 계정이 잠겨있지 않았는 지 리턴한다. (true: 잠기지 않음)

 isCredentialNonExpired()

 boolean

 비밀번호가 만료되지 않았는 지 리턴한다. (true: 만료안됨)

 isEnabled()

 boolean

 계정이 활성화(사용가능)인 지 리턴한다. (true: 활성화)


getUsername() 메소드를 보면 계정의 이름을 리턴한다고 해놓았는데, 계정의 아이디(혹은 이메일)을 리턴한다고 생각하면 된다. 그리고 계정이 만료되었는 지, 계정이 잠겨있는 지 등등 이것들에 대해 체크할 필요가 없다면 true를 리턴해주면 된다. 만약 체크할 멤버변수가 존재한다면 그 멤버변수를 리턴해주면 된다. 내가 사용하는 DB에는 계정의 활성/비활성화를 확인하는 멤버변수가 존재한다. 따라서 isEnabled() 메소드는 해당 멤버변수를 리턴해준다.


CustomUserDetails.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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@SuppressWarnings("serial")
public class CustomUserDetails implements UserDetails {
    
    private String ID;
    private String PASSWORD;
    private String AUTHORITY;
    private boolean ENABLED;
    private String NAME;
    
    @Override
    public Collection<extends GrantedAuthority> getAuthorities() {
        ArrayList<GrantedAuthority> auth = new ArrayList<GrantedAuthority>();
        auth.add(new SimpleGrantedAuthority(AUTHORITY));
        return auth;
    }
 
    @Override
    public String getPassword() {
        return PASSWORD;
    }
 
    @Override
    public String getUsername() {
        return ID;
    }
 
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
 
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
 
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
 
    @Override
    public boolean isEnabled() {
        return ENABLED;
    }
    
    public String getNAME() {
        return NAME;
    }
 
    public void setNAME(String name) {
        NAME = name;
    }
 
}
cs

- 04~08행: 사용자 정보들의 멤버변수를 선언한다. 정보에 관한 멤버변수가 더 필요하다면 추가해도 된다.

- 12~14행: 계정이 갖고 있는 권한을 목록으로 리턴하기 위한 설정이다. 

- 27~40행: 체크를 하지 않아도 되는 항목이라서 모두 true를 리턴한다.

- 43~45행: 계정의 활성/비활성 여부가 담긴 ENABLED 멤버변수를 리턴한다.

- 47~53행: 사용자의 추가 정보들의 getter, setter 이다.



2. UserDetailsService

사용자의 정보를 담을 객체를 만들었으니, DB에서 유저 정보를 직접 가져오는 인터페이스를 구현해보자. UserDetailsService 인터페이스에는 DB에서 유저 정보를 불러오는 중요한 메소드가 존재한다. 바로 loadUserByUsername() 메소드이다. 이 메소드에서 유저 정보를 불러오는 작업을 하면 된다. UserDetailsService 인터페이스를 구현하면 loadUserByUsername() 메소드가 오버라이드 될 것이다. 여기에서 CustomUserDetails 형으로 사용자의 정보를 가져오면 된다. 가져온 사용자의 정보를 유/무에 따라 예외와 사용자 정보를 리턴하면 된다. 다시 한번 말하자면 이 부분은 DB에서 유저의 정보를 가져와서 리턴해주는 작업이다. 


CustomUserDetailsService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CustomUserDetailsService implements UserDetailsService {
    
    @Autowired
    private UserAuthDAO userAuthDAO;
 
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        CustomUserDetails user = userAuthDAO.getUserById(username);
        if(user==null) {
            throw new UsernameNotFoundException(username);
        }
        return user;
    }
 
}
cs

- 07행: loadUserByUsername() 메소드를 오버라이드한다.

- 08행: 사용자의 정보를 CustomUserDetails 형으로 가져온다.

- 09~11행: 만약 해당 username의 사용자 정보가 없다면 UsernameNotFoundException 예외를 던져준다.

- 12행: 사용자의 정보가 담긴 user를 리턴해준다.



3. DAO, SQL

UserAuthDAO.java

1
2
3
4
5
6
7
8
9
10
11
@Repository("userAuthDAO")
public class UserAuthDAO {
    
    @Autowired
    private SqlSessionTemplate sqlSession;
 
    public CustomUserDetails getUserById(String username) {
        return sqlSession.selectOne("user.selectUserById", username);
    }
 
}
cs


user_sql.xml

1
2
3
4
5
6
7
8
9
10
11
12
<mapper namespace="user">
     <select id="selectUserById" resultType="tody.common.vo.CustomUserDetails">
        <![CDATA[
            SELECT
                *
            FROM
                user
            WHERE
                ID=#{ loginId}
        ]]>
    </select>
 </mapper>
cs

- 02행: resultType이 반드시 있어야한다.

- 09행: 로그인 페이지에서 지정한 ID의 input 태그에 있는 name 속성을 적어줘야 한다.



4. user-service-ref

context-security.xml

1
2
3
4
5
6
7
8
9
10
<context:component-scan base-package="tody.common.dao"/>
 
...
 
<security:authentication-manager>
    <security:authentication-provider user-service-ref="userService">
    </security:authentication-provider>
</security:authentication-manager>
        
<bean id="userService" class="tody.common.service.CustomUserDetailsService"/>
cs

- 01행: 이 부분이 없으면 오류가 발생한다. 여기에 대해서 완전히 파악한 게 아니라서 정확하게 말할 순 없지만 security 설정파일에서 SqlSessionTemplate 부분을 찾아야 되기 때문에 component 설정을 해주는 것 같다.

- 06행: 유저 정보를 가져오는 클래스를 설정하는 곳이다. userService는 10행의 클래스를 뜻한다. 

- 10행: UserDetailsService 인터페이스를 구현해서 만든 클래스를 userService라는 이름으로 bean 등록한다.



5. 실행

계정이 활성화되어있는 사용자의 정보로 로그인하면 위와 같은 로그를 볼 수 있다. 아무런 에러가 안난다면 UserDetailsService 인터페이스가 잘 구현된 것이다. 야호! 원래는 UsetDetails 인터페이스와 UserDetailsService 인터페이스를 분리해서 설명하려고 했다. UserDetailsService 인터페이스로만으로 유저 정보를 받아오게끔 하고 싶었기 때문이다. 근데 resultType에서 UserDetails 인터페이스를 가져올 방법을 도저히 찾지 못해서.. 그냥 둘 다 합쳐서 구현하게 되었다.(ㅎㅎ더이상 찾기 귀찮아.. 아는 사람은 아시는 분 대로...) 인증 절차 작업에서는 DB 유저 정보와 입력 정보를 비교하는 인터페이스를 구현하는 작업만 남았다. 휴!


GitHub


댓글을 달아 주세요

  1. 초보자 2019.02.18 18:12

    지난번에 이어서 또 막히는 부분이 생겨서 질문 드립니다.. 하하

    혹시 해당 포스트 내용이 [Spring Security - MyBatis 연결 및 DB를 이용한 간단한 로그인 인증]의 연장선 인가요 ???

    링크 걸어주신 깃허브 들어가서 살펴보는데 이전 포스트 내용이 없는 것 같아서, 삭제하고 이 포스트 보면서 로그인 인증 절차? 하고 있는데 ..

    혹시 다음 포스팅 [인증 절차 인터페이스 구현(2)] 까지 완료해야지 완벽한 로그인 처리가 되는 것 인지 궁금합니다. DB에 임의의 데이터를 집어 넣고 로그인 테스트를 해보는데 잘 안되네요 ㅠ

    잘 동작하나 살펴보려고 CustomUserDetailsService의 loadUserByUsername()에 sysout 하나 찍어뒀는데.. 콘솔창에 나오지 않네요 ...

    • BlogIcon #에게 2019.02.18 18:18 신고

      네! 연장이라고 보면 돼요. 이전 포스팅 내용이 없는 이유는 이 글을 보다 보면 나오지만, security 설정에서 사용하던 쿼리가 빠지고 userDetailsService 를 사용하기 때문이에요! 콘솔에 나오지 않는 건 security 설정에서 customUserDtailsService를 셋팅하셨는 지 확인해보셔야 할거 같아요! 4번을 참고해주세용. 그리고 구현(2)는 굳이 하지 않아도 됩니당~

    • 초보자 2019.02.19 13:29

      감사합니다 ! 이번에도 덕분에 바로 찾아서 해결 했습니다 !

      그런데 ... 추가적으로 문제가 생겼네요 ㅠㅠ

      DB에 존재하는 활성화 계정
      = [Bad credentials]

      DB에 존재하지 않는 계정
      = [Bad credentials]

      DB에 존재하는 비활성화 계정
      = [User is disabled]

      각각 이렇게 오류가 발생하면서, 로그인 처리가 이루어지지 않습니다..

      CustomUserDetailsService에서 Dao로 선택한 유저 콘솔창에 찍어보면

      DB상에 존재하지 않는 계정은 null로 표기되고

      DB상에 존재하는 계정은 [customUserDetails@54bcf081] 이런식으로 표기 되는 것을 보면

      DB를 통해서 데이터를 가져오기는 하는 것 같은데..

      무슨 이유로 활성화 계정도 Bad credentials가 발생하는지 모르겠네요 ... ㅠㅠ

    • 초보자 2019.02.19 13:41

      customUserDetails에 toString 추가해서
      콘솔창에 다시 제대로 찍어보니

      CustomUserDetailsService에서 userAuthDao로 선택한 유저에

      ID와 PASSWORD가 NULL로 찍히네요 ;;

      [customUserDetails [ID=null, PASSWORD=null, AUTHORITY=ROLE_EMAIL, ENABLED=true, NAME=미인증]]

    • 초보자 2019.02.19 13:46

      해결했습니다 !

      customUserDetails에 적은 변수명 하고 DB상에 칼럼명이 달라서 값을 제대로 받아오지 못하고 있었네요

      댓글,... 삭제하는 방법을 몰라서 계속 이렇게 댓글 남깁니다 죄송합니다 ㅠㅠ

      덕분에 스프링 공부 잘 하고 있습니다 !!

    • BlogIcon #에게 2019.02.19 13:49 신고

      댓글 다는 와중에 해결하셨네요! 원래 이렇게 누구한테 문제점들을 설명하다가 해결되는 경우가 많아요ㅎㅎ 스스로가 정리가 되니까요! 이런 댓글도 저는 감사합니다! 지우시지 않으셔도 돼요! 다른 분들에게도 좋은 댓글이 될 수 있으니까요!

  2. 초보 2019.03.21 15:57

    이전에 spring-security.xml의 속성인 users-by-username-query와 authorities-by-username-query 에서 인증과 권한 한 확인하는걸 UserDetailService 인터페이스를 implements해서 로그인 처리를 한다는거죠??(두서없이 질문을해서 죄송합니다 ㅎㅎ)

  3. 띠로리 2020.01.07 14:39

    해당 블로거글에 context-mapper.xml 파일에 sqlSessionTemplate 빈등록하는 과정이 필요한거 같습니다. 아직 게시글(2)는 보지않았지만

    context-mapper.xml 파일에 아래 빈정보를 등록하는 부분이 있어야 정상적인 실행이 될거같습니다..

    <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
    <constructor-arg index="0" ref="sqlSession"/>
    </bean>

    • BlogIcon #에게 2020.01.07 17:09 신고

      안녕하세요! 맞아요! 저는 기본 설정이 완료된 프로젝트에 시큐리티 설정을 했기 때문에 그 부분은 생각도 못하고 넘어갔네요! 게시글에 추가해둬야겠네요 좋은 정보 감사합니다 :)

  4. 완전썡초보 2020.02.12 18:25

    안녕하세요~ 지금 프로젝트를 하고있는 학생입니다 ㅠㅠ시큐리티자세하게 설명해주셔서 참고하고있는데
    저는로그인시 제가 입력한값과 DB쪽 값이 맞지만 로그인이 항상 실패로 떠서요 ㅠㅠ
    올려주신 자료 이외에 따로 설정해주신게 있을까요?

  5. 하야시 2020.05.18 20:14

    이렇게 새로운 기술 익히는데... 어떻게 하시나요 ??
    보통 혼자서 이렇게 질투나게... 이해하기 쉽지 않잖아요....
    구글링 하시나요... 어떻게 하시나요?? 궁금합니다..

    • BlogIcon #에게 2020.09.27 11:38 신고

      구글링을 주로 하는 편입니다ㅎㅎ 저도 이해 못해요ㅠㅠ 이해 못하는 부분도 있지만 이렇게 사용한다더라, 해서 포스팅을 하는 부분도 많아요 ㅠㅠㅎㅎ..

  6. spring 2020.05.31 15:06

    저는 로그인이 계속 안되었는데, dao쪽에 코드를 실행하기 직전까지는 실행이 되는데 그 이후엔 로그도 안뜨고 아무것도 안떠서 오류찾는데 좀 오래걸렸습니다.. exception을 throw하지 말고 try catch로 묶으시길 추천드립니다!
    그러면 catch에서 에러로그를 찍어줘서 답을 풀 수 있습니다..

    제가 에러가 났던 이유는, enabled에 대해서 오버라이딩으로 isenabled 메소드도 있고,
    또 이클립스 자체로 만들어 두었던 enabled 겟터셋터 메소드도 있고,
    이 두개가 중복으로 있으면 에러를 던지는 것을 확인 했습니다.. 혹시 로그인 안되시는 분들은 이것도 참고해보세요..

    • BlogIcon #에게 2020.09.27 11:40 신고

      exception throw 만으로도 에러가 잡히지 않는 경우가 있군요 ㅠㅠ 또 다른 방법을 제시해주셔서 감사합니당~

  7. BlogIcon 김코더 김주역 2021.01.28 16:46 신고

    덕분에 성공했습니다 감사합니다!

  8. 초보개발 2021.09.01 10:28

    Spring security 공부를 하고 있어 포스팅을 쭉 보고 있는데
    현재 login 컨트롤러가 정의되었지만 선언된 곳은 없는데 혹시 이 부분은 어떻게 되는걸까요??..