반응형

예전 사이트에 적용한 security를 사용한 중복 로그인 방지를 작성해 보려고 해요

 

요구사항

  • 관리자 계정은 로그인이 한 명만 가능하고 중복 로그인하면 이전 사용자는 강제 로그아웃
  • 일반 사용자는 설정에 맞게 중복 로그인이 가능 로그인 사용자가 꽉 차면 로그인 불가

그래서 org.springframework.security.core.session.SessionRegistry를 이용하여 구현을 했어요.

 

protected void configure(HttpSecurity http) throws Exception {
	http.authorizeRequests().antMatchers("/ex/**").permitAll().anyRequest().authenticated();
	http.authorizeRequests().anyRequest().authenticated().and().formLogin().successHandler(
	        defaultLoginSuccessHandler).failureHandler(defaultLoginFailureHandler).loginPage(loginPage).failureUrl(
	                loginPage + "?error").permitAll().usernameParameter("id").passwordParameter("pwd");
	http.logout().logoutUrl("/logout").logoutSuccessUrl(loginPage).addLogoutHandler(defaultLogoutHandler)
	        .invalidateHttpSession(true).permitAll();
	// X-Frame-Options: DENY 설정하지 않음(다운로드컴포넌트를 위해)
	http.headers().frameOptions().disable();
	http.addFilterBefore(duplicatLoginFilter(), UsernamePasswordAuthenticationFilter.class);
	http.sessionManagement().maximumSessions(500).maxSessionsPreventsLogin(true).sessionRegistry(sessionRegistry)
	        .expiredUrl("/");
	http.csrf().disable();
}

protected DuplicateLoginFilter duplicatLoginFilter() throws Exception {
	
    return new DuplicateLoginFilter(sessionRegistry, loginSecurityService, loginPage, siteMapper);
    
}

@Bean
public PasswordEncoder passwordEncoder() {
	PasswordEncoder encoder = new CustomPasswordEncoder();
    return encoder;
    
}

 

세션은 총 500개로 잡았어요. 패스워드는 SHA256를 사용했어요.

 

public class DuplicateLoginFilter extends GenericFilterBean {

	private SessionRegistry sessionRegistry;
	private UserDetailsService userDetailsService;
	private String loginProcessUrl;
	private SiteMapper siteMapper;
	public DuplicateLoginFilter(final SessionRegistry sessionRegistry, final UserDetailsService userDetailsService,
	    final String loginProcessUrl, final SiteMapper siteMapper) {
		this.sessionRegistry = sessionRegistry;
		this.userDetailsService = userDetailsService;
		this.loginProcessUrl = loginProcessUrl;
		this.siteMapper = siteMapper;
	}
	@Override
	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
	    throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
		// post 방식에 로그인 요청이 아닌 경우 작업종료
		if (!new AntPathRequestMatcher(loginProcessUrl, "POST").matches(request)) {
			chain.doFilter(req, res);
			return;
		}
		log.info("login check filter has been requested.");
		boolean error = false;
		log.debug("로그인 세션수 -> {}", String.valueOf(sessionRegistry.getAllPrincipals().size()));
		try {
			String username = request.getParameter("id");
			String password = request.getParameter("pwd");
			// 패스워드 암호화
			byte pbCipher[] = new byte[32];
			SHA256Utils.SHA256_Encrpyt(password.getBytes("UTF-8"), password.length(), pbCipher);
			StringBuffer encPassword = new StringBuffer();
			for (int i = 0; i < 32; i++) {
				String hex = Integer.toHexString(0xff & pbCipher[i]);
				if (hex.length() == 1) {
					encPassword.append('0');
				}
				encPassword.append(hex);
			}
			UserDetails details = userDetailsService.loadUserByUsername(username);
			LoginUser principal = (LoginUser) details;
			// 사용자 정보 일치여부
			if (details.getPassword().equals(encPassword.toString())) {
				// 동일한 로그인 세션 체크
				log.debug(principal.toString());
				//public 공용, private 개인
				String idType = principal.getIdType();
				//공용일경우 로그인 제한 count
				int connCnt = principal.getConnCnt();
				log.debug("connCnt==" + connCnt);
				List<SessionInformation> sessionInfo = sessionRegistry.getAllSessions(principal, false);
			
				if ("private".equals(idType)) {
					//개인용 일경우 기존 정보가 있을경우 만료 후 로그인
					Iterator<SessionInformation> session = sessionInfo.iterator();
					while (session.hasNext()) {
						SessionInformation sessionInformation = session.next();
						LoginUser user = (LoginUser) sessionInformation.getPrincipal();
						if (user.getUsername().equals(username)) {
							sessionInformation.expireNow();
							break;
						}
					}
				} else {
					Iterator<SessionInformation> session = sessionInfo.iterator();
					int loginCnt = 1;
					while (session.hasNext()) {
						SessionInformation sessionInformation = session.next();
						LoginUser user = (LoginUser) sessionInformation.getPrincipal();
						if (user.getUsername().equals(username)) {
							if (connCnt <= loginCnt) {
								error = true;
								break;
							}
							loginCnt++;
						}
					}
				}
			} else {
				//사용자 정보 불일치
				MessageAlertUtils.alertRedirectMsg(response, "아이디 또는 비밀번호를 다시 확인하세요.", "/auth/login?error");
				return;
			}
		} catch (UsernameNotFoundException e) {
			MessageAlertUtils.alertRedirectMsg(response, "아이디 또는 비밀번호를 다시 확인하세요.", "/auth/login?error");
			//error = false;
		}
		log.debug("error==" + error);
		if (!error) {
			chain.doFilter(req, res);
			return;
		}
		MessageAlertUtils.alertRedirectMsg(response, "접속 가능한 인원수가 초과 되었습니다.", "/auth/login?error");
		return;
	}
}
728x90

위는 중복체크 filter 전체 소스예요

실제 중복 처리하는 부분은 아래 소스예요

Iterator<SessionInformation> session = sessionInfo.iterator();
while (session.hasNext()) {
	SessionInformation sessionInformation = session.next();
	LoginUser user = (LoginUser) sessionInformation.getPrincipal();
		
	//현재 로그인 된 사용자가 있으면 기존사용자 세션만료
	if (user.getUsername().equals(username)) {
		sessionInformation.expireNow();
		break;
	}
}

//해당계정 설정된 유저수가 넘으면 로그인 차단
Iterator<SessionInformation> session = sessionInfo.iterator();
int loginCnt = 1;
while (session.hasNext()) {
	SessionInformation sessionInformation = session.next();
	LoginUser user = (LoginUser) sessionInformation.getPrincipal();
	if (user.getUsername().equals(username)) {
		if (connCnt <= loginCnt) {
			error = true;
			break;
		}
		loginCnt++;
	}
}

각 요구사항에 맞게 처리한 로직이에요

 

직접 적용한 소스 기반으로 작성해봤어요

반응형

'개발' 카테고리의 다른 글

이클립스에서 한글이 깨질경우  (3) 2020.12.20
톰켓 Symbolic link(Windows)  (0) 2020.10.27
티스토리 소스코드 넣기  (0) 2020.08.13

+ Recent posts