본문 바로가기

프로그래밍/Spring Security

Spring Security의 계정 클래스와 권한 클래스 설계

저번 글에서는 Spring Security를 커스터마이징 하는데 있어 필요한 Database 스키마 설명을 했다. 이번에는 Spring Security에서 다루어지는 사용자(앞으로는 설명의 편의를 위해 계정이라고 하겠다)와 권한의 개념을 좀더 자세히 짚고 넘어가도록 하자

 

Spring Security 에서 계정 객체를 자바로 정의하기 위해서는 org.springframework.security.core.userdetails.UserDetails 인터페이스를 이해해야 한다. 이 인터페이스를 구현한 클래스를 Spring Security에서는 사용자라고 보고 작업을 하게 된다. 이 인터페이스에 정의된 메소드와 역할을 도표로 정리하면 다음과 같다

 

return 타입

 메소드명

 설명

 String

 getUsername()

 계정의 이름을 리턴한다

 String

 getPassword()

 계정의 패스워드를 리턴한다

 boolean

 isAccountNonExpired()

 계정이 만료되지 않았는지를 리턴한다(true를 리턴하면 만료되지 않음을 의미)

 boolean

 isAccountNonLocked()

 계정이 잠겨있지 않은지를 리턴한다(true를 리턴하면 계정이 잠겨있지 않음을 의미)

 boolean

 isCredentialsNonExpired()

 계정의 패스워드가 만료되지 않았는지를 리턴한다(true를 리턴하면 패스워드가 만료되지 않음을 의미)

 boolean

 isEnabled()

 계정이 사용가능한 계정인지를 리턴한다(true를 리턴하면 사용가능한 계정인지를 의미)

 Collection<? extends GrantedAuthority>

 getAuthorities()

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

 

계정의 이름이란 우리가 흔히 사용하는 로그인 ID라고 이해하면 된다. 우리가 계정 클래스를 설계할때 업무적인 용도땜에 여러가지 멤버변수를 넣을것이다. 계정을 사용하는 사용자의 이름도 넣을수 있고, 성별을 넣을수도 있으며, 정말 필요로 하다면 주소를 넣을수도 있다. 그런 멤버변수를 다 넣어도 된다. 넣어두되 계정 클래스를 UserDetails 인터페이스를 구현한 클래스로 설계하고 설명에 맞게 각 메소드를 구현해주면 된다. 예를 들어 계정이 만료되지 않았는지를 체크할 필요가 없다면 isAccountNoExpired() 메소드를 항상 true로 리턴하게끔 해주면 된다. 체크를 하고 그것이 DB에 저장되어 있다면 그것을 저장하는 멤버변수를 만들고 DB에서 조회해서 그 멤버변수에 넣게 한뒤에 isAccountNonExpired() 메소드를 그 변수값을 리턴하게끔 해주면 된다. 설명이 잘 이해가 안된다면 Spring Securiry에서 UserDetails 인터페이스를 구현한 클래스인 org.springframework.security.core.userdetails.User 클래스 소스를 보라. 그 소스코드와 User 클래스의 Javadoc 내용을 보면 이해할 수 있을 것이다.(다음에 보여지는 소스도 User 클래스 소스를 가져다가 쓴 부분이 몇몇 있다)

 

이런것을 이해하고 앞으로의 예제에서 사용될 계정 클래스의 소스 코드를 보도록 하자

 

import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.Assert;
public class MemberInfo implements UserDetails {

	private static final long serialVersionUID = -4086869747130410600L;
	
	private String id;                          // 계정 아이디
	private String password;                    // 계정 비밀번호
	private String name;                        // 계정 사용자의 이름
	private Set<GrantedAuthority> authorities;  // 계정이 가지고 있는 권한 목록
	
	public MemberInfo(String id, String password, String name, Collection<? extends GrantedAuthority> authorities){
		this.id = id;
		this.password = password;
		this.name = name;
		this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
	}

	public String getId() {
		return id;
	}

	public void setId(String id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public void setPassword(String password) {
		this.password = password;
	}
	
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		// TODO Auto-generated method stub
		return authorities;
	}
	
