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.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<RequestAuthorizationContext> authorizationManager;
// 세션 관련 상수 설정
@ -64,6 +69,9 @@ public class SecurityConfig {
final String SECURITY_BASE_URL = "/api/user";
final String LOGIN_URL = SECURITY_BASE_URL + "/login";
final String LOGIN_KEY = "loginSecretKey";
// 보안 필터 체인 설정
@Bean
public SecurityFilterChain restSecurityFilterChain(HttpSecurity http, WebCorsFilter webCorsFilter) throws Exception {
@ -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());

View File

@ -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;
@ -33,10 +38,33 @@ import java.io.IOException;
@Component("successHandler")
public class MemberAuthSuccessHandler implements AuthenticationSuccessHandler {
private final TokenService tokenService;
private final NetmemberService netmemberService;
public MemberAuthSuccessHandler(@Lazy TokenService tokenService, NetmemberService netmemberService) {
this.tokenService = tokenService;
this.netmemberService = netmemberService;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
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;
}
// 로그인 토큰 생성
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);

View File

@ -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;
@ -39,11 +38,12 @@ public class CustomRememberMeServices implements RememberMeServices {
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) {
String token = rememberMeCookie.getValue();
if (!tokenService.validateToken(token, "rememberme")) {
return null;
}
String[] tokenParts = decodeAndSplitCookie(token);
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;
}
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);
@ -149,24 +139,4 @@ public class CustomRememberMeServices implements RememberMeServices {
}
}
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);
int updateMemberToken(String id, String token);
int insertMember(MapDto map);
int selectCheckId(String memberIds);

View File

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

View File

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