Admin , Member annotation 추가
rememberMe 쿠키 저장방식 변경 roleHierarchy 적용
This commit is contained in:
parent
6d5649535f
commit
cc5e87af0c
@ -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 {
|
||||||
|
}
|
||||||
@ -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 {
|
||||||
|
}
|
||||||
@ -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 {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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()))
|
||||||
// 로그인 성공 및 실패 핸들러 설정
|
// 로그인 성공 및 실패 핸들러 설정
|
||||||
@ -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.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()));
|
response.getWriter().write(mapper.writeValueAsString(UserErrorCode.NOT_AUTH_USER.getApiResponse()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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,16 +58,24 @@ 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()
|
||||||
|
.anyMatch(grantedAuthority -> grantedAuthority.getAuthority().equals("ROLE_MEMBER")); // 수정
|
||||||
|
return new AuthorizationDecision(isMember);
|
||||||
|
}else if (requestMappingWrapper.hasAnyAnnotation(request, Admin.class)) {
|
||||||
|
boolean isAdmin = auth.isAuthenticated() &&
|
||||||
|
roleHierarchy.getReachableGrantedAuthorities(auth.getAuthorities()).stream()
|
||||||
|
.anyMatch(grantedAuthority -> grantedAuthority.getAuthority().equals("ROLE_ADMIN")); // 수정
|
||||||
|
return new AuthorizationDecision(isAdmin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
for (RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>> mapping : this.mappings) {
|
for (RequestMatcherEntry<AuthorizationManager<RequestAuthorizationContext>> mapping : this.mappings) {
|
||||||
|
|
||||||
RequestMatcher matcher = mapping.getRequestMatcher();
|
RequestMatcher matcher = mapping.getRequestMatcher();
|
||||||
@ -72,35 +87,35 @@ public class CustomDynamicAuthorizationManager implements AuthorizationManager<R
|
|||||||
new RequestAuthorizationContext(object.getRequest(), matchResult.getVariables()));
|
new RequestAuthorizationContext(object.getRequest(), matchResult.getVariables()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return DENY;
|
}catch (Exception e){
|
||||||
|
ExceptionUtil.messageTrace(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|
||||||
Boolean rememberMe = (Boolean) request.getAttribute(REMEMBER_ME_KEY);
|
|
||||||
if (rememberMe != null && rememberMe) {
|
|
||||||
// Remember-Me 토큰 생성 및 설정
|
|
||||||
Object principal = successfulAuthentication.getPrincipal();
|
Object principal = successfulAuthentication.getPrincipal();
|
||||||
MemberVo member = (MemberVo)principal;
|
if (!(principal instanceof MemberVo)) {
|
||||||
String username = member.getLoginId();
|
return;
|
||||||
String rememberMeToken = generateRememberMeToken(username);
|
}
|
||||||
int tokenValiditySeconds = getTokenValiditySeconds();
|
|
||||||
|
|
||||||
Cookie rememberMeCookie = new Cookie(SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY, rememberMeToken);
|
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.setPath("/");
|
||||||
rememberMeCookie.setMaxAge(tokenValiditySeconds);
|
rememberMeCookie.setMaxAge((int) TOKEN_VALIDITY_SECONDS);
|
||||||
rememberMeCookie.setHttpOnly(true);
|
rememberMeCookie.setHttpOnly(true);
|
||||||
rememberMeCookie.setSecure(request.isSecure());
|
rememberMeCookie.setSecure(request.isSecure());
|
||||||
response.addCookie(rememberMeCookie);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user