DB에서 인증 처리를 하기 위해 UserDetailsService클래스를 커스트마이징 해야 합니다. spring security 인증 처리에 관련된 클래스를 저장하기 위해 "ktds.erp.emp.authentication"패키지를 작성합니다.
[step1 - 커스트마이징한 UserDetailsService의 loadUserByUsername이 호출되도록 작성]
커스트마이징을 한다고 해도 우선 호출이 자동으로 되어야 뭘 추가하고 변경할 수 있겠죠?
UserDetailsService를 상속하는 SecurityLoginService를 작성합니다. loadUserByUsername메서드를 오버 라이딩하고 매개변수로 전달되는 username을 콘솔에 출력합니다.
@Service("loginService")
public class SecurityLoginService implements UserDetailsService {
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println(username+"........loadUserByUsername");
return null;
}
}
security-config_ver5.xml로 변경한 후 web.xml에서 security-config_ver5.xml를 인식할 수 있도록 수정합니다.
web.xml
security-config_ver5.xml
사용자 정의로 작성한 UserDetailsService를 실행할 수 있도록 <bean> 엘리먼트를 이용하여 빈으로 등록하고 <authentication-provider> 엘리먼트의 "user-service-ref"속성에 빈을 추가합니다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.2.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.2.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<security:http pattern="/**/*.js" security="none" />
<security:http pattern="/**/*.css" security="none" />
<security:http pattern="/images/**" security="none" />
<security:http auto-config="true" use-expressions="true">
<!-- <security:intercept-url pattern="/images/**" access="ROLE_ANONYMOUS"/> -->
<security:intercept-url pattern="/admin/**"
access="hasRole('admin')" />
<security:intercept-url pattern="/emp/login"
access="permitAll" />
<security:intercept-url pattern="/emp/logout.do"
access="hasAnyRole('user','admin')" />
<security:intercept-url pattern="/index.do"
access="permitAll" />
<security:intercept-url pattern="/**/user/**"
access="hasAnyRole('user','admin')" />
<security:intercept-url pattern="/board/*"
access="permitAll" />
<security:intercept-url pattern="/emp/*"
access="hasRole('admin')" />
<security:intercept-url pattern="/**"
access="hasAnyRole('user','admin')" />
<security:form-login username-parameter="id"
password-parameter="pass" login-page="/emp/login" default-target-url="/index.do"
authentication-failure-url="/emp/login.do?fail=true" />
<security:logout delete-cookies="true"
logout-success-url="/emp/login" logout-url="/emp/logout.do"
invalidate-session="true" />
</security:http>
<security:authentication-manager>
<security:authentication-provider user-service-ref="loginService">
</security:authentication-provider>
</security:authentication-manager>
<import resource="spring-config.xml" />
</beans>
user-service-ref는 사용자 정의로 작성된 UserDetailsService빈을 등록하는 속성으로 "loginService"로 정의된 빈을 UserDetailsService로 등록하겠다는 의미입니다.
인증된 정보가 어떤 타입으로 실행되는지 확인하기 위해 최초로 실행되는 /index.do가 정의된 IndexController의 main메서드에 매개변수를 추가합니다.
인증정보를 출력하기 위해 추가한 매개변수입니다. java.security.Principal객체는 인증 정보가 저장되며 인증된 정보로 mapping된 객체를 확인하기 위해 추가하였습니다.
server를 restart하고 실행해보도록 하겠습니다.
최초 요청 시에는 인증된 정보가 없으므로 다음과 같이 Principal객체가 null로 출력됩니다.
게시판의 상세보기를 클릭하여 인증화면으로 전환한 후 db에 저장된 "92115kim"과 "1234"를 각각 아이디와 패스워드에 입력하고 로그인 버튼을 누릅니다.
아직 인증 처리를 하지 않았고 loadUserByUsername메소드에서 null을 리턴하기 때문에 UsernamePasswordAuthenticationFilter에서 InternalAuthenticationServiceException이 발생하고 있습니다.
그러나 우리가 사용자정의로 작성한 UserDetailsService의 loadUserByUsername메서드는 잘 호출되고 있는 것이 확인되었습니다.
[step2 - loadUserByUsername의 매개변수로 전달된 아이디 정보로 db에서 사용자 확인하기]
우리가 작성한 SecurityLoginService클래스는 서비스 클래스입니다. 이 클래스 내부에서 인증을 요청한 사용자의 아이디로 DB에서 조회하여 사용자 정보가 있는지 확인합니다.
emp.xml mapper에 이미 만들어 놓은 "login" <select>엘리먼트 태그를 복사해서 "securityLogin"라고 변경합니다. id만 가지고 조회할 것이므로 아래와 같이 수정합니다.
우리 시스템은 로그인한 후 로그인한 사용자의 작업 그룹이 어느 그룹에 속해 있냐에 따라 보일 페이지가 달라져야 하므로 기존 로그인과 동일하게 resultType="login"으로 정의합니다.
<select id="securityLogin" resultType="login" parameterType="String">
select m.*,d.deptname,j.job_category,j.menupath
from member m, dept d, job j
where m.deptno = d.deptno and d.job_category = j.job_id
and id=#{id}
</select>
그래도 DAO메소드에는 추가를 해야겠죠? EmpDAO와 EmpDAOImpl에 메서드를 추가하도록 하겠습니다.
EmpDAO
id로 db에서 조회하여 DTO를 리턴하는 findByUsername을 정의합니다.
package ktds.erp.emp;
import java.util.ArrayList;
public interface EmpDAO {
ArrayList<MemberDTO> getTreeEmpList(String deptno);
int insert(MemberDTO user);
ArrayList<MemberDTO> getMemberList();
int delete(String id);
MemberDTO read(String id);
ArrayList<MemberDTO> search(String column, String search,String pass);
int update(MemberDTO user);
LoginDTO login(MemberDTO loginUser);
boolean idCheck(String id);
LoginDTO findById(String id);
}
EmpDAOImpl
findById를 오버라이딩합니다.
@Override
public LoginDTO findById(String id) {
return sqlSession.selectOne("ktds.erp.emp.securityLogin",id);
}
SecurityLoginService
SecurityLoginService에서 EmpDAO를 주입받아 dao클래스의 findById메서드를 호출하여 리턴 받은 결과를 출력해 보도록 하겠습니다.
package ktds.erp.emp.authentication;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import ktds.erp.emp.EmpDAO;
import ktds.erp.emp.LoginDTO;
@Service("loginService")
public class SecurityLoginService implements UserDetailsService {
@Autowired
EmpDAO dao;
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println(username+"........loadUserByUsername");
LoginDTO user = dao.findById(username);
System.out.println("user===>"+user);
return null;
}
}
아이디로 사용자가 조회되는 것을 알 수 있습니다.
[step3 - DB에서 조회한 정보와 권한 정보를 이용하여 UserDetails객체 만들어서 리턴하기]
loadUserByUsername메서드 안에서 db조회를 처리하여 결과를 리턴 받았습니다. 이제 이 결과 정보를 가공하여 UserDetails객체로 만들어서 리턴하도록 하겠습니다.
지금까지는 loadUserByUsername메서드 안에서 null을 리턴했기 때문에 Exception이 발생했었습니다. UserDetails객체는 그냥 권한 정보가 포함된 DTO라 생각하시면 됩니다. 주로 UserDetails를 리턴하기보다는 UserDetails의 구현체 클래스인 User를 생성하여 리턴합니다.
loadUserByUsername메서드를 아래와 같이 수정합니다.
package ktds.erp.emp.authentication;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import ktds.erp.emp.EmpDAO;
import ktds.erp.emp.LoginDTO;
@Service("loginService")
public class SecurityLoginService implements UserDetailsService {
@Autowired
EmpDAO dao;
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println(username+"........loadUserByUsername");
LoginDTO user = dao.findById(username);
System.out.println("user===>"+user);
UserDetails loginUser = null;
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
authorities.add(new SimpleGrantedAuthority(user.getAuthority()));
loginUser = new User(user.getId(), user.getPass() ,authorities);
return loginUser;
}
}
User
User클래스는 UserDetails를 구현해놓은 클래스로 7개의 멤버 변수를 가지고 있습니다.
username,password,authorities,accountNonExpired,accountNonLocked,credentialsNonExpired,enabled입니다.
username : id
password : password
authorities : 권한들로 Collection타입 객체에 담고 있습니다.
enabled : 사용 가능한 사용자인지 유무를 true/false로 지정합니다.
accountNonExpired : 계정이 만료된 계정인지 유무를 true/false로 지정합니다.
accountNonLocked : 계정 잠금 여부를 나타내는 변수로 패스워드 연속 5번 입력 실패하면 잠금
으로 설정하면 됩니다. 물론 true/false로 지정합니다.
credentialsNonExpired : 크리덴셜 만료 여부를 true/false로 정의합니다.
실제 spring security 내부의 User클래스의 소스를 보면 아래와 같이 생성자가 존재합니다. 우리는 맨 위에 매개변수 3개 (username, password, authorities)를 갖고 있는 생성자를 호출하여 User객체를 만들어서 리턴합니다.
username은 아이디 정보 password는 db에서 조회한 패스워드 정보, authorities는 권한 리스트입니다.
....
public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}
public User(String username, String password, boolean enabled, boolean accountNonExpired,
boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
if (((username == null) || "".equals(username)) || (password == null)) {
throw new IllegalArgumentException("Cannot pass null or empty values to constructor");
}
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
}
.......
GrantedAuthority
GrantedAuthority는 권한을 모델링한 객체입니다. 실제 사용자가 권한을 여러 개 가질 수 있으므로 spring security내부에서는 Collection에 GrantedAuthority를 저장하여 관리합니다.
실제로는 GrantedAuthority를 구현하고 있는 SimpleGrantedAuthority를 사용합니다.
이렇게 AuthenticationProvider에게 User를 만들어서 리턴하면 Provider내부에서는 db에서 조회한 패스워드 정보와 사용자가 입력한 패스워드 정보가 일치하는지 확인하여 인증처리를 진행합니다.
실행하면 spring security내부에서 db에 저장된 데이터와 내가 입력한 아이디 패스워드를 비교하여 9401023jang/1234와 같은 "ROLE_USER"권한을 갖고 있는 사용자나 92115kim/1234와 같이 "ROLE_ADMIN" 권한을 갖고 있는 사용자로 테스트해도 모두 정상 로그인이 되고 권한에 따라 접속할 수 있는 리소스가 제한되는 것을 알 수 있습니다.
'보안 > Spring Security' 카테고리의 다른 글
password암호화 - ShaPasswordEncoder(회원가입 수정) (0) | 2019.09.13 |
---|---|
실제 DB에서 인증하기 - step04 권한 테이블을 생성하기 (0) | 2019.09.12 |
실제 DB에서 인증하기 - step02 spring security 내부의 처리흐름 이해하기 (0) | 2019.09.11 |
실제 DB에서 인증하기 - step01 테이블수정(simple) (0) | 2019.09.10 |
[실습] 권한별로 접근 페이지 제어하기 - 스프링EL로 변경하기 (0) | 2019.09.10 |