안녕하세요, 코린이의 코딩 학습기 채니 입니다.
개인 포스팅용으로 내용에 오류 및 잘못된 정보가 있을 수 있습니다.
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에 저장되어있는 회원으로 로그인 시도
정보를 잘 가져오고, 로그인 처리도 정상적으로 작동하는 것을 확인할 수 있습니다.
'Java > Spring' 카테고리의 다른 글
Spring) Spring-WebSocket (관련 설정) (2) | 2022.09.14 |
---|---|
Spring) Security - 인증된 사용자 정보 가져오기, 처리 순서 파악 (2) | 2022.09.11 |
Spring) Security - 로그인 후처리 (0) | 2022.09.11 |
Spring) Security - 커스텀 로그인/로그아웃 페이지, CSRF 공격대비 설정(403에러), error처리 (0) | 2022.09.11 |
Spring) Spring Security - 설정 및 관련 설정 구체화 (설명 포함) (0) | 2022.09.10 |