Admin , Member annotation 추가

rememberMe 쿠키 저장방식 변경
roleHierarchy 적용
This commit is contained in:
ckx6954 2024-12-12 09:41:15 +09:00
parent 6d5649535f
commit cc5e87af0c
10 changed files with 233 additions and 141 deletions

View File

@ -0,0 +1,11 @@
package io.company.localhost.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Admin {
}

View File

@ -0,0 +1,11 @@
package io.company.localhost.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Member {
}

View File

@ -1,15 +1,10 @@
package io.company.localhost.common.annotation; package io.company.localhost.common.annotation;
import static java.lang.annotation.ElementType.*; import java.lang.annotation.*;
import static java.lang.annotation.RetentionPolicy.*;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
@Documented @Documented
@Retention(RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target({ PARAMETER }) @Target(ElementType.PARAMETER)
public @interface ReqMap { public @interface ReqMap {
} }

View File

@ -1,14 +1,16 @@
package io.company.localhost.common.config; package io.company.localhost.common.security.config;
import java.util.HashMap;
import java.util.Map;
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.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder; import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.HashMap;
import java.util.Map;
@Configuration @Configuration
public class AuthConfig { public class AuthConfig {
@ -22,4 +24,9 @@ public class AuthConfig {
return delegatingPasswordEncoder; return delegatingPasswordEncoder;
} }
@Bean
public RoleHierarchy roleHierarchy() {
return RoleHierarchyImpl.fromHierarchy("ROLE_ADMIN > ROLE_MEMBER > ROLE_ANONYMOUS");
}
} }

View File