	public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
		this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
	}
	

	@Override
	public String getPassword() {
		// TODO Auto-generated method stub
		return password;
	}

	@Override
	public String getUsername() {
		// TODO Auto-generated method stub
		return getId();
	}

	@Override
	public boolean isAccountNonExpired() {
		// TODO Auto-generated method stub
		return true;
	}

	@Override
	public boolean isAccountNonLocked() {
		// TODO Auto-generated method stub
		return true;
	}

	@Override
	public boolean isCredentialsNonExpired() {
		// TODO Auto-generated method stub
		return true;
	}

	@Override
	public boolean isEnabled() {
		// TODO Auto-generated method stub
		return true;
	}
	
	
	private static SortedSet<GrantedAuthority> sortAuthorities(Collection<? extends GrantedAuthority> authorities) {
        Assert.notNull(authorities, "Cannot pass a null GrantedAuthority collection");
        // Ensure array iteration order is predictable (as per UserDetails.getAuthorities() contract and SEC-717)
        SortedSet<GrantedAuthority> sortedAuthorities =
            new TreeSet<GrantedAuthority>(new AuthorityComparator());

        for (GrantedAuthority grantedAuthority : authorities) {
            Assert.notNull(grantedAuthority, "GrantedAuthority list cannot contain any null elements");
            sortedAuthorities.add(grantedAuthority);
        }

        return sortedAuthorities;
    }
	
	private static class AuthorityComparator implements Comparator<GrantedAuthority>, Serializable {
        private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

        public int compare(GrantedAuthority g1, GrantedAuthority g2) {
            // Neither should ever be null as each entry is checked before adding it to the set.
            // If the authority is null, it is a custom authority and should precede others.
            if (g2.getAuthority() == null) {
                return -1;
            }

            if (g1.getAuthority() == null) {
                return 1;
            }

            return g1.getAuthority().compareTo(g2.getAuthority());
        }
    }

}

 

이 소스에서 getId() 메소드getUsername() 메소드를 비교해보도록 하자. getId() 메소드는 멤버변수 id의 getter 메소드이다. 그래서 멤버변수 id를 return 하고 있다. 그럼 이제 getUsername() 메소드를 보자. getUsername() 메소드는 UserDetails 인터페이스에 정의된 메소드이다. 위에 작성한 도표를 보면 이 메소드는 계정 이름을 리턴하도록 되어 있다. 계정의 이름이란 무엇인가? 바로 로그인 ID이다. 그 역할을 하는 것이 멤버변수 id이고 이 멤버변수 id의 getter 메소드인 getId() 메소드를 내부에서 사용해서 리턴하도록 되어 있다. 즉 클래스 내부에서 getter, setter 함수 작성하는 것과 별도로 Spring Security가 사용하게 될 UserDetails 인터페이스 구현 함수도 멤버변수의 getter 함수를 상황에 맞게 사용하는 식으로 맞춰서 구현해주면 된다. 이 클래스는 처음 객체를 생성하는 시점에 사용자 계정 이름, 패스워드, 권한 등을 셋팅하고 있으며 앞으로 설명할 예제에서는 패스워드 만료를 체크하거나 계정 사용 가능 여부 등의 기능은 사용하지 않을 것이어서 UserDetails 인터페이스의 이와 관련된 메소드에서는 모두 true를 return 하도록 했다.(이것을 사용해야 한다면 true가 아니라 이것을 검사하는 로직을 넣어서 그에 맞춰서 true, false를 리턴해주면 된다)

 

흔히 Spring Security와 관련되어 많이 나오는 질문 중에 하나로 나오는게 계정 이름과 비밀번호가 아닌 추가 데이터를 어떻게 조회해야 하는가 이 부분이다. 이 부분에 대한 멤버변수가 name이다. 사용자 이름을 넣는 이 부분은 Spring Security 에서는 이용하지 않는 부분이다. 이런게 사용자 이름 뿐만 아니라 기타 다른정보(ex: 사용자 결혼여부)도 있다면 그에 대한 멤버변수도 만들어두자. 그리고 그렇게 만들어진 변수들에 대한 getter, setter 함수를 만들어두자. 나중에 이런 성격들이 변수값을 넣고 그것을 꺼내 쓰는 부분을 설명할 것이다.

 

Spring Security에서 권한 객체는 org.springframework.security.core.GrantedAuthority 인터페이스를 구현한 클래스 객체로 만들면 된다. Spring Security 에서는 이를 구현한 클래스를 4가지를 제공하고 있으며 이 중 org.springframework.security.core.authority.SimpleGrantedAuthority 클래스를 사용할 것이다. 권한을 저장하기 위한 구조는 단순하게 되어 있다. 예를 들어 "사용자 권한" 이란 권한이 있다고 하자. 그러면 "사용자 권한"이란 문자열 값을 SimpleGrantedAuthority의 생성자 파라미터에 넣어주는 것으로 권한 객체 생성은 끝이 난다. 즉 권한 명칭만 저장하는 것이다.

 

이번 글에서는 Spring Security에서 사용하게 될 계정 클래스와 권한 클래스에 대한 설계쪽을 언급했다. 다음 글에서는 이런 정보를 조회하는 기능과 이를 Spring Security에 설정하는 부분을 설명하도록 하겠다