▸Spring Security

스프링 Security_로그인_암호화된 DB 패스워드로 인증 [6/9]

코데방 2020. 3. 29.
728x90
- 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]

 

 

붉은색 네모가 이번 포스팅에서 커스터마이징할 곳

 

 


 

 

 

이전글들에서 다룬 로그인 인증을 위한 로직은 아래와 같습니다.

 

1. 커스터마이징한 UserDetailsService 구현 클래스가 DB에서 계정 정보를 가져옴

2. 정보를 담은 UserDetails 객체를 디폴트 authentication-provider에게 전달해서 자동 인증 진행

 

<!-- DB 연동 설정 -->
<s:authentication-manager>
	<s:authentication-provider user-service-ref="userDetailsServiceCustom">
	</s:authentication-provider>
</s:authentication-manager>

 

DB에 저장된 데이터가 평문 패스워드일때는 디폴트 Provider 클래스에서 인증을 진행해줬지만, 이제 DB에서는 암호화된 패스워드를 건내주고 사용자는 평문 패스워드를 입력합니다. 기존처럼 equals()를 사용해 문자열을 비교하는 방식이 아닌 평문 패스워드를 암호화해서 DB의 데이터와 비교해주는 작업이 필요하다는 것이죠.

 

따라서 이전 커스터마이징 방법과 마찬가지로, Authentication-Provider 역할을 할 클래스를 하나 만들어서 바꿔치기(DI) 해주도록 하면 됩니다. 아래와 같이 고쳐주면 됩니다.

 

user-service-ref 는 디폴트 Provider에게 커스터마이징한 UserDetailsService를 주입해주기 위한 것이므로 삭제해도 됩니다. Provider를 우리가 직접 만들테니 해당 클래스 안에서 UserDetailsService Bean을 직접 주입받으면 되니까요.

<!-- DB 연동 설정 -->
<s:authentication-manager>
	<s:authentication-provider ref="userLoginAuthenticationProvider">
	</s:authentication-provider>
</s:authentication-manager>

 

 


 

 

 

1. AuthenticationProvider 인터페이스 구현 및 메소드 오버라이딩

 

Provider 역할을 하는 클래스는 해당 인터페이스를 구현하고 있습니다. 따라서 우리도 같이 구현해서 클래스 타입과 실행 메소드를 동일하게 맞춰줍니다.

 

package hs.spring.hsweb.service.user;

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Service;

@Service
public class UserLoginAuthenticationProvider implements AuthenticationProvider {

	@Override
	public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		
		
		return null;
	}

	@Override
	public boolean supports(Class<?> authentication) {
		
		
		return false;
	}

}

 

 


 

2. authenticate() 메소드 작성

 

실제 인증을 구현하는 로직입니다. 파라미터로 받은 Authentication에는 사용자가 입력한 ID/패스워드 정보를 담고 있습니다. 이 객체에서 필요한 정보를 뽑아 쓰고, 인증에 성공하면 새로운 Authentication 객체를 만들어 계정정보와 권한정보를 넣어 리턴하면 됩니다. 파라미터로 받은 객체를 재활용하면 될 것 같은데 setter 메소드가 없고 생성자로만 데이터를 넣을 수 있도록 되어 있어 어쩔 수 없이 새로 만들어줘야 합니다. 아마 인증 정보를 담은 객체를 생성한 이후로는 데이터를 변조할 수 없도록 하기 위해 setter가 없는 것 같습니다. 이 객체는 세션에 자동으로 저장되기 때문에 MVC 어느곳에서도 가져다 사용할 있습니다. 

 

그리고 인증에 실패할 경우 적절한 예외 객체를 생성해 던져주면 역시 이전에 커스터마이징 했던 Fail Handler에게 예외 객체를 던져줍니다. 따라서 Provider와 Fail Handler의 예외 상황을 동일하게 맞춰주면 됩니다.

 

사실 이제 인증에 관련된 대부분의 부분들을 커스터마이징 했기 때문에 지금 작성하고 있는 Provider를 제외하고, UserDetailsService 구현체라거나 데이터를 전달하는 UserDetails 구현체는 굳이 사용할 필요가 없습니다. 하지만 스프링 Security의 모든 동작 과정을 완전히 파악하고 있는 것이 아닌 이상, 기존 구조를 유지하면서 커스터마이징 하는 것이 가장 안정적일 것 같습니다. 굳이 새로운 구조를 만들 필요는 없지 않을까요.

 

 

 

아래는 전체 코드입니다. 패스워드는 인증 후에 필요없으므로 null 처리했습니다. getClass()로 확인해보면 Athentication 인터페이스의 실제 구현체를 확인할 수 있습니다. 같은 구현체를 만들면서 생성자로 값을 넣어주면 됩니다.

 

