본문 바로가기
Java/Spring

Spring) Security - DB에 있는 사용자 조회(로그인 처리), UserDetails/UserDetailsService

by 박채니 2022. 9. 11.
안녕하세요, 코린이의 코딩 학습기 채니 입니다.
개인 포스팅용으로 내용에 오류 및 잘못된 정보가 있을 수 있습니다.

 

authority 테이블 생성

create table authority(
    member_id varchar2(20),
    auth varchar2(50),
    constraint pk_authority primary key (member_id, auth),
    constraint fk_authority_member_id foreign key(member_id) references member(member_id) on delete cascade
);

insert into authority values('abcde', 'ROLE_USER');
insert into authority values('abcdef', 'ROLE_USER');
insert into authority values('abcdefe', 'ROLE_USER');
insert into authority values('qwerty', 'ROLE_USER');
insert into authority values('honggd', 'ROLE_USER');
insert into authority values('admin', 'ROLE_USER');
insert into authority values('admin', 'ROLE_ADMIN');
commit;

select * from authority;


Security - DB에 있는 사용자 조회 

 

사용자와 대응하는 Member 클래스UserDetails 구현

이를 조회하는 Service클래스 UserDetailsService 구현

 

com.ce.spring2 패키지 하위에 클래스를 생성한다면 servlet-context에서 빈이 관리되므로,

com.ce 패키지 하위에 클래스를 생성해줍니다. (DB관련은 root-context에서 관리!)

 

클래스 구조

Dto

MemberEntity - DB 레코드와 상응

@Data
@NoArgsConstructor
@AllArgsConstructor
public class MemberEntity {
	@NonNull
	protected String memberId;
	@NonNull
	protected String password;
	@NonNull
	protected String name;
	protected Gender gender;
	@DateTimeFormat(pattern = "yyyy-MM-dd") //클라이언트에서 온 값을 커맨드객체에 대입하기 위함!
	protected LocalDate birthday;
	protected String email;
	@NonNull
	protected String phone;
	protected String address;
	protected String[] hobby;
	protected LocalDateTime createdAt;
	protected LocalDateTime updatedAt;
	protected boolean enabled;
}

 

Dto

Member - UserDetails 구현

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
public class Member extends MemberEntity implements UserDetails {
	/**
	 * SimpleGrantedAuthority
	 * - 문자열로 권한을 관리
	 * "ROLE_USER" -> new SimpleGrantedAUthority("ROLE_USER")
	 */
	private List<SimpleGrantedAuthority> authorities;
	
	/**
	 * 사용자가 어떤 권한을 가지고 있는 지 조회
	 * GrantedAuthority -> 실제 권한 정보를 가진 객체
	 */
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return authorities;
	}

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

	/**
	 * 계정 만료 여부
	 */
	@Override
	public boolean isAccountNonExpired() {
		// TODO Auto-generated method stub
		return enabled;
	}

	/**
	 * 계정 잠김 여부
	 */
	@Override
	public boolean isAccountNonLocked() {
		// TODO Auto-generated method stub
		return enabled;
	}

	/**
	 * 비밀번호 만료 여부
	 */
	@Override
	public boolean isCredentialsNonExpired() {
		// TODO Auto-generated method stub
		return enabled;
	}
}

 

Service

MemberSecurityService - UserDetailsService 구현

@Service
@Slf4j
public class MemberSecurityService implements UserDetailsService {

	@Autowired
	MemberSecurityDao memberSecurityDao;
	
	/**
	 * 아이디를 가지고 조회 -> 조회된 결과를 UserDetails 리턴
	 * 
	 * username으로 해당 회원 조회(member, authority)
	 * - 조회된 회원이 없는 경우 UsernameNotFoundException 예외 던지기
	 */
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		Member member = memberSecurityDao.loadUserByUsername(username);
		if(member == null) {
			throw new UsernameNotFoundException(username);
		}
		log.info("member= {}", member);
		return member;
	}
}

// 로그인 성공 시
@콘솔출력값
INFO : com.ce.security.model.service.MemberSecurityService - member= Member(super=MemberEntity(memberId=honggd, password=$2a$10$G7F3EfcXE8wJWUAsCznaoOqttim4MwJBNlWey5yuL5ZRMwRSdPvEi, name=홍길동그랑땡, gender=M, birthday=1999-09-09, email=honggd12@abc.com, phone=01045674567, address=경기 성남시 분당구 대왕판교로 688, hobby=[등산, 게임], createdAt=2022-08-22T12:23:43, updatedAt=2022-08-23T12:47:25, enabled=true), authorities=[ROLE_USER])

// 존재하지 않는 아이디 로그인 시
DEBUG: com.ce.security.model.dao.MemberSecurityDao.loadUserByUsername - ==>  Preparing: select * from member where member_id = ? 
DEBUG: com.ce.security.model.dao.MemberSecurityDao.loadUserByUsername - ==> Parameters: asdfasdf(String)
DEBUG: com.ce.security.model.dao.MemberSecurityDao.loadUserByUsername - <==      Total: 0

 

