반응형
예전 사이트에 적용한 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 |