본문 바로가기
  • A space that records me :)
기술/Spring Security

[Spring Security] 로그인 기능 구현

by yjkim_97 2021. 10. 24.

2021.10.23 - [IT 기술/권한 인증&인가] - [Spring Security] Authentication 라이브러리 구현

 

[Spring Security] Authentication 라이브러리 구현

프로젝트마다 로그인/회원가입/권한인증 등 기능을 매번 구현하기 힘들다. 그래서 직접 프로젝트마다 적용시킬 Authentication 라이브러리를 개발하였다. Authentication 라이브러리는 인증 및 권한 관

yjkim97.tistory.com


6. 로그인 기능 구현

spring-security의 DaoAuthenticationProvider를 커스텀하였다. (CustomAuthenticationProvider)

 

  • CustomAuthenticationProvider에서는 DB에서 사용자 정보를 가져오기 위한 CustomUserDetailsService를 사용한다.
  • 로그인 인증(비밀번호 체크, 기타 계정 상태)을 하기 위한 PreAccountStatusUserDetailsChecker, PostAccountStatusUserDetailsChecker 를 사용한다.

 

6-1. CustomUserDetailsService.java

CustomUserDetailsService는 spring-security의 UserDetailsService 인터페이스를 따른다.

spring-security의 '로그인 인증'관련 필터는 세션-쿠키 기반 폼로그인 요청이 들어오면 UserDetailsService의 loadUserByUsername 메소드를 통해 DB에서 정보를 읽어와 UserDetails를 생성한다.