* new UsernamePasswordAuthenticationToken(생성자)

 

그리고 해당 객체를 만들 때는 String 타입이 아닌 UserDetails 구현체를 넣어주는 것이 좋습니다. Authentication 객체는 세션 Scope로 공유되기 때문에 사용자 인증 정보를 필요한 곳에서 꺼내 쓰는 용도로 편리하게 사용할 수 있는데, 그냥 ID를 String 타입으로 넣어버리면 이 기능을 제대로 사용할 수 없습니다. 정확한 내용은 아래 링크를 참조하시면 됩니다.

 

[Spring MVC/- 기본 문법] - 스프링 Security_로그인_Principal 객체 [7/8]

 

package hs.spring.hsweb.service.user;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import hs.spring.hsweb.mapper.vo.user.UserDetailsVO;

@Service
public class UserLoginAuthenticationProvider implements AuthenticationProvider {

	@Autowired
	// DB의 값을 가져다주는 커스터마이징 클래스
	UserDetailsService userDetailsServcie;

	// 패스워드 암호화 객체
	@Autowired
	BCryptPasswordEncoder pwEncoding;

	@Override
	// 인증 로직
	public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {

		/* 사용자가 입력한 정보 */
		String userId = authentication.getName();
		String userPw = (String) authentication.getCredentials();

		/* DB에서 가져온 정보 (커스터마이징 가능) */
		UserDetailsVO userDetails = (UserDetailsVO) userDetailsServcie
				.loadUserByUsername(userId);

		
		
		/* 인증 진행 */
		
		// DB에 정보가 없는 경우 예외 발생 (아이디/패스워드 잘못됐을 때와 동일한 것이 좋음)
		// ID 및 PW 체크해서 안맞을 경우 (matches를 이용한 암호화 체크를 해야함)
		if (userDetails == null || !userId.equals(userDetails.getUsername())
				|| !pwEncoding.matches(userPw, userDetails.getPassword())) {

			throw new BadCredentialsException(userId);

		// 계정 정보 맞으면 나머지 부가 메소드 체크 (이부분도 필요한 부분만 커스터마이징 하면 됨)
		// 잠긴 계정일 경우
		} else if (!userDetails.isAccountNonLocked()) {
			throw new LockedException(userId);

		// 비활성화된 계정일 경우
		} else if (!userDetails.isEnabled()) {
			throw new DisabledException(userId);

		// 만료된 계정일 경우
		} else if (!userDetails.isAccountNonExpired()) {
			throw new AccountExpiredException(userId);

		// 비밀번호가 만료된 경우
		} else if (!userDetails.isCredentialsNonExpired()) {
			throw new CredentialsExpiredException(userId);
		}

		// 다 썼으면 패스워드 정보는 지워줌 (객체를 계속 사용해야 하므로)
		userDetails.setPassword(null);

		/* 최종 리턴 시킬 새로만든 Authentication 객체 */
		Authentication newAuth = new UsernamePasswordAuthenticationToken(
				userDetails, null, userDetails.getAuthorities());

		return newAuth;
	}

	@Override
	// 위의 authenticate 메소드에서 반환한 객체가 유효한 타입이 맞는지 검사
	// null 값이거나 잘못된 타입을 반환했을 경우 인증 실패로 간주
	public boolean supports(Class<?> authentication) {

		// 스프링 Security가 요구하는 UsernamePasswordAuthenticationToken 타입이 맞는지 확인
		return authentication.equals(UsernamePasswordAuthenticationToken.class);
	}
}

 

 

 


 

 

 

3. supports() 메소드 작성

 

위에서 작성한 authenticate() 메소드의 리턴 객체 타입을 확인하는 메소드입니다. Athentication 인터페이스의 실제 구현체가 스프링 Security에서 규정한 타입이 맞는지 확인한 뒤, true를 반환해야 최종적으로 인증에 성공할 수 있습니다. 

 

	@Override
	// 위의 authenticate 메소드에서 반환한 객체가 유효한 타입이 맞는지 검사
	// null 값이거나 잘못된 타입을 반환했을 경우 인증 실패로 간주
	public boolean supports(Class<?> authentication) {

		// 스프링 Security가 요구하는 UsernamePasswordAuthenticationToken 타입이 맞는지 확인
		return authentication.equals(UsernamePasswordAuthenticationToken.class);
	}

 

 


 

 

 

이것으로 패스워드 암호화와, 이를 통한 인증 로직 작성도 완료되었습니다. 다음 포스팅은 Principal 객체에 대한 내용과 뷰에서 사용하는 Security 태그 라이브러리에 대한 것입니다. 

728x90

댓글

💲 추천 글