@ -1,4 +1,4 @@
package io.company.localhost.common.config; package io.company.localhost.common.security.config;
import io.company.localhost.common.filter.WebCorsFilter; import io.company.localhost.common.filter.WebCorsFilter;
import io.company.localhost.common.security.dsl.RestApiDsl; import io.company.localhost.common.security.dsl.RestApiDsl;
@ -45,12 +45,10 @@ public class SecurityConfig {
final boolean MAX_SESSION_PRENT = false; // 최대 세션 초과 로그인 방지 여부 final boolean MAX_SESSION_PRENT = false; // 최대 세션 초과 로그인 방지 여부
final String REMEMBER = "remember"; // 'remember me' 기능을 위한 final String REMEMBER = "remember"; // 'remember me' 기능을 위한
final String REMEMBER_KEY = "rememberSecretKey"; final String REMEMBER_KEY = "rememberSecretKey";
final int REMEMBER_TIME = 60 * 60 * 24 * 365; // 'remember me' 기능의 유효기간 (1년)
// 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 INVALID_URL = SECURITY_BASE_URL + "/login/duplicated-login";
// 보안 필터 체인 설정 // 보안 필터 체인 설정
@Bean @Bean
@ -89,7 +87,6 @@ public class SecurityConfig {
.rememberMe(rm -> .rememberMe(rm ->
rm rm
.key(REMEMBER_KEY) .key(REMEMBER_KEY)
.tokenValiditySeconds(REMEMBER_TIME)
.rememberMeParameter(REMEMBER) .rememberMeParameter(REMEMBER)
.rememberMeServices(rememberMeServices())) .rememberMeServices(rememberMeServices()))
// 로그인 성공 실패 핸들러 설정 // 로그인 성공 실패 핸들러 설정

View File

@ -1,17 +1,17 @@
package io.company.localhost.common.security.handler; package io.company.localhost.common.security.handler;
import java.io.IOException; import com.fasterxml.jackson.databind.ObjectMapper;
import io.company.localhost.common.exception.code.UserErrorCode;
import io.company.localhost.utils.AuthUtil;
import io.company.localhost.vo.MemberVo;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.AuthenticationEntryPoint;
import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException;
import io.company.localhost.common.exception.code.UserErrorCode;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
public class RestAuthenticationEntryPointHandler implements AuthenticationEntryPoint { public class RestAuthenticationEntryPointHandler implements AuthenticationEntryPoint {
@ -21,12 +21,15 @@ public class RestAuthenticationEntryPointHandler implements AuthenticationEntryP
public void commence(HttpServletRequest request, HttpServletResponse response, public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException { AuthenticationException authException) throws IOException, ServletException {
// HTTP 상태 코드를 401 (Unauthorized) 설정
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
// 응답의 콘텐츠 타입을 JSON으로 설정
response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setContentType(MediaType.APPLICATION_JSON_VALUE);
MemberVo user = AuthUtil.getUser();
// 사용자 정의 에러 코드와 메시지를 JSON 형식으로 응답 if(user != null) {
response.getWriter().write(mapper.writeValueAsString(UserErrorCode.NOT_AUTH_USER.getApiResponse())); response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write(mapper.writeValueAsString(UserErrorCode.INACTIVE_USER.getApiResponse()));
}else{
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write(mapper.writeValueAsString(UserErrorCode.NOT_AUTH_USER.getApiResponse()));
}
} }
} }

View File

@ -1,25 +1,29 @@
package io.company.localhost.common.security.manager; package io.company.localhost.common.security.manager;
import io.company.localhost.common.annotation.Admin;
import io.company.localhost.common.annotation.Guest; import io.company.localhost.common.annotation.Guest;
import io.company.localhost.common.annotation.Member;
import io.company.localhost.common.security.mapper.MapBasedUrlRoleMapper; import io.company.localhost.common.security.mapper.MapBasedUrlRoleMapper;
import io.company.localhost.common.security.service.DynamicAuthorizationService; import io.company.localhost.common.security.service.DynamicAuthorizationService;
import io.company.localhost.common.wrapper.RequestMappingWrapper;
import io.company.localhost.utils.ExceptionUtil;
import io.company.localhost.vo.MemberVo; import io.company.localhost.vo.MemberVo;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.authorization.AuthorityAuthorizationManager; import org.springframework.security.authorization.AuthorityAuthorizationManager;
import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.AuthorizationResult; import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.web.access.expression.DefaultHttpSecurityExpressionHandler;
import org.springframework.security.web.access.expression.WebExpressionAuthorizationManager; import org.springframework.security.web.access.expression.WebExpressionAuthorizationManager;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext; import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcherEntry; import org.springframework.security.web.util.matcher.RequestMatcherEntry;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector; import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
@ -30,10 +34,13 @@ import java.util.stream.Collectors;
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class CustomDynamicAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> { public class CustomDynamicAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
List<RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>>> mappings; List<RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>>> mappings;
private static final AuthorizationDecision DENY = new AuthorizationDecision(false);
private final HandlerMappingIntrospector handlerMappingIntrospector; private final HandlerMappingIntrospector handlerMappingIntrospector;
private final RequestMappingHandlerMapping requestMappingHandlerMapping; private final RequestMappingHandlerMapping requestMappingHandlerMapping;
private final RequestMappingWrapper requestMappingWrapper;
private final RoleHierarchy roleHierarchy;
// 클래스 초기화 동적으로 URL-권한 매핑을 설정 // 클래스 초기화 동적으로 URL-권한 매핑을 설정
@PostConstruct @PostConstruct
@ -51,56 +58,64 @@ public class CustomDynamicAuthorizationManager implements AuthorizationManager<R
@Override @Override
public AuthorizationResult authorize(Supplier<Authentication> authentication, RequestAuthorizationContext object) { public AuthorizationResult authorize(Supplier<Authentication> authentication, RequestAuthorizationContext object) {
HttpServletRequest request = object.getRequest(); HttpServletRequest request = object.getRequest();
Authentication auth = authentication.get(); Authentication auth = authentication.get();
// @Guest 메서드인지 확인 try {
if (isGuestRequest(request) && !(auth.getPrincipal() instanceof MemberVo)) { if (requestMappingWrapper.hasAnyAnnotation(request, Guest.class) && !(auth.getPrincipal() instanceof MemberVo)) {
boolean allowGuest = !auth.isAuthenticated() || "anonymousUser".equals(auth.getPrincipal()); boolean allowGuest = !auth.isAuthenticated() || "anonymousUser".equals(auth.getPrincipal());
return new AuthorizationDecision(allowGuest); return new AuthorizationDecision(allowGuest);
} }else if (requestMappingWrapper.hasAnyAnnotation(request, Member.class)) {
boolean isMember = auth.isAuthenticated() &&
roleHierarchy.getReachableGrantedAuthorities(auth.getAuthorities()).stream()
for (RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>> mapping : this.mappings) { .anyMatch(grantedAuthority -> grantedAuthority.getAuthority().equals("ROLE_MEMBER")); // 수정
return new AuthorizationDecision(isMember);
RequestMatcher matcher = mapping.getRequestMatcher(); }else if (requestMappingWrapper.hasAnyAnnotation(request, Admin.class)) {
RequestMatcher.MatchResult matchResult = matcher.matcher(object.getRequest()); boolean isAdmin = auth.isAuthenticated() &&
roleHierarchy.getReachableGrantedAuthorities(auth.getAuthorities()).stream()
if (matchResult.isMatch()) { .anyMatch(grantedAuthority -> grantedAuthority.getAuthority().equals("ROLE_ADMIN")); // 수정
AuthorizationManager<RequestAuthorizationContext> manager = mapping.getEntry(); return new AuthorizationDecision(isAdmin);
return manager.authorize(authentication,
new RequestAuthorizationContext(object.getRequest(), matchResult.getVariables()));
} }
for (RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>> mapping : this.mappings) {
RequestMatcher matcher = mapping.getRequestMatcher();
RequestMatcher.MatchResult matchResult = matcher.matcher(object.getRequest());
if (matchResult.isMatch()) {
AuthorizationManager<RequestAuthorizationContext> manager = mapping.getEntry();
return manager.authorize(authentication,
new RequestAuthorizationContext(object.getRequest(), matchResult.getVariables()));
}
}
}catch (Exception e){
ExceptionUtil.messageTrace(e);
} }
return DENY;
return new AuthorizationDecision(false);
} }
// 역할에 맞는 AuthorizationManager 반환 // 역할에 맞는 AuthorizationManager 반환
private AuthorizationManager<RequestAuthorizationContext> customAuthorizationManager(String role) { private AuthorizationManager<RequestAuthorizationContext> customAuthorizationManager(String role) {
if (role.startsWith("ROLE")) { if (role.startsWith("ROLE")) {
return AuthorityAuthorizationManager.hasAuthority(role); AuthorityAuthorizationManager<RequestAuthorizationContext> objectAuthorityAuthorizationManager =
AuthorityAuthorizationManager.hasAuthority(role);
objectAuthorityAuthorizationManager.setRoleHierarchy(roleHierarchy);
return objectAuthorityAuthorizationManager;
}else{ }else{
return new WebExpressionAuthorizationManager(role); DefaultHttpSecurityExpressionHandler expressionHandler = new DefaultHttpSecurityExpressionHandler();
expressionHandler.setRoleHierarchy(roleHierarchy);
WebExpressionAuthorizationManager webExpressionAuthorizationManager =
new WebExpressionAuthorizationManager(role);
webExpressionAuthorizationManager.setExpressionHandler(expressionHandler);
return webExpressionAuthorizationManager;
} }
} }
// @Guest가 적용된 요청인지 확인
private boolean isGuestRequest(HttpServletRequest request) {
try {
HandlerExecutionChain handlerExecutionChain = requestMappingHandlerMapping.getHandler(request);
if (handlerExecutionChain != null) {
Object handler = handlerExecutionChain.getHandler();
if (handler instanceof HandlerMethod handlerMethod) {
return handlerMethod.getMethod().isAnnotationPresent(Guest.class); }
}
} catch (Exception e) {
return false;
}
return false;
}
@Override @Override
public void verify(Supplier<Authentication> authentication, RequestAuthorizationContext object) { public void verify(Supplier<Authentication> authentication, RequestAuthorizationContext object) {

View File

@ -10,14 +10,11 @@ public class MapBasedUrlRoleMapper implements UrlRoleMapper{
final String PERMIT_ALL = "permitAll"; final String PERMIT_ALL = "permitAll";
final String ROLE_MEMBER = "ROLE_MEMBER"; final String ROLE_MEMBER = "ROLE_MEMBER";
final String ROLE_MANAGER = "ROLE_MANAGER";
final String ROLE_ADMIN = "ROLE_ADMIN"; final String ROLE_ADMIN = "ROLE_ADMIN";
@Override @Override
public Map<String, String> getUrlRoleMappings() { public Map<String, String> getUrlRoleMappings() {
urlRoleMappings.put("/api/login", PERMIT_ALL);
urlRoleMappings.put("/api/user/**", ROLE_MEMBER); urlRoleMappings.put("/api/user/**", ROLE_MEMBER);
urlRoleMappings.put("/api/user/logout", PERMIT_ALL);
urlRoleMappings.put("/api/test/**", ROLE_MEMBER); urlRoleMappings.put("/api/test/**", ROLE_MEMBER);

View File

@ -1,32 +1,35 @@
package io.company.localhost.common.security.service; package io.company.localhost.common.security.service;
import static org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices.*;
import java.util.Base64;
import org.springframework.security.authentication.RememberMeAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.RememberMeServices;
import io.company.localhost.common.security.details.MemberPrincipalDetails; import io.company.localhost.common.security.details.MemberPrincipalDetails;
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;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.RememberMeAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
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;
@Slf4j @Slf4j
public class CustomRememberMeServices implements RememberMeServices { public class CustomRememberMeServices implements RememberMeServices {
private static final String REMEMBER_ME_KEY = "remember"; private static final String REMEMBER_ME_COOKIE_NAME = "remember-me";
private final UserDetailsService userDetailsService; private static final long TOKEN_VALIDITY_SECONDS = 60 * 60 * 24 * 365; // 1 year
private final String key; private static final String DELIMITER = ":";
public CustomRememberMeServices(String key, UserDetailsService userDetailsService) { private final String secretKey;
this.key = key; private final UserDetailsService userDetailsService;
public CustomRememberMeServices(String secretKey, UserDetailsService userDetailsService) {
this.secretKey = secretKey;
this.userDetailsService = userDetailsService; this.userDetailsService = userDetailsService;
} }
@ -35,26 +38,39 @@ public class CustomRememberMeServices implements RememberMeServices {
Cookie rememberMeCookie = getRememberMeCookie(request); Cookie rememberMeCookie = getRememberMeCookie(request);
if (rememberMeCookie == null) { if (rememberMeCookie == null) {
log.debug("No remember-me cookie found");
return null; return null;
} }
String username = decodeCookie(rememberMeCookie.getValue()); String[] tokenParts = decodeAndSplitCookie(rememberMeCookie.getValue());
if (username == null || username.isEmpty()) { if (tokenParts == null || tokenParts.length != 3) {
log.error("Invalid remember-me cookie"); 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; 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(key, memberVo, userDetails.getAuthorities()); RememberMeAuthenticationToken auth = new RememberMeAuthenticationToken(secretKey, memberVo, userDetails.getAuthorities());
auth.setAuthenticated(true);
SecurityContextHolder.getContext().setAuthentication(auth); SecurityContextHolder.getContext().setAuthentication(auth);
return auth; return auth;
}else {
log.error("UserDetailsService returned null for username: {}", username);
} }
return null; return null;
@ -62,58 +78,77 @@ public class CustomRememberMeServices implements RememberMeServices {
@Override @Override
public void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { public void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
Object principal = successfulAuthentication.getPrincipal();
Boolean rememberMe = (Boolean) request.getAttribute(REMEMBER_ME_KEY); if (!(principal instanceof MemberVo)) {
if (rememberMe != null && rememberMe) { return;
// Remember-Me 토큰 생성 설정
Object principal = successfulAuthentication.getPrincipal();
MemberVo member = (MemberVo)principal;
String username = member.getLoginId();
String rememberMeToken = generateRememberMeToken(username);
int tokenValiditySeconds = getTokenValiditySeconds();
Cookie rememberMeCookie = new Cookie(SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, rememberMeToken);
rememberMeCookie.setPath("/");
rememberMeCookie.setMaxAge(tokenValiditySeconds);
rememberMeCookie.setHttpOnly(true);
rememberMeCookie.setSecure(request.isSecure());
response.addCookie(rememberMeCookie);
} }
MemberVo member = (MemberVo) principal;
MemberPrincipalDetails details = new MemberPrincipalDetails(member);
String username = member.getLoginId();
long expiryTime = System.currentTimeMillis() + (TOKEN_VALIDITY_SECONDS * 1000);
String signature = generateSignature(username, expiryTime);
String tokenValue = encodeToken(username, expiryTime, signature);
SecurityContextHolder.getContext().setAuthentication(successfulAuthentication);
Cookie rememberMeCookie = new Cookie(REMEMBER_ME_COOKIE_NAME, tokenValue);
rememberMeCookie.setPath("/");
rememberMeCookie.setMaxAge((int) TOKEN_VALIDITY_SECONDS);
rememberMeCookie.setHttpOnly(true);
rememberMeCookie.setSecure(request.isSecure());
response.addCookie(rememberMeCookie);
} }
@Override @Override
public void loginFail(HttpServletRequest request, HttpServletResponse response) { public void loginFail(HttpServletRequest request, HttpServletResponse response) {
// 로그인 실패 처리 Cookie cookie = new Cookie(REMEMBER_ME_COOKIE_NAME, null);
cookie.setPath("/");
cookie.setMaxAge(0);
response.addCookie(cookie);
} }
private Cookie getRememberMeCookie(HttpServletRequest request) { private Cookie getRememberMeCookie(HttpServletRequest request) {
Cookie[] cookies = request.getCookies(); if (request.getCookies() == null) {
if (cookies == null) {
return null; return null;
} }
for (Cookie cookie : request.getCookies()) {
for(Cookie cookie : cookies) { if (REMEMBER_ME_COOKIE_NAME.equals(cookie.getName())) {
if(SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY.equals(cookie.getName())) {
return cookie; return cookie;
} }
} }
return null; return null;
} }
private String decodeCookie(String cookieValue) { private String[] decodeAndSplitCookie(String cookieValue) {
try { // Base64 디코딩 try {
return new String(Base64.getDecoder().decode(cookieValue)); String decodedValue = new String(Base64.getDecoder().decode(cookieValue), StandardCharsets.UTF_8);
} catch (IllegalArgumentException e) { return decodedValue.split(DELIMITER);
} catch (Exception e) {
log.error("Failed to decode remember-me cookie", e); log.error("Failed to decode remember-me cookie", e);
return null; return null;
} }
} }
private String generateRememberMeToken(String username) { private String encodeToken(String username, long expiryTime, String signature) {
return Base64.getEncoder().encodeToString(username.getBytes()); String tokenValue = username + DELIMITER + expiryTime + DELIMITER + signature;
return Base64.getEncoder().encodeToString(tokenValue.getBytes(StandardCharsets.UTF_8));
} }
private int getTokenValiditySeconds() { private boolean isTokenValid(String username, long expiryTime, String signature) {
return 60 * 60 * 24 * 365; 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

@ -1,10 +1,18 @@
package io.company.localhost.controller.common; package io.company.localhost.controller.common;
import static org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices.*; import io.company.localhost.common.annotation.Admin;
import io.company.localhost.common.annotation.Guest;
import java.util.HashMap; import io.company.localhost.common.annotation.Member;
import java.util.Map; import io.company.localhost.common.response.ApiResponse;
import io.company.localhost.utils.AuthUtil;
import io.company.localhost.utils.SessionListener;
import io.company.localhost.vo.MemberVo;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.RememberMeAuthenticationToken; import org.springframework.security.authentication.RememberMeAuthenticationToken;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.annotation.AuthenticationPrincipal;
@ -16,16 +24,10 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import io.company.localhost.common.response.ApiResponse; import java.util.HashMap;
import io.company.localhost.utils.AuthUtil; import java.util.Map;
import io.company.localhost.utils.SessionListener;
import io.company.localhost.vo.MemberVo; import static org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
@RestController @RestController
@ -72,7 +74,7 @@ public class UserController {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
boolean remember = false; boolean remember = false;
if (authentication != null && authentication instanceof RememberMeAuthenticationToken) { if (authentication instanceof RememberMeAuthenticationToken) {
remember = true; remember = true;
} }
// 쿠키 확인 // 쿠키 확인
@ -94,6 +96,7 @@ public class UserController {
//로그아웃 //로그아웃
@Guest
@GetMapping("/logout") @GetMapping("/logout")
public ApiResponse<String> logout(HttpServletRequest request, HttpServletResponse response) { public ApiResponse<String> logout(HttpServletRequest request, HttpServletResponse response) {
String returnMessage = "Successfully logged out"; String returnMessage = "Successfully logged out";
@ -117,4 +120,22 @@ public class UserController {
} }
@Guest
@GetMapping("get1")
public ApiResponse<?> getAuthTest1() {
return ApiResponse.ok(AuthUtil.getUser());
}
@Member
@GetMapping("get2")
public ApiResponse<?> getAuthTest2() {
return ApiResponse.ok(AuthUtil.getUser());
}
@Admin
@GetMapping("get3")
public ApiResponse<?> getAuthTest3() {
return ApiResponse.ok(AuthUtil.getUser());
}
} }