/**
 * HomeService
 * 
 * UserDetailsService인터페이스를 구현
 * 
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService 
{

	@Value("${security.login.fail.imsi-lock.lock-minute}")
	private Integer imsiLockLockMinute;
	
	@Value("${security.credentials-expired.month}")
	private Integer credentialsExpiredMonth;
	
	@Value("${security.credentials-expired.date}")
	private Integer credentialsExpiredDate;
	
	@Value("${security.credentials-expired.use}")
	private boolean credentialsExpiredUse;
	
	private final AuthService authService;
	
	@Autowired
	private AuthUserService authUserService;
	
	/*
	 * 로그인
	 */
	@Override
	public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException
	{

		User user = null;
		ArrayList<GrantedAuthority> authlist = new ArrayList<GrantedAuthority>();
		
		try 
		{
			// TB_USER 정보
			user = authUserService.getUserByLoginId(userId);
			
			// 권한그룹
			user.setUsrgrpList(authService.getUserGroupByUserId(user.getUserSid()));
			
			// 메뉴
			user.setMenuList(authService.getMenuListByUserSid(user.getUserSid()));
			
			// GrantedAuthority stting ======================================
			for (Usrgrp ugv : user.getUsrgrpList()) {
				
				AuthGroup authGroup = new AuthGroup(ugv.getUsrgrpSid(), ugv.getUsrgrpNm());
				// 역할 리스트
				List<AuthRole> hasRoleList = new ArrayList<AuthRole>();
				if(!ObjectUtils.isEmpty(ugv.getRoleList()))
				{
					for (Role rolw : ugv.getRoleList()) {
						hasRoleList.add(new AuthRole(rolw.getRoleId(), rolw.getRoleId()));
					}
				}
				authGroup.setAuthRoleList(hasRoleList);
				this.setChildAuthGroup(authGroup, ugv.getChildUsrgrpList());
				
				authlist.add(new UserGrantedAuthority(authGroup));
			}
		}
		catch (UsernameNotFoundException e) {
			throw e;
		}
		
		// 계정 상태 여부 ===============================================
		boolean enabled = true; // 활성화
		boolean credentialsNonExpired = true; // 비번만료x
		boolean accountNonLocked = true; // 계정잠김x
		
		ACNT_STTS_CD acntStts = user.getAcntSttsCd(); // 계정상태
		LOGIN_PRVNT_STTS_CD loginPrvntStts = user.getLoginPrvntSttsCd(); // 로그인 방지 상태
		SVC_STTS_CD svcStts = user.getSvcSttsCd(); // 서비스 상태
		
		if(acntStts == ACNT_STTS_CD.Active && !ObjectUtils.isEmpty(loginPrvntStts))
		{
			switch (loginPrvntStts) {
			case AccountDisabledByBalckList:
				enabled = false;
				break;
			case LockedByPassword:
				accountNonLocked = !this.checkUserLock(user.getAcntLockDt());
				break;
			default:
				break;
			}
		}
		
		// 비번 만료
		if(credentialsExpiredUse)
		{
			credentialsNonExpired = this.checkCredentialsNonExpried(user.getPwdUpdDt());
		}
		
		// UserDetails 세팅 =============================================
		// id, pwd, 활성화, 계정만료x, 비밀번호만료x, 계정잠김x
		CustomUserDetails userDetails = new CustomUserDetails(user.getLoginId(), user.getPwd(),authlist
				, enabled, true, credentialsNonExpired, accountNonLocked);
		
		userDetails.setUser(user);
		if(!ObjectUtils.isEmpty(acntStts))
		{
			userDetails.setAcntStts(acntStts);
		}
		if(!ObjectUtils.isEmpty(loginPrvntStts))
		{
			userDetails.setLoginPrvntStts(loginPrvntStts);
		}
		if(!ObjectUtils.isEmpty(svcStts))
		{
			userDetails.setSvcStts(svcStts);
		}
		
		return userDetails;
		
	}

	public void setChildAuthGroup(AuthGroup authGroup, List<Usrgrp> childUserGroupList)
	{
		List<AuthGroup> cAuthGroupList = new ArrayList<AuthGroup>();
		if(childUserGroupList != null)
		{
			for (Usrgrp ugv : childUserGroupList) {
				AuthGroup cAuthGroup = new AuthGroup(ugv.getUsrgrpSid(), ugv.getUsrgrpNm());
				
				// 역할 리스트
				List<AuthRole> hasRoleList = new ArrayList<AuthRole>();
				if(!ObjectUtils.isEmpty(ugv.getRoleList()))
				{
					for (Role rolw : ugv.getRoleList()) {
						hasRoleList.add(new AuthRole(rolw.getRoleId(), rolw.getRoleId()));
					}
				}
				cAuthGroup.setAuthRoleList(hasRoleList);
				
				this.setChildAuthGroup(cAuthGroup, ugv.getChildUsrgrpList());
				
				cAuthGroupList.add(cAuthGroup);
			}
		}
		
		if(cAuthGroupList.size() > 0)
		{
			authGroup.setChlidAuthGroupList(cAuthGroupList);
		}
	}
	
	public boolean checkUserLock(Date lockDt)
	{
		// true : 잠김
		if(lockDt == null || ObjectUtils.isEmpty(imsiLockLockMinute))
		{
			return true;
		}
		
		Date unLockedDt = DateUtil.minuteAdd(lockDt, imsiLockLockMinute);
		return unLockedDt.after(new Date());
	}
	
	public boolean checkCredentialsNonExpried(Date pwdUpdDt)
	{
		// expiredDt : 비밀번호 만료일
		// tre : 비밀번호 만료아님.
		Date expiredDt = DateUtil.dayAdd(
				DateUtil.monthAdd(pwdUpdDt, (ObjectUtils.isEmpty(credentialsExpiredMonth) ? 0 : credentialsExpiredMonth))
				, (ObjectUtils.isEmpty(credentialsExpiredDate) ? 0 : credentialsExpiredDate));
		return expiredDt.after(new Date());
	}

}
  1. 로그인 시도한 id로 DB에서 사용자 정보 select
    • DB에 사용자 정보가 있는지 없는지 판단.
  2. DB에서 사용자의 권한그룹과 역할을 가져와 UserGrantedAuthority 객체를 생성
  3. DB에서 가져온 사용자 정보에서 계정 상태 여부를 판단.
    • 비밀번호 만료 여부, 비밀번호 실패 여부 등 을 파악 해 CustomUserDetails에 세팅한다.

 

6-2. PreAccountStatusUserDetailsChecker.java

spring-security UserDetailsChecker 인터페이스를 따른다.

CustomUserDetailsService를 통해 DB에서 사용자 정보를 가져온 후, 비밀번호를 체크하기 전 AuthenticationChecker이다.

로그인 가능한 상태인지 체크한다.

@Slf4j
public class PreAccountStatusUserDetailsChecker implements UserDetailsChecker{

