login token

This commit is contained in:
yoon 2025-02-03 14:43:38 +09:00
parent b2e86d5350
commit 1d3f7d7ddc
7 changed files with 219 additions and 69 deletions

View File

@ -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.RestAccessDeniedHandler;
import io.company.localhost.common.security.handler.RestAuthenticationEntryPointHandler; import io.company.localhost.common.security.handler.RestAuthenticationEntryPointHandler;
import io.company.localhost.common.security.service.CustomRememberMeServices; 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.service.MemberPrincipalDetailService;
import io.company.localhost.common.security.session.AuthenticationSessionControlStrategy; import io.company.localhost.common.security.session.AuthenticationSessionControlStrategy;
import io.company.localhost.common.security.session.CustomSessionRegistryImpl; import io.company.localhost.common.security.session.CustomSessionRegistryImpl;
import io.company.localhost.service.NetmemberService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
@ -52,6 +56,7 @@ public class SecurityConfig {
private final MemberPrincipalDetailService userDetailsService; private final MemberPrincipalDetailService userDetailsService;
private final MemberAuthSuccessHandler successHandler; private final MemberAuthSuccessHandler successHandler;
private final MemberAuthFailureHandler failureHandler; private final MemberAuthFailureHandler failureHandler;
private final NetmemberService netmemberService;
private final AuthorizationManager<RequestAuthorizationContext> authorizationManager; private final AuthorizationManager<RequestAuthorizationContext> authorizationManager;
// 세션 관련 상수 설정 // 세션 관련 상수 설정
@ -63,6 +68,9 @@ public class SecurityConfig {
// API 경로 관련 상수 설정 // API 경로 관련 상수 설정
final String SECURITY_BASE_URL = "/api/user"; final String SECURITY_BASE_URL = "/api/user";
final String LOGIN_URL = SECURITY_BASE_URL + "/login"; final String LOGIN_URL = SECURITY_BASE_URL + "/login";
final String LOGIN_KEY = "loginSecretKey";
// 보안 필터 체인 설정 // 보안 필터 체인 설정
@Bean @Bean
@ -115,9 +123,15 @@ public class SecurityConfig {
@Bean @Bean
public RememberMeServices rememberMeServices(){ 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() { protected ConcurrentSessionControlAuthenticationStrategy sessionControlStrategy() {
AuthenticationSessionControlStrategy sessionControlStrategy = new AuthenticationSessionControlStrategy(sessionRegistry()); AuthenticationSessionControlStrategy sessionControlStrategy = new AuthenticationSessionControlStrategy(sessionRegistry());

View File

@ -16,10 +16,15 @@ package io.company.localhost.common.security.handler;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import io.company.localhost.common.dto.ApiResponse; 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.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession; import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
@ -31,25 +36,48 @@ import java.io.IOException;
@Slf4j @Slf4j
@Component("successHandler") @Component("successHandler")
public class MemberAuthSuccessHandler implements AuthenticationSuccessHandler{ public class MemberAuthSuccessHandler implements AuthenticationSuccessHandler {
@Override private final TokenService tokenService;
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { private final NetmemberService netmemberService;
ObjectMapper mapper = new ObjectMapper();
response.setStatus(HttpStatus.OK.value()); public MemberAuthSuccessHandler(@Lazy TokenService tokenService, NetmemberService netmemberService) {
response.setContentType(MediaType.APPLICATION_JSON_VALUE); this.tokenService = tokenService;
this.netmemberService = netmemberService;
}
response.getWriter().write(mapper.writeValueAsString(ApiResponse.ok("Success"))); @Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
clearAuthenticationAttributes(request); Authentication authentication) throws IOException {
} ObjectMapper mapper = new ObjectMapper();
protected final void clearAuthenticationAttributes(HttpServletRequest request) { // 로그인 성공한 사용자 가져오기
HttpSession session = request.getSession(false); Object principal = authentication.getPrincipal();
if (session == null) { if (!(principal instanceof MemberVo member)) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return; 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);
}
} }

View File

@ -10,11 +10,12 @@
* DATE AUTHOR NOTE * DATE AUTHOR NOTE
* ----------------------------------------------------------- * -----------------------------------------------------------
* 24.12.06 조인제 최초 생성 * 24.12.06 조인제 최초 생성
* * 24.02.03 박지윤 토큰 로직 변경
*************************************************************/ *************************************************************/
package io.company.localhost.common.security.service; package io.company.localhost.common.security.service;
import io.company.localhost.common.security.details.MemberPrincipalDetails; import io.company.localhost.common.security.details.MemberPrincipalDetails;
import io.company.localhost.service.NetmemberService;
import io.company.localhost.vo.MemberVo; import io.company.localhost.vo.MemberVo;
import jakarta.servlet.http.Cookie; import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest; 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.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.RememberMeServices; import org.springframework.security.web.authentication.RememberMeServices;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Base64; 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 String REMEMBER_ME_COOKIE_NAME = "remember-me";
private static final long TOKEN_VALIDITY_SECONDS = 60 * 60 * 24 * 365; // 1 year private static final long TOKEN_VALIDITY_SECONDS = 60 * 60 * 24 * 365; // 1 year
private static final String DELIMITER = ":"; private static final String DELIMITER = ":";
private final String secretKey; private final TokenService tokenService;
private final UserDetailsService userDetailsService; private final UserDetailsService userDetailsService;
public CustomRememberMeServices(String secretKey, UserDetailsService userDetailsService) { public CustomRememberMeServices(TokenService tokenService, UserDetailsService userDetailsService) {
this.secretKey = secretKey; this.tokenService = tokenService;
this.userDetailsService = userDetailsService; this.userDetailsService = userDetailsService;
} }
@ -55,33 +55,22 @@ public class CustomRememberMeServices implements RememberMeServices {
return null; return null;
} }
String[] tokenParts = decodeAndSplitCookie(rememberMeCookie.getValue()); String token = rememberMeCookie.getValue();
if (tokenParts == null || tokenParts.length != 3) {
return null; if (!tokenService.validateToken(token, "rememberme")) {
} return null;
}
String username = tokenParts[0]; String[] tokenParts = decodeAndSplitCookie(token);
long expiryTime; String username = tokenParts[0];
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;
}
UserDetails userDetails = userDetailsService.loadUserByUsername(username); UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (userDetails != null) { if (userDetails != null) {
MemberPrincipalDetails memberDetails = (MemberPrincipalDetails) userDetails; MemberPrincipalDetails memberDetails = (MemberPrincipalDetails) userDetails;
MemberVo memberVo = memberDetails.member(); 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); auth.setAuthenticated(true);
SecurityContextHolder.getContext().setAuthentication(auth); SecurityContextHolder.getContext().setAuthentication(auth);
return auth; return auth;
@ -104,10 +93,11 @@ public class CustomRememberMeServices implements RememberMeServices {
} }
String username = member.getLoginId(); String username = member.getLoginId();
long expiryTime = System.currentTimeMillis() + (TOKEN_VALIDITY_SECONDS * 1000); String tokenValue = tokenService.generateToken(username, "rememberme");
String signature = generateSignature(username, expiryTime); String loginToken = tokenService.generateToken(username, "login");
String tokenValue = encodeToken(username, expiryTime, signature);
// DB에 저장
// netmemberService.updateMemberToken(username, loginToken);
SecurityContextHolder.getContext().setAuthentication(successfulAuthentication); SecurityContextHolder.getContext().setAuthentication(successfulAuthentication);
@ -148,25 +138,5 @@ public class CustomRememberMeServices implements RememberMeServices {
return null; 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);
}
}
} }

