diff --git a/src/main/java/io/company/localhost/common/security/config/SecurityConfig.java b/src/main/java/io/company/localhost/common/security/config/SecurityConfig.java index e8e6819..a0af78e 100644 --- a/src/main/java/io/company/localhost/common/security/config/SecurityConfig.java +++ b/src/main/java/io/company/localhost/common/security/config/SecurityConfig.java @@ -21,10 +21,14 @@ import io.company.localhost.common.security.handler.MemberAuthSuccessHandler; import io.company.localhost.common.security.handler.RestAccessDeniedHandler; import io.company.localhost.common.security.handler.RestAuthenticationEntryPointHandler; import io.company.localhost.common.security.service.CustomRememberMeServices; +import io.company.localhost.common.security.service.TokenService; import io.company.localhost.common.security.service.MemberPrincipalDetailService; import io.company.localhost.common.security.session.AuthenticationSessionControlStrategy; import io.company.localhost.common.security.session.CustomSessionRegistryImpl; +import io.company.localhost.service.NetmemberService; import lombok.RequiredArgsConstructor; + +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; @@ -52,6 +56,7 @@ public class SecurityConfig { private final MemberPrincipalDetailService userDetailsService; private final MemberAuthSuccessHandler successHandler; private final MemberAuthFailureHandler failureHandler; + private final NetmemberService netmemberService; private final AuthorizationManager authorizationManager; // 세션 관련 상수 설정 @@ -63,6 +68,9 @@ public class SecurityConfig { // API 경로 관련 상수 설정 final String SECURITY_BASE_URL = "/api/user"; final String LOGIN_URL = SECURITY_BASE_URL + "/login"; + + final String LOGIN_KEY = "loginSecretKey"; + // 보안 필터 체인 설정 @Bean @@ -115,9 +123,15 @@ public class SecurityConfig { @Bean public RememberMeServices rememberMeServices(){ - return new CustomRememberMeServices(REMEMBER_KEY , userDetailsService); + return new CustomRememberMeServices(tokenService(), userDetailsService); + } + + @Bean + public TokenService tokenService() { + return new TokenService(REMEMBER_KEY, LOGIN_KEY); } + // 세션 관리 protected ConcurrentSessionControlAuthenticationStrategy sessionControlStrategy() { AuthenticationSessionControlStrategy sessionControlStrategy = new AuthenticationSessionControlStrategy(sessionRegistry()); diff --git a/src/main/java/io/company/localhost/common/security/handler/MemberAuthSuccessHandler.java b/src/main/java/io/company/localhost/common/security/handler/MemberAuthSuccessHandler.java index e125983..d79fa78 100644 --- a/src/main/java/io/company/localhost/common/security/handler/MemberAuthSuccessHandler.java +++ b/src/main/java/io/company/localhost/common/security/handler/MemberAuthSuccessHandler.java @@ -16,10 +16,15 @@ package io.company.localhost.common.security.handler; import com.fasterxml.jackson.databind.ObjectMapper; import io.company.localhost.common.dto.ApiResponse; +import io.company.localhost.common.security.service.TokenService; +import io.company.localhost.service.NetmemberService; +import io.company.localhost.vo.MemberVo; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import lombok.extern.slf4j.Slf4j; + +import org.springframework.context.annotation.Lazy; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.core.Authentication; @@ -31,25 +36,48 @@ import java.io.IOException; @Slf4j @Component("successHandler") -public class MemberAuthSuccessHandler implements AuthenticationSuccessHandler{ +public class MemberAuthSuccessHandler implements AuthenticationSuccessHandler { - @Override - public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { - ObjectMapper mapper = new ObjectMapper(); + private final TokenService tokenService; + private final NetmemberService netmemberService; - response.setStatus(HttpStatus.OK.value()); - response.setContentType(MediaType.APPLICATION_JSON_VALUE); + public MemberAuthSuccessHandler(@Lazy TokenService tokenService, NetmemberService netmemberService) { + this.tokenService = tokenService; + this.netmemberService = netmemberService; + } - response.getWriter().write(mapper.writeValueAsString(ApiResponse.ok("Success"))); - - clearAuthenticationAttributes(request); - } - - protected final void clearAuthenticationAttributes(HttpServletRequest request) { - HttpSession session = request.getSession(false); - if (session == null) { + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + + // 로그인 성공한 사용자 가져오기 + Object principal = authentication.getPrincipal(); + if (!(principal instanceof MemberVo member)) { + response.setStatus(HttpStatus.UNAUTHORIZED.value()); return; } - session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION); - } + + // 로그인 토큰 생성 + String username = member.getLoginId(); + String loginToken = tokenService.generateToken(username, "login"); + + // DB에 저장 + netmemberService.updateMemberToken(username, loginToken); + + response.setStatus(HttpStatus.OK.value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + + response.getWriter().write(mapper.writeValueAsString(ApiResponse.ok("Success"))); + + clearAuthenticationAttributes(request); + } + + protected final void clearAuthenticationAttributes(HttpServletRequest request) { + HttpSession session = request.getSession(false); + if (session == null) { + return; + } + session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION); + } } diff --git a/src/main/java/io/company/localhost/common/security/service/CustomRememberMeServices.java b/src/main/java/io/company/localhost/common/security/service/CustomRememberMeServices.java index 724c266..4470c42 100644 --- a/src/main/java/io/company/localhost/common/security/service/CustomRememberMeServices.java +++ b/src/main/java/io/company/localhost/common/security/service/CustomRememberMeServices.java @@ -10,11 +10,12 @@ * DATE AUTHOR NOTE * ----------------------------------------------------------- * 24.12.06 조인제 최초 생성 - * + * 24.02.03 박지윤 토큰 로직 변경 *************************************************************/ package io.company.localhost.common.security.service; import io.company.localhost.common.security.details.MemberPrincipalDetails; +import io.company.localhost.service.NetmemberService; import io.company.localhost.vo.MemberVo; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; @@ -27,8 +28,6 @@ import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.authentication.RememberMeServices; -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.util.Base64; @@ -38,12 +37,13 @@ public class CustomRememberMeServices implements RememberMeServices { private static final String REMEMBER_ME_COOKIE_NAME = "remember-me"; private static final long TOKEN_VALIDITY_SECONDS = 60 * 60 * 24 * 365; // 1 year private static final String DELIMITER = ":"; + - private final String secretKey; + private final TokenService tokenService; private final UserDetailsService userDetailsService; - public CustomRememberMeServices(String secretKey, UserDetailsService userDetailsService) { - this.secretKey = secretKey; + public CustomRememberMeServices(TokenService tokenService, UserDetailsService userDetailsService) { + this.tokenService = tokenService; this.userDetailsService = userDetailsService; } @@ -55,33 +55,22 @@ public class CustomRememberMeServices implements RememberMeServices { return null; } - String[] tokenParts = decodeAndSplitCookie(rememberMeCookie.getValue()); - if (tokenParts == null || tokenParts.length != 3) { - return null; - } + String token = rememberMeCookie.getValue(); + + if (!tokenService.validateToken(token, "rememberme")) { + return null; + } - String username = tokenParts[0]; - long expiryTime; - try { - expiryTime = Long.parseLong(tokenParts[1]); - } catch (NumberFormatException e) { - return null; - } - - String signature = tokenParts[2]; - if (!isTokenValid(username, expiryTime, signature)) { - return null; - } - - if (System.currentTimeMillis() > expiryTime) { - return null; - } + String[] tokenParts = decodeAndSplitCookie(token); + String username = tokenParts[0]; UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (userDetails != null) { MemberPrincipalDetails memberDetails = (MemberPrincipalDetails) userDetails; MemberVo memberVo = memberDetails.member(); - RememberMeAuthenticationToken auth = new RememberMeAuthenticationToken(secretKey, memberVo, userDetails.getAuthorities()); + + String rememberMeSecretKey = tokenService.getRememberMeSecretKey(); + RememberMeAuthenticationToken auth = new RememberMeAuthenticationToken(rememberMeSecretKey, memberVo, userDetails.getAuthorities()); auth.setAuthenticated(true); SecurityContextHolder.getContext().setAuthentication(auth); return auth; @@ -104,10 +93,11 @@ public class CustomRememberMeServices implements RememberMeServices { } String username = member.getLoginId(); - long expiryTime = System.currentTimeMillis() + (TOKEN_VALIDITY_SECONDS * 1000); - String signature = generateSignature(username, expiryTime); - String tokenValue = encodeToken(username, expiryTime, signature); + String tokenValue = tokenService.generateToken(username, "rememberme"); + String loginToken = tokenService.generateToken(username, "login"); + // DB에 저장 +// netmemberService.updateMemberToken(username, loginToken); SecurityContextHolder.getContext().setAuthentication(successfulAuthentication); @@ -148,25 +138,5 @@ public class CustomRememberMeServices implements RememberMeServices { return null; } } - - private String encodeToken(String username, long expiryTime, String signature) { - String tokenValue = username + DELIMITER + expiryTime + DELIMITER + signature; - return Base64.getEncoder().encodeToString(tokenValue.getBytes(StandardCharsets.UTF_8)); - } - - private boolean isTokenValid(String username, long expiryTime, String signature) { - String expectedSignature = generateSignature(username, expiryTime); - return expectedSignature.equals(signature); - } - - private String generateSignature(String username, long expiryTime) { - try { - String data = username + DELIMITER + expiryTime; - Mac mac = Mac.getInstance("HmacSHA256"); - mac.init(new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); - return Base64.getEncoder().encodeToString(mac.doFinal(data.getBytes(StandardCharsets.UTF_8))); - } catch (Exception e) { - throw new IllegalStateException("Failed to generate signature", e); - } - } + } diff --git a/src/main/java/io/company/localhost/common/security/service/TokenService.java b/src/main/java/io/company/localhost/common/security/service/TokenService.java new file mode 100644 index 0000000..165b0e8 --- /dev/null +++ b/src/main/java/io/company/localhost/common/security/service/TokenService.java @@ -0,0 +1,121 @@ +/************************************************************ + * + * @packageName : io.company.localhost.common.security.service + * @fileName : TokenService.java + * @author : 박지윤 + * @date : 24.01.24 + * @description : + * + * =========================================================== + * DATE AUTHOR NOTE + * ----------------------------------------------------------- + * 24.01.24 박지윤 최초 생성 + *************************************************************/ +package io.company.localhost.common.security.service; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +import org.springframework.stereotype.Service; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +public class TokenService { + private static final long REMBER_TOKEN_VALIDITY_SECONDS = 60 * 60 * 24 * 365; // 1년 + private static final long LOGIN_TOKEN_VALIDITY_SECONDS = 60 * 60 * 24 * 365; // 하루 + private static final String DELIMITER = ":"; + private final String rememberMeSecretKey; + private final String loginSecretKey; + + public TokenService(String rememberMeSecretKey, String loginSecretKey) { + this.rememberMeSecretKey = rememberMeSecretKey; + this.loginSecretKey = loginSecretKey; + } + + public String generateToken(String username, String tokenType) { + long expiryTime; + + // 토큰타입에 따라 만료기간 다르게 설정 + if ("rememberme".equals(tokenType)) { + expiryTime = System.currentTimeMillis() + (REMBER_TOKEN_VALIDITY_SECONDS * 1000); + } else if ("login".equals(tokenType)) { + expiryTime = System.currentTimeMillis() + (LOGIN_TOKEN_VALIDITY_SECONDS * 1000); + } else { + throw new IllegalArgumentException("Invalid token type"); + } + + String signature = generateSignature(username, expiryTime, tokenType); + return encodeToken(username, expiryTime, signature); + } + + public boolean validateToken(String token, String tokenType) { + try { + String[] tokenParts = decodeAndSplitToken(token); + if (tokenParts == null || tokenParts.length != 3) { + return false; + } + + String username = tokenParts[0]; + long expiryTime; + try { + expiryTime = Long.parseLong(tokenParts[1]); + } catch (NumberFormatException e) { + return false; + } + + String signature = tokenParts[2]; + if (!isTokenValid(username, expiryTime, signature, tokenType)) { + return false; + } + + return System.currentTimeMillis() <= expiryTime; + } catch (Exception e) { + return false; + } + } + + + private String generateSignature(String username, long expiryTime, String tokenType) { + try { + // tokenType에 따라 login 또는 remember secretKey 사용 + String secretKeyToUse = "rememberme".equals(tokenType) ? rememberMeSecretKey : loginSecretKey; + + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(secretKeyToUse.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); + String data = username + DELIMITER + expiryTime; + return Base64.getEncoder().encodeToString(mac.doFinal(data.getBytes(StandardCharsets.UTF_8))); + } catch (Exception e) { + throw new IllegalStateException("Failed to generate signature", e); + } + } + + + private String encodeToken(String username, long expiryTime, String signature) { + String tokenValue = username + DELIMITER + expiryTime + DELIMITER + signature; + return Base64.getEncoder().encodeToString(tokenValue.getBytes(StandardCharsets.UTF_8)); + } + + private String[] decodeAndSplitToken(String token) { + try { + String decodedValue = new String(Base64.getDecoder().decode(token), StandardCharsets.UTF_8); + return decodedValue.split(DELIMITER); + } catch (Exception e) { + return null; + } + } + + private boolean isTokenValid(String username, long expiryTime, String signature, String tokenType) { + String expectedSignature = generateSignature(username, expiryTime, tokenType); + return expectedSignature.equals(signature); + } + + public String getRememberMeSecretKey() { + return rememberMeSecretKey; + } + +} \ No newline at end of file diff --git a/src/main/java/io/company/localhost/mapper/NetmemberMapper.java b/src/main/java/io/company/localhost/mapper/NetmemberMapper.java index fcaf28f..3020353 100644 --- a/src/main/java/io/company/localhost/mapper/NetmemberMapper.java +++ b/src/main/java/io/company/localhost/mapper/NetmemberMapper.java @@ -26,6 +26,8 @@ public interface NetmemberMapper { MemberVo findByLoginId(String id); + int updateMemberToken(String id, String token); + int insertMember(MapDto map); int selectCheckId(String memberIds); diff --git a/src/main/java/io/company/localhost/service/netmemberService.java b/src/main/java/io/company/localhost/service/NetmemberService.java similarity index 92% rename from src/main/java/io/company/localhost/service/netmemberService.java rename to src/main/java/io/company/localhost/service/NetmemberService.java index 68baf89..e00f7df 100644 --- a/src/main/java/io/company/localhost/service/netmemberService.java +++ b/src/main/java/io/company/localhost/service/NetmemberService.java @@ -19,6 +19,7 @@ import java.util.List; import org.springframework.security.crypto.password.DelegatingPasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import io.company.localhost.common.dto.MapDto; @@ -28,7 +29,7 @@ import lombok.RequiredArgsConstructor; @Service @RequiredArgsConstructor -public class netmemberService { +public class NetmemberService { private final NetmemberMapper memberMapper; private final commoncodMapper commoncodMapper; private final DelegatingPasswordEncoder passwordEncoder; @@ -79,6 +80,7 @@ public class netmemberService { public boolean selectCheckId(String memberIds) { return memberMapper.selectCheckId(memberIds) > 0; } + /** * 사원 목록 전체 조회 * @@ -89,4 +91,9 @@ public class netmemberService { return memberMapper.getallUserList(); } + + @Transactional + public void updateMemberToken(String id, String token) { + memberMapper.updateMemberToken(id, token); + } } diff --git a/src/main/resources/mapper/netmemberMapper.xml b/src/main/resources/mapper/netmemberMapper.xml index 1dea72a..e79edec 100644 --- a/src/main/resources/mapper/netmemberMapper.xml +++ b/src/main/resources/mapper/netmemberMapper.xml @@ -32,6 +32,13 @@ MEMBERIDS = #{id} + + UPDATE netmember + SET MEMBERTKN = #{token} + WHERE MEMBERIDS = #{id} + + + INSERT INTO netmember ( @@ -87,6 +94,7 @@ FROM netmember WHERE MEMBERIDS = #{memberIds} +