Dao

MemberSecurityDao interface

public interface MemberSecurityDao {
	
	Member loadUserByUsername(String username);

}

 

mapper

security-mapper.xml

<mapper namespace="com.ce.security.model.dao.MemberSecurityDao">
  <select id="loadUserByUsername" resultMap="memberAutoMap">
  	select
  		*
  	from
  		member
  	where
  		member_id = #{username}
  </select>
  <resultMap type="member" id="memberAutoMap">
  	<id column="member_id" property="memberId"/>
  	<collection property="authorities"
  				javaType="arraylist"
  				column="member_id"
  				ofType="simpleGrantedAuthority"
  				select="selectAuthorities" />
  				 <!-- member_id값을 가지고 가서 selectAuthorities 쿼리 실행! -->
  </resultMap>
  
  <select id="selectAuthorities" resultMap="simpleGrantedAuthorityMap">
  	select
  		*
  	from
  		authority
  	where
  		member_id = #{memberId}
  </select>
  <!-- property 방식이 아닌 생성자를 이용해 대입! (setter가 없음) -->
  <resultMap type="simpleGrantedAuthority" id="simpleGrantedAuthorityMap">
  	<constructor>
  		<!-- auth컬럼을 가져와 생성자에 대입! (타입은 string) -->
  		<arg column="auth" javaType="string"/>
  	</constructor>
  </resultMap>
</mapper>

loadUserByUsername 쿼리가 실행되고, 그 안에 collection에 의해 selectAuthorities 쿼리가 실행됩니다.

member_id 값을 가지고 가서 selectAuthorities 쿼리를 실행하여 한 레코드의 값을 arraylist에 차곡차곡 쌓아줍니다.

 

또한SimpleGrantedAuthority는 setter가 없으므로 생성자를 이용해 대입해줍니다.

public SimpleGrantedAuthority(String role) {
    Assert.hasText(role, "A granted authority textual representation is required");
    this.role = role;
}

 

☆ simpleGrantedAuthority는 별칭 등록이 안되어있으므로 등록 처리!

mybatis-config.xml

  <typeAliases>
  	<!-- 별칭 등록 -->
  	<typeAlias type="org.springframework.security.core.authority.SimpleGrantedAuthority" alias="simpleGrantedAuthority"/>
  	<package name="com.ce.spring2"/> <!-- 해당 패키지 하위에 있는 클래스들을 소문자로 변환하여 별칭등록 -->
  </typeAliases>

 

security-context.xml

<!-- provider 관리 -->
<authentication-manager> 
    <!-- 인증관련 처리(인증주체) -->
    <authentication-provider user-service-ref="memberSecurityService">
        <password-encoder ref="bcryptPasswordEncoder"/>
    </authentication-provider> 
</authentication-manager>

user-service-ref에 memberSecurityService를 대입해줍니다.

하지만 이렇게만 해놓는다면 아래와 같은 오류가 발생하게 됩니다.

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter#0': Cannot resolve reference to bean 'org.springframework.security.authentication.ProviderManager#0' while setting bean property 'authenticationManager'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.security.authentication.ProviderManager#0': Cannot resolve reference to bean 'org.springframework.security.config.authentication.AuthenticationManagerFactoryBean#0' while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.security.config.authentication.AuthenticationManagerFactoryBean#0': FactoryBean threw exception on object creation; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.security.authenticationManager': Cannot resolve reference to bean 'org.springframework.security.authentication.dao.DaoAuthenticationProvider#0' while setting constructor argument with key [0]; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.security.authentication.dao.DaoAuthenticationProvider#0': Cannot resolve reference to bean 'memberSecurityService' while setting bean property 'userDetailsService'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'memberSecurityService' available

그 이유는 root-context에는 memberSecurityService 빈이 없기 때문이죠.

만일 servlet-context.xml에 등록하였으니 사용가능하다고 생각든다면, 틀렸습니다.

 

servlet-context.xml

<context:component-scan base-package="com.ce.spring2" />

servlet-context.xml에는 base-package가 "com.ce.spring2" 하위 패키지일 뿐더러, 만일 "com.ce" 하위 모든 패키지라고 해도,

root-context가 먼저 생성된 후 servlet-context가 생성되므로 root-context에서는 servlet-context에서 생성한 빈을 가져와 사용할 수 없기 때문이죠.

따라서 root-context에도 빈 등록을 해줘야합니다.

 

security-context.xml

<!-- @Service 클래스를 빈으로 등록 -->
<context:component-scan base-package="com.ce.security"/>

 

root-context.xml

<!-- #6.3 @Mapper 인터페이스 등록 : Dao 구현 객체를 동적으로 생성 -->
<mybatis-spring:scan base-package="com.ce.**.dao"/>

com.ce 하위에 모든 dao 패키지에 대해 @Mapper 인터페이스 등록을 하기 위해 위와 같은 처리도 필수!

 

 

DB에 저장되어있는 회원으로 로그인 시도

정보를 잘 가져오고, 로그인 처리도 정상적으로 작동하는 것을 확인할 수 있습니다.