View File

@ -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;
}
}

View File

@ -26,6 +26,8 @@ public interface NetmemberMapper {
MemberVo findByLoginId(String id); MemberVo findByLoginId(String id);
int updateMemberToken(String id, String token);
int insertMember(MapDto map); int insertMember(MapDto map);
int selectCheckId(String memberIds); int selectCheckId(String memberIds);

View File

@ -19,6 +19,7 @@ import java.util.List;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder; import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import io.company.localhost.common.dto.MapDto; import io.company.localhost.common.dto.MapDto;
@ -28,7 +29,7 @@ import lombok.RequiredArgsConstructor;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class netmemberService { public class NetmemberService {
private final NetmemberMapper memberMapper; private final NetmemberMapper memberMapper;
private final commoncodMapper commoncodMapper; private final commoncodMapper commoncodMapper;
private final DelegatingPasswordEncoder passwordEncoder; private final DelegatingPasswordEncoder passwordEncoder;
@ -79,6 +80,7 @@ public class netmemberService {
public boolean selectCheckId(String memberIds) { public boolean selectCheckId(String memberIds) {
return memberMapper.selectCheckId(memberIds) > 0; return memberMapper.selectCheckId(memberIds) > 0;
} }
/** /**
* 사원 목록 전체 조회 * 사원 목록 전체 조회
* *
@ -89,4 +91,9 @@ public class netmemberService {
return memberMapper.getallUserList(); return memberMapper.getallUserList();
} }
@Transactional
public void updateMemberToken(String id, String token) {
memberMapper.updateMemberToken(id, token);
}
} }

View File

@ -32,6 +32,13 @@
MEMBERIDS = #{id} MEMBERIDS = #{id}
</select> </select>
<update id="updateMemberToken">
UPDATE netmember
SET MEMBERTKN = #{token}
WHERE MEMBERIDS = #{id}
</update>
<!-- 회원가입 --> <!-- 회원가입 -->
<insert id="insertMember" useGeneratedKeys="true" keyProperty="MEMBERSEQ"> <insert id="insertMember" useGeneratedKeys="true" keyProperty="MEMBERSEQ">
INSERT INTO netmember ( INSERT INTO netmember (
@ -87,6 +94,7 @@
FROM netmember FROM netmember
WHERE MEMBERIDS = #{memberIds} WHERE MEMBERIDS = #{memberIds}
</select> </select>
<select id="getallUserList" resultType="Map"> <select id="getallUserList" resultType="Map">
SELECT * SELECT *
FROM FROM