본문으로 바로가기

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