	/**
	 * the list below is checked in order. in class User and CustomUserDetails.
	 * {@link CustomUserDetails}.acntStts > ACNT_STTS_CD.Waiting, Rejected
	 * {@link CustomUserDetails}.acntStts > ACNT_STTS_CD.Inactive
	 * {@link User}.accountNonLocked > {@link CustomUserDetails}.loginPrvntStts > LOGIN_PRVNT_STTS_CD.LockedByPassword
	 * {@link User}.enabled > {@link CustomUserDetails}.loginPrvntStts > LOGIN_PRVNT_STTS_CD.AccountDisabledByBlackList
	 * @param toCheck
	 */
	@Override
	public void check(UserDetails toCheck) {
		CustomUserDetails userDetails = (CustomUserDetails) toCheck;
		Integer userSid = userDetails.getUser().getUserSid();
		
		// 탈퇴
		if(BooleanUtils.toBoolean(userDetails.getUser().getWhtdYn()))
		{
			throw new SurfinnUsernameNotFoundException("userSid : " + userSid);
		}
		
		// 계정 상태
		switch (userDetails.getAcntStts()) {
		
			// 승인 대기, 승인 거절
			case Waiting:
			case Rejected:
				log.debug("Faild to authenticate since account authorization was not completed.");
				throw new SurfinnNotApproveException("userSid : " + userSid + ", Faild to authenticate since account authorization was not completed.");
				
			// 휴면
			case Inactive:
				log.debug("Faild to authenticate since account is inactive.");
				throw new SurfinnInactiveException("userSid : " + userSid + ", Faild to authenticate since account is inactive.");
			
			// 정상
			case Active:
				
				// 락
				if(!userDetails.isAccountNonLocked())
				{
					// 비밀번호 연속 실패
					if(LOGIN_PRVNT_STTS_CD.LockedByPassword == userDetails.getLoginPrvntStts())
					{
						log.debug("Faild to authenticate since account is locked. (password is incorrect.)");
						throw new SurfinnLockedByPasswordException("Faild to authenticate since account is locked. (password is incorrect.)",null);
					}
					log.debug("Faild to authenticate since account is locked.");
					throw new SurfinnLockedException("Faild to authenticate since account is locked.",null);
				}
				
				// 비활성화
				if(!userDetails.isEnabled())
				{
					// 블랙리스트
					if(LOGIN_PRVNT_STTS_CD.AccountDisabledByBalckList == userDetails.getLoginPrvntStts())
					{
						log.debug("Faild to authenticate since account is disabled. (black list account.)");
						throw new SurfinnAccountDisabledBlackListException("Faild to authenticate since account is disabled. (black list account.)",null);
					}
					log.debug("Faild to authenticate since account is disabled.");
					throw new SurfinnDisabledException("Faild to authenticate since account is disabled.",null);
				}
				break;
				
			// 알수없음
			default:
				throw new SurfinnAuthenticationException(userDetails.getAcntStts() + " value is not included in 'ACNT_STTS_CD'.");
		}

	}
}

 

6-3. PostAccountStatusUserDetailsChecker.java

spring-security의 UserDetailsChecker 인터페이스를 따르며, 비밀번호 체크 이후 권한체크하는 Checker이다.

이곳에서는 커스텀만 하두었고 따로 기능을 추가하지 않았다.

@Slf4j
public class PostAccountStatusUserDetailsChecker implements UserDetailsChecker{

	/**
	 * the list below is checked in order. in class User and CustomUserDetails.
	 * {@link User}.credentialsNonExpired
	 * @param toCheck
	 */
	@Override
	public void check(UserDetails toCheck) {
		
	}

}

 

6-4. CustomAuthenticationProvider.java

위 CustomUserDetailsService, PerAccountStatusUserDetailsChecker, PostAccountStatusUserDtailsChecker를 가지고 시큐리티 필터체인으로 인해 실행될 세션-쿠키 기반 폼로그인 인증 Provider를 커스텀한 것이다.

@Slf4j
public class CustomAuthenticationProvider extends DaoAuthenticationProvider
{
	
	@Override
	protected void additionalAuthenticationChecks(UserDetails userDetails,
			UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
		if (authentication.getCredentials() == null) {
			log.debug("Failed to check credentials. this value is null.");
			throw new SurfinnBadCredentialsException("userSid : " + ((CustomUserDetails)userDetails).getUser().getUserSid());
		}
		
		String presentedPassword = authentication.getCredentials().toString();
		if (!super.getPasswordEncoder().matches(presentedPassword, userDetails.getPassword())) {
			log.debug("Failed to check credentials. not matches.");
			throw new SurfinnBadCredentialsException("userSid : " + ((CustomUserDetails)userDetails).getUser().getUserSid());
		}
	}
	
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException 
	{
		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports"));
		String username = authentication.getName();
		UserDetails user = super.getUserCache().getUserFromCache(username);
		if (user == null) {
			try {
				user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
			}
			catch (UsernameNotFoundException ex) {
				log.debug("Failed to find user '" + username + "'");
				throw new SurfinnUsernameNotFoundException("Failed to find user '" + username + "'",ex);
			}
			Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
		}
		try {
			
			// 승인, 휴면, 계정만료, 락, 비활성화
			super.getPreAuthenticationChecks().check(user);
			
			// 비밀번호 체크
			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
			
		}
		catch (AuthenticationException ex) {
			throw ex;
		}
		
		// 비밀번호 만료
		super.getPostAuthenticationChecks().check(user);

		return createSuccessAuthentication(user, authentication, user);
	}
	
}
  1. CustomUserDetailsService를 통해 DB에서 사용자 정보를 가져온다.
  2. PreAccountStatusUserDetailsChecker를 통해 전처리 권한체크를 한다.
  3. 로그인 시도한 비밀번호와 DB에 저장된 비밀번호가 일치하는 지 체크한다.
  4. PostAccountStatusUserDetailsChecker를 통해 후처리 권한체크를 한다.

 

7. 로그인 인증 핸들러

7-1. LoginFailureHandler.java

로그인 인증 실패 핸들러이다.

spring-security의 AuthenticationFailureHandler 인터페이스를 따른다.

이곳에서 CustomUserDetails의 계정상태에 따라 로그인 실패 에러 메시지를 만든다.

@Slf4j
public class LoginFailureHandler implements AuthenticationFailureHandler {

	@Value("${security.login.fail.redirect-uri.default}")
	private String loginFailRedirectDefault;

	@Value("${security.login.fail.imsi-lock.max-count}")
	private Integer imsiLockMaxCnt;

	@Value("${security.login.fail.imsi-lock.lock-minute}")
	private long imsiLockLockMinute;

	@Value("${security.login.fail.imsi-lock.use}")
	private boolean imsiLockUse;
	
	public LoginFailureHandler() {
	}

	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException exception) throws IOException, ServletException {
		String fowardPath = "/loginFailException";
		
		try {
		    String key = "unknown";
		    
			if(exception instanceof SessionAuthenticationException)
			{
				key = AuthConstants.KEY_SESSION_AUTHENTICATION;
			}
			else if(exception instanceof SurfinnAuthenticationException)
			{
			    key = ((SurfinnAuthenticationException) exception).getKey();
			}
			
            request.setAttribute("key", key);
				
		} catch (Exception e) {
			log.error(e.getMessage(), e);
		}
		
		request.getRequestDispatcher(fowardPath).forward(request, response);
	}
}

AuthErrorController.java

LoginFailueHandler에서 포워드로 /loginFailException을 호출한다.

@Slf4j
@Controller
@RequiredArgsConstructor
public class AuthErrorController {

	@Value("${security.login.fail.redirect-uri.default}")
	private String loginFailRedirect;
	
	@Value("${security.login.param.id}")
	private String loginParamId;

	private final AuthErrorService authErrorService;
	private final RedirectExceptionProperties redirectExceptionProperties;

	@PostMapping("/loginFailException")
	private String sendRedirectwithErrorMessage(HttpServletRequest request, HttpServletResponse response,
			RedirectAttributes redirectAuttribute) {
		
		String key = (String) request.getAttribute("key");
		String loginId = (String) request.getParameter(loginParamId);
		String redirectUri = loginFailRedirect;

		redirectAuttribute.addFlashAttribute("errorMessage",
				authErrorService.getErrorMessageByAuthenticationException(key, loginId));

		if (!ObjectUtils.isEmpty(redirectExceptionProperties.getUri(key))) {
			redirectUri = redirectExceptionProperties.getUri(key);
		}
		
		log.debug("AuthErrorController :: key ("+ key+"), redirect ("+redirectUri+")");
		
		return "redirect:" + redirectUri;
	}
}

AuthErrorService.java

실제 에러메시지를 만드는 서비스이다. 메시지는 messageProperty로 관리한다.

@Slf4j
@Service
@RequiredArgsConstructor
public class AuthErrorService {

	@Value("${security.login.fail.imsi-lock.max-count}")
	private Integer insiLockMaxCount;

	@Value("${security.login.fail.imsi-lock.lock-minute}")
	private Integer imsiLockLockMinute;

	private final AuthUserService authUserService;
	private final MessageProvider messageProvider;

	public String getErrorMessageByAuthenticationException(String key, String loginId) {

		switch (key) {
		
			/**
			 * SurfinnApproveException '승인되지 않은 계정입니다. 관리자에게 문의 바랍니다.'
			 */
			case AuthConstants.KEY_NOT_APPROVE:
				return messageProvider.getMessage(AuthConstants.MSG_USER_NO_APPROVAL,
						messageProvider.getMessage(AuthConstants.MSG_CONTACT_ADMIN));
				
			/**
			 * SurfinnInactiveException '휴면계정입니다.'
			 */
			case AuthConstants.KEY_INACTIVE:
				return messageProvider.getMessage(AuthConstants.MSG_LOGIN_FAIL_INACTIVE);
				
			/**
			 * SurfinnUsernameNotFoundException '아이디 또는 비밀번호가 올바르지 않습니다.'
			 */
			case AuthConstants.KEY_USER_NOT_FOUND:
				return messageProvider.getMessage(AuthConstants.MSG_LOGIN_FAIL_DEFAULT);
				
			/**
			 * SurfinnBadCredentialsException 
			 * '아이디 또는 비밀번호가 올바르지 않습니다. ({0}회 이상 실패시 계정이 {1}동안 잠깁니다.)'
			 */
			case AuthConstants.KEY_BAD_CREDENTIALS:
				return getErrorMassageBadCredential(key, loginId);
				
			/**
			 * SurfinnLockedByPasswordException
			 * '{0}회 이상 로그인 실패로 계정이 잠겼습니다. {1} 후 재시도 바랍니다.'
			 */
			case AuthConstants.KEY_LOCKED_BY_PASSWORD:
				return getErrorMassageLockedByPassword(loginId);
				
			/**
			 * SurfinnLockedException '계정이 잠겼습니다. 관리자에게 문의 바랍니다.'
			 */
			case AuthConstants.KEY_LOCKED:
				return messageProvider.getMessage(AuthConstants.MSG_LOGIN_FAIL_ACCOUNT_LOCK,
						messageProvider.getMessage(AuthConstants.MSG_CONTACT_ADMIN));
			
			/**
			 * SurfinnAccountDisabledBlackListException, SurfinnDisabledException
			 * '비활성화된 계정입니다. 관리자에게 문의 바랍니다.'
			 */
			case AuthConstants.KEY_DISABLED_BLACKLIST:
			case AuthConstants.KEY_DISABLED:
				return getErrorMassageDisabled(loginId);
			
			/**
			 * SessionAuthenticationException
			 * '이미 다른 세션에서 로그인 중 입니다.'
			 */
			case AuthConstants.KEY_SESSION_AUTHENTICATION:
				return messageProvider.getMessage(AuthConstants.MSG_LOGIN_FAIL_SESSION);
			/**
			 *  '로그인에 실패하였습니다. 관리자에게 문의 바랍니다.'
			 */
			default:
				return messageProvider.getMessage(AuthConstants.MSG_LOGIN_FAIL_ETC,
						messageProvider.getMessage(AuthConstants.MSG_CONTACT_ADMIN));
		}
		
	}

	// 비밀번호 연속 실패 잠김 메시지
	public String getErrorMassageLockedByPassword(String loginId) {
		
		User user = authUserService.getUserByLoginId(loginId);
		
		if (!ObjectUtils.isEmpty(user)) {
			if (!ObjectUtils.isEmpty(user.getAcntLockDt())) {
				
				if(ObjectUtils.isEmpty(imsiLockLockMinute))
				{
					imsiLockLockMinute = 0;
				}
				
				Date unLockDt = DateUtil.minuteAdd(user.getAcntLockDt(), imsiLockLockMinute);
				double diffMin = (unLockDt.getTime() - user.getAcntLockDt().getTime()) / (double) DateUtil.MINUTE;
				int min = (int) NumberUtil.ceil(diffMin);

				String wait = (min < 1 ? messageProvider.getMessage(AuthConstants.MSG_WAIT_A_MINUTE)
						: min + messageProvider.getMessage(AuthConstants.MSG_MINUTE));

				return messageProvider.getMessage(AuthConstants.MSG_LOGIN_FAIL_ACCOUNT_LOCK_BY_PWD,
						String.valueOf(insiLockMaxCount), wait);
			}
		}
		
		return messageProvider.getMessage(AuthConstants.MSG_LOGIN_FAIL_ACCOUNT_LOCK_BY_PWD,
				String.valueOf(insiLockMaxCount), messageProvider.getMessage(AuthConstants.MSG_WAIT_A_MINUTE));
	}

	// 비밀번호 실패 메시지
	public String getErrorMassageBadCredential(String key, String loginId) {
		
		StringBuffer sb = new StringBuffer();
		
		User user = authUserService.getUserByLoginId(loginId);
		
		try {
			
			sb.append(messageProvider.getMessage(AuthConstants.MSG_LOGIN_FAIL_DEFAULT));

			// 로그인 실패 카운트 증가
			User result = authUserService.addLoginFailCount(user.getUserSid());
			
			if (!ObjectUtils.isEmpty(result)) {
				Integer cnt = insiLockMaxCount - result.getLoginFailCnt();

				sb.append(String.format(" (%s)", (result.getLoginFailCnt() < insiLockMaxCount
						? messageProvider.getMessage(AuthConstants.MSG_LOGIN_FAIL_COUNT_INFO, String.valueOf(cnt),
								String.valueOf(imsiLockLockMinute)
										+ messageProvider.getMessage(AuthConstants.MSG_MINUTE))
						: messageProvider.getMessage(AuthConstants.MSG_LOGIN_FAIL_ACCOUNT_LOCK_BY_PWD,
								String.valueOf(insiLockMaxCount), String.valueOf(imsiLockLockMinute)
										+ messageProvider.getMessage(AuthConstants.MSG_MINUTE)))));
			}

		} catch (SurfinnAuthenticationException e) {
			
		    if(!e.getKey().equals(key))
		    {
		        sb.setLength(0);
		        sb.append(this.getErrorMessageByAuthenticationException(e.getKey(), loginId));
		    }
		    
			log.error(e.getMessage(), e);

		} catch (Exception e) {
			log.error(e.getMessage(), e);
		}
		
		return sb.toString();
	}

	// 비활성화 메시지
	public String getErrorMassageDisabled(String loginId) {
		User user = authUserService.getUserByLoginId(loginId);
		
		String errorMessage = messageProvider.getMessage(AuthConstants.MSG_LOGIN_FAIL_DISABLED,
				messageProvider.getMessage(AuthConstants.MSG_CONTACT_ADMIN),
				(ObjectUtils.isEmpty(user.getAcntDsabldDt()) ? messageProvider.getMessage(AuthConstants.MSG_UNKOWN) : DateUtil.formatDate(user.getAcntDsabldDt(), AuthConstants.DATE_FM)));
		return errorMessage;
	}
}

 

7-2. LoginSuccessHandler.java

로그인 인증 성공 핸들러이다.

spring-security AuthenticationSuccessHandler 인터페이스를 따른다.

이곳에서 로그인 성공처리를한다. (계정상태 리셋, 에러세션 제거 등)

@Slf4j
public class LoginSuccessHandler implements AuthenticationSuccessHandler{

	@Value("${security.credentials-expired.redirect}")
	private String credentialsExpiredRedirect;
	
	@Autowired
	private AuthUserService authUserService;
	
	@Autowired
	private LoginFailureHandler loginFailureHandler;

	private String redirectUri;
	
	public LoginSuccessHandler(String redirectUrl) 
	{
		this.redirectUri = redirectUrl;
	}
	
	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException, ServletException {
		
		CustomUserDetails userDetails = (CustomUserDetails)authentication.getPrincipal();
		try 
		{			
			// 에러세션 지우기
			HttpSession session = request.getSession(false);
	        if(!ObjectUtils.isEmpty(session))
	        {
	        	session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
			}
			
	        // 계정 잠금 해제
	        authUserService.updateUserLoginSuccess(userDetails.getUser().getUserSid());
	        
	        // 비밀번호 만료시 리다이렉트
	        String uri = redirectUri;
	        if(!userDetails.isCredentialsNonExpired() && !ObjectUtils.isEmpty(credentialsExpiredRedirect))
	        {
        		uri = credentialsExpiredRedirect;
	        }
			response.sendRedirect(uri);
		}
		catch (Exception e) {
			log.error(e.getMessage(),e);
			loginFailureHandler.onAuthenticationFailure(request, response, new SurfinnAuthenticationException(null, e));
		}
		
	}
}