- Develop OS : Windows10 Ent, 64bit - WEB/WAS Server : Tomcat v9.0 - DBMS : MySQL 5.7.29 for Linux (Docker) - Language : JAVA 1.8 (JDK 1.8) - Framwork : Spring 3.1.1 Release - Build Tool : Maven 3.6.3 - ORM : Mybatis 3.2.8 |
커스터마이징 순서대로 총 9개의 포스팅으로 나눠져 있습니다. 순서대로 보면 쉽게 적용할 수 있습니다.
[Spring MVC/- 기본 문법] - 스프링 Security_로그인_기본 컨텍스트 설정 [1/9]
[Spring MVC/- 기본 문법] - 스프링 Security_로그인_DB 연동 로직 작성 [2/9]
[Spring MVC/- 기본 문법] - 스프링 Security_로그인_로그인 실패 대응 로직 [3/9]
[Spring MVC/- 기본 문법] - 스프링 Security_로그인_로그인 성공 대응 로직 [4/9]
[Spring MVC/- 기본 문법] - 스프링 Security_로그인_DB 패스워드 암호화 [5/9]
[Spring MVC/- 기본 문법] - 스프링 Security_로그인_암호화된 DB 패스워드로 인증 [6/9]
[Spring MVC/- 기본 문법] - 스프링 Security_로그인_Principal 객체 [7/9]
[Spring MVC/- 기본 문법] - 스프링 Security_로그인_자동 로그인(Remember-me) [8/9]
[Spring MVC/- 기본 문법] - 스프링 Security_로그인_security 태그 라이브러리 [9/9]
이전글에서 컨텍스트 설정을 완료했으니 이번엔 로그인을 요청한 사용자의 ID, PW, 권한 정보를 DB에서 가져와 스프링 Security에게 전달해주는 클래스만 작성하면 됩니다.
DB는 MySQL, 연동은 Mabatis를 사용했습니다. 어떤걸 어떻게 사용하던 상관 없습니다. 그냥 정해진 형식으로 스프링 Security에게 해당 사용자에 대한 정보를 전달해주기만 하면 됩니다.
[ 데이터 전달 구조 ]
생소한 클래스들이 좀 나와서 복잡해보이지만 별 거 없습니다. 이미 만들어진 클래스들을 구현해 필요한 정보만 담아주면 됩니다. 아래와 같은 구조로 데이터를 최종적으로 전달합니다.
데이터 클래스의 데이터는 어떻게 채워넣든 개발자 마음이고, 중요한 부분은 서비스 클래스가 최종적으로 완성된 데이터 클래스의 객체를 리턴하기만 하면 된다는 것입니다.
1. 데이터 전달 클래스(VO, DTO) 작성 (UserDetails 인터페이스 구현)
DB에 저장된 사용자의 "ID, PW, 권한" 정보를 저장하는 데이터 전달 객체입니다. UserDetails 인터페이스를 구현받아야 하고, 7개의 메소드를 오버라이딩 해줘야 합니다. "ID, PW, 권한"의 가장 기본적인 세 가지를 제외하고 나머지는 추가로 활용할 수 있는 기능들입니다. 나중에 Provider 까지 커스터마이징 하고나면 그대로 구성할 필요는 없지만 일단 기본대로 구조를 따라가보겠습니다. 스프링 구조에 맞춰서 커스터마이징하는 것이 가장 편리하고 깔끔할 듯합니다.
UserDetailsService 인터페이스를 구현한 서비스 클래스에서 최종적으로 완성된 데이터 클래스 객체를 return 시키면 스프링 Security에게 전달이 됩니다. 그리고 오버라이딩 된 7개의 메소드를 실행해 정보를 가져가므로, 7개 메소드에서 필요한 부분의 리턴값을 잘 설정해주면 됩니다. 3개는 getter 메소드이고 4개는 계정에 대한 부가 정보입니다.
아래 설명은 제가 구현한 방식이며 오버라이딩하는 메소드는 어떻게 구현하건 리턴 값만 잘 적용되면 됩니다. 아래는 전체코드이고 이어서 각 메소드에 대한 설명입니다.
package hs.spring.hsweb.mapper.vo.user;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
/* Spring Security 로그인을 위한 UserDetails VO 객체 */
public class UserDetailsVO implements UserDetails {
// 안만들어도 상관없지만 Warning이 발생함
private static final long serialVersionUID = 1L;
private String username; // ID
private String password; // PW
private List<GrantedAuthority> authorities;
// setter
public void setUsername(String username) {
this.username = username;
}
// setter
public void setPassword(String password) {
this.password = password;
}
// setter
public void setAuthorities(List<String> authList) {
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
for (int i = 0; i < authList.size(); i++) {
authorities.add(new SimpleGrantedAuthority(authList.get(i)));
}
this.authorities = authorities;
}
@Override
// ID
public String getUsername() {
return username;
}
@Override
// PW
public String getPassword() {
return password;
}
@Override
// 권한
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
// 계정이 만료 되지 않았는가?
public boolean isAccountNonExpired() {
return true;
}
@Override
// 계정이 잠기지 않았는가?
public boolean isAccountNonLocked() {
return true;
}
@Override
// 패스워드가 만료되지 않았는가?
public boolean isCredentialsNonExpired() {
return true;
}
@Override
// 계정이 활성화 되었는가?
public boolean isEnabled() {
return true;
}
}
* 필드에 대한 setter 메소드
나중에 객체의 데이터를 완성시켜줘야 하므로 setter를 추가해줍니다. 필드는 위의 세 개가 가장 기본인데 추가로 더 구성해서 사용해도 무방합니다.
* public String getUsername() : ID 값
ID정보의 필드에 대한 getter 입니다. security에서는 ID를 'username'이라고 표현합니다.
private String username; // ID
@Override
// ID
public String getUsername() {
return userId;
}
* public String getPassword() : PW 값
위와 동일한 구성입니다.
private String password; // PW
@Override
// PW
public String getPassword() {
return userPw;
}
* public Collection<? extends GrantedAuthority> getAuthorities() : 권한
권한은 'GrantedAuthority' 인터페이스를 구현한 'SimleGrantedAuthority' 클래스에 하나씩 담아주면 됩니다. 한 객체 당 하나의 권한이며, 컬렉션 계열에 모두 담아주면 됩니다.
한 사람이 여러 권한을 가질 수 있기 때문에 복수의 데이터를 담을 수 있도록 되어 있고, 이름은 복잡하지만 실질적으로 우리는 그냥 String 타입의 권한명만 객체에 담아주면 되는지라 매우 간단한 구조입니다.
List<String> 타입으로 권한 데이터를 받아서 해당 타입으로 변환해주는 setter를 만들어서 주입해줬습니다. 스프링 Security에서 고정된 권한 리스트 타입이기 때문에 그대로 구현해줘야 합니다.
- 권한 객체 생성 : new SimpleGrantedAuthority("권한명")
private List<GrantedAuthority> authorities;
// setter
public void setAuthorities(List<String> authList) {
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
for (int i = 0; i < authList.size(); i++) {
authorities.add(new SimpleGrantedAuthority(authList.get(i)));
}
this.authorities = authorities;
}
@Override
// 권한
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
나머지 메소드는 필요하면 추가로 구현하는 값들이므로 일단 기본값으로 셋팅했습니다. DB에 상태값을 저장해뒀다가 가져와서 판별해주면 됩니다. 모든 커스터마이징을 끝내고 나면 굳이 그대로 사용하지 않아도 되지만, 일단 구현되어 있으니 필요하면 적당히 재활용해주는 것이 좋습니다.
* public boolean isAccountNonExpired() : 계정이 만료되지 '않았는가'?
DB에 만료여부에 대한 컬럼을 따로 만들어두고 판별해서 만료된 계정이라면 false, 만료되지 않았다면 true를 반환하면 됩니다. "만료되지 않았는지(Non) 여부" 이기 때문에 true를 반환해야 사용가능한 계정이라는 뜻입니다.
* public boolean isAccountNonLocked() : 계정이 잠기지 '않았는가'?
만료가 아니라 잠김입니다. 만료는 유효기간이 다 됐다는 뜻이고 잠김은 좀 더 여러가지 이유로 인해 사용 불가 처리된 것이라 생각하면 될 것 같습니다. '않았는가'로 묻는 메소드이기 때문에 역시 true가 정상을 의미합니다.
사실 크게 의미는 없고 이 메소드에서 false가 날 경우 어떻게 처리할 것인가를 커스터마이징하면 되는 문제라 메소드의 의미 자체는 크게 상관이 없습니다. 다만 헷갈리지 않도록 여러 가지 가능성들을 메소드로 정의해둔 것입니다. false일 경우 어떻게 처리해준다는 로직을 미리 만들어둔 것이 아닙니다.
* public boolean isCredentialsNonExpired() : 패스워드가 잠기지 '않았는가'?
위와 마찬가지로 여러 가능성 중 하나에 대한 메소드입니다. 이번엔 계정이 아닌 패스워드 만료입니다. 계정 자체는 살아있으나 패스워드가 만료된 경우이며, 이 경우 패스워드 변경 화면을 띄워준다거나 하는 로직으로 대응하면 됩니다. Credential은 패스워드를 의미합니다. 역시 true가 정상을 의미합니다.
* public boolean isEnabled() : 계정이 사용 '가능한가'? (활성화 되었는가?)
또 미묘하게 어감이 다른 메소드입니다. 그냥 마음대로 해석해서 로직을 대응시키면 됩니다. 이번에는 '가능한가?'로 물었기 때문에 true가 정상을 의미합니다. 네 가지 메소드 모두 true가 정상적인 계정을 의미한다고 보면 됩니다.
2. 서비스 클래스 작성 (UserDetailsService 인터페이스 구현)
컨텍스트 설정의 아래 설정 부분에 레퍼런스 Bean 객체로 주입해준 클래스입니다. 이 클래스를 작성해두고 위에서 작성한 데이터 전달 클래스 객체의 완성본을 return 시켜주면 스프링 Security로 인증을 위한 정보가 최종 전달됩니다.
직접 작성한 클래스의 Bean을 Authentication-provider에게 주입해주면 해당 Bean에 오버라이딩 된 메소드의 리턴값을 통해 ID, PW, 권한 데이터를 받아 인증을 진행하고, 레퍼런스를 비워두면 미리 만들어진 디폴트 클래스를 Bean으로 등록해 사용합니다. 하지만 스프링 Security의 디폴트 로직에는 DB 연동이 없으므로 실제 사용할 일은 거의 없습니다.
그리고 나중에 이 Provider도 커스터마이징하면 아래 설정은 필요 없어집니다. 순서대로 글을 읽으시면 알 수 있습니다.
아래는 구현 코드입니다. 구현 방식은 엿장수 마음이고 중요한 것은 완성된 데이터 클래스 객체를 리턴해주는 것이므로 별도의 설명은 생략하겠습니다. 위에서 작성한 VO 객체에 값을 넣어 완성시킨 뒤 리턴해주면 됩니다.
package hs.spring.hsweb.service.user;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;
import hs.spring.hsweb.mapper.user.UserMapper;
import hs.spring.hsweb.mapper.vo.user.UserDetailsVO;
import hs.spring.hsweb.mapper.vo.user.UserInfoVO;
@Service
public class UserDetailsServiceCustom implements UserDetailsService {
@Autowired
private UserMapper mapper;
@Override
public UserDetails loadUserByUsername(String inputUserId) {
// 최종적으로 리턴해야할 객체
UserDetailsVO userDetails = new UserDetailsVO();
// 사용자 정보 select
UserInfoVO userInfo = mapper.selectUserInfoOne(inputUserId);
// 사용자 정보 없으면 null 처리
if (userInfo == null) {
return null;
// 사용자 정보 있을 경우 로직 전개 (userDetails에 데이터 넣기)
} else {
userDetails.setUsername(userInfo.getUserId());
userDetails.setPassword(userInfo.getUserPw());
// 사용자 권한 select해서 받아온 List<String> 객체 주입
userDetails.setAuthorities(mapper.selectUserAuthOne(inputUserId));
}
return userDetails;
}
}
이전글에서 다룬 컨텍스트 설정 파일 작성과 이번글에서 다룬 클래스 파일 작성만 완료했다면 DB 로그인 로직이 정상적으로 작동할 것입니다. 추가적인 기능들은 계속해서 정리하도록 하겠습니다.
'▸Spring Security' 카테고리의 다른 글
스프링 Security_로그인_암호화된 DB 패스워드로 인증 [6/9] (0) | 2020.03.29 |
---|---|
스프링 Security_로그인_DB 패스워드 암호화 [5/9] (1) | 2020.03.29 |
스프링 Security_로그인_로그인 성공 대응 로직 [4/9] (1) | 2020.03.28 |
스프링 Security_로그인_로그인 실패 대응 로직 [3/9] (1) | 2020.03.28 |
스프링 Security_로그인_기본 컨텍스트 설정 [1/9] (4) | 2020.03.27 |
댓글