diff --git a/README.md b/README.md index 1e7ba652..454a7f56 100644 --- a/README.md +++ b/README.md @@ -1 +1,25 @@ # spring-security-authentication + +## 기능 요구 사항 + +1. 아이디 비밀번호 기반 로그인 기능 구현 +2. Basic 인증 및 사용자를 식별하는 기능 구현 + +## 구현 기능 목록 + +### 아이디 비밀번호 기반 로그인 기능 구현 + + 1.사용자가 입력한 아이디와 비밀번호를 바탕으로 사용자 정보를 읽어 온 후 인증 + 2.로그인 성공 시 Session에 인증 정보를 저장 + +### Basic 인증 구현 + + 1. Basic Token을 디코딩하는 기능 + 2. 디코딩된 내용을 바탕으로 사용자를 식별하는 기능 + +### 리팩토링 사항 + + 인증 로직과 서비스 로직 사이의 패키지 분리 + 패키지 사이의 의존성이 단반향으로 흐르도록 변경 + + \ No newline at end of file diff --git a/src/main/java/nextstep/app/SecurityAuthenticationApplication.java b/src/main/java/nextstep/SecurityAuthenticationApplication.java similarity index 93% rename from src/main/java/nextstep/app/SecurityAuthenticationApplication.java rename to src/main/java/nextstep/SecurityAuthenticationApplication.java index 0f8eb47d..1ecd05fe 100644 --- a/src/main/java/nextstep/app/SecurityAuthenticationApplication.java +++ b/src/main/java/nextstep/SecurityAuthenticationApplication.java @@ -1,4 +1,4 @@ -package nextstep.app; +package nextstep; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; diff --git a/src/main/java/nextstep/app/application/UserDetailServiceImpl.java b/src/main/java/nextstep/app/application/UserDetailServiceImpl.java new file mode 100644 index 00000000..4eaaa4ba --- /dev/null +++ b/src/main/java/nextstep/app/application/UserDetailServiceImpl.java @@ -0,0 +1,28 @@ +package nextstep.app.application; + +import nextstep.app.domain.Member; +import nextstep.app.domain.MemberRepository; +import nextstep.security.userdetail.UserDetail; +import nextstep.security.userdetail.UserDetailService; +import org.springframework.stereotype.Service; + +@Service +public class UserDetailServiceImpl implements UserDetailService { + + private final MemberRepository memberRepository; + + public UserDetailServiceImpl(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + @Override + public UserDetail getUserDetail(String username) { + return memberRepository.findByEmail(username) + .map(this::convertToUserDetail) + .orElse(null); + } + + public UserDetail convertToUserDetail(Member member) { + return new UserDetail(member.getEmail(), member.getPassword()); + } +} diff --git a/src/main/java/nextstep/app/ui/LoginController.java b/src/main/java/nextstep/app/ui/LoginController.java index 0ea94f1b..00dd61ef 100644 --- a/src/main/java/nextstep/app/ui/LoginController.java +++ b/src/main/java/nextstep/app/ui/LoginController.java @@ -1,32 +1,16 @@ package nextstep.app.ui; -import nextstep.app.domain.MemberRepository; -import org.springframework.http.HttpStatus; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; - @RestController public class LoginController { - public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; - - private final MemberRepository memberRepository; - - public LoginController(MemberRepository memberRepository) { - this.memberRepository = memberRepository; - } @PostMapping("/login") public ResponseEntity login(HttpServletRequest request, HttpSession session) { return ResponseEntity.ok().build(); } - - @ExceptionHandler(AuthenticationException.class) - public ResponseEntity handleAuthenticationException() { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } } diff --git a/src/main/java/nextstep/security/authentication/Authentication.java b/src/main/java/nextstep/security/authentication/Authentication.java new file mode 100644 index 00000000..ed153308 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/Authentication.java @@ -0,0 +1,10 @@ +package nextstep.security.authentication; + +public interface Authentication { + + Object getCredentials(); + + Object getPrincipal(); + + boolean isAuthenticated(); +} diff --git a/src/main/java/nextstep/security/authentication/AuthenticationManager.java b/src/main/java/nextstep/security/authentication/AuthenticationManager.java new file mode 100644 index 00000000..17fc6760 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/AuthenticationManager.java @@ -0,0 +1,6 @@ +package nextstep.security.authentication; + +public interface AuthenticationManager { + + Authentication authenticate(Authentication authentication); +} diff --git a/src/main/java/nextstep/security/authentication/AuthenticationProvider.java b/src/main/java/nextstep/security/authentication/AuthenticationProvider.java new file mode 100644 index 00000000..2f8ddf25 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/AuthenticationProvider.java @@ -0,0 +1,10 @@ +package nextstep.security.authentication; + +import nextstep.security.exception.AuthenticationException; + +public interface AuthenticationProvider { + + Authentication authenticate(Authentication authentication) throws AuthenticationException; + + boolean supports(Class authentication); +} diff --git a/src/main/java/nextstep/security/authentication/DaoAuthenticationProvider.java b/src/main/java/nextstep/security/authentication/DaoAuthenticationProvider.java new file mode 100644 index 00000000..1ea1638c --- /dev/null +++ b/src/main/java/nextstep/security/authentication/DaoAuthenticationProvider.java @@ -0,0 +1,31 @@ +package nextstep.security.authentication; + +import nextstep.security.exception.AuthenticationException; +import nextstep.security.userdetail.UserDetail; +import nextstep.security.userdetail.UserDetailService; + +public class DaoAuthenticationProvider implements AuthenticationProvider { + + private final UserDetailService userDetailService; + + public DaoAuthenticationProvider(UserDetailService userDetailService) { + this.userDetailService = userDetailService; + } + + @Override + public Authentication authenticate(Authentication authentication) + throws AuthenticationException { + UserDetail userDetail = userDetailService.getUserDetail( + authentication.getPrincipal().toString()); + if (userDetail.verifyPassword(authentication.getCredentials().toString())) { + throw new AuthenticationException(); + } + return UsernamePasswordAuthenticationToken.authenticated(userDetail.getUsername(), + userDetail.getPassword()); + } + + @Override + public boolean supports(Class authentication) { + return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); + } +} diff --git a/src/main/java/nextstep/security/authentication/ProviderManager.java b/src/main/java/nextstep/security/authentication/ProviderManager.java new file mode 100644 index 00000000..b6e40e42 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/ProviderManager.java @@ -0,0 +1,22 @@ +package nextstep.security.authentication; + +import java.util.List; + +public class ProviderManager implements AuthenticationManager { + + private final List authenticationProviders; + + public ProviderManager(List authenticationProviders) { + this.authenticationProviders = authenticationProviders; + } + + @Override + public Authentication authenticate(Authentication authentication) { + for (AuthenticationProvider provider : authenticationProviders) { + if (provider.supports(authentication.getClass())) { + return provider.authenticate(authentication); + } + } + return null; + } +} diff --git a/src/main/java/nextstep/security/authentication/UsernamePasswordAuthenticationToken.java b/src/main/java/nextstep/security/authentication/UsernamePasswordAuthenticationToken.java new file mode 100644 index 00000000..ad983974 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/UsernamePasswordAuthenticationToken.java @@ -0,0 +1,40 @@ +package nextstep.security.authentication; + +public class UsernamePasswordAuthenticationToken implements Authentication { + + private final Object principal; + private final Object credentials; + private final boolean authenticated; + + public UsernamePasswordAuthenticationToken(Object principal, Object credentials, + boolean authenticated) { + this.principal = principal; + this.credentials = credentials; + this.authenticated = authenticated; + } + + public static UsernamePasswordAuthenticationToken unauthenticated(String principal, + String credentials) { + return new UsernamePasswordAuthenticationToken(principal, credentials, false); + } + + public static UsernamePasswordAuthenticationToken authenticated(String principal, + String credentials) { + return new UsernamePasswordAuthenticationToken(principal, credentials, true); + } + + @Override + public Object getCredentials() { + return this.credentials; + } + + @Override + public Object getPrincipal() { + return this.principal; + } + + @Override + public boolean isAuthenticated() { + return this.authenticated; + } +} diff --git a/src/main/java/nextstep/security/authentication/filter/BasicAuthFilter.java b/src/main/java/nextstep/security/authentication/filter/BasicAuthFilter.java new file mode 100644 index 00000000..9e3d08b9 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/filter/BasicAuthFilter.java @@ -0,0 +1,45 @@ +package nextstep.security.authentication.filter; + +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import nextstep.security.authentication.AuthenticationManager; +import nextstep.security.authentication.UsernamePasswordAuthenticationToken; +import nextstep.security.userdetail.UserDetail; +import nextstep.security.util.TokenDecoder; +import org.springframework.http.HttpHeaders; +import org.springframework.web.filter.OncePerRequestFilter; + +public class BasicAuthFilter extends OncePerRequestFilter { + + private final TokenDecoder tokenDecoder; + private final AuthenticationManager authenticationManager; + + public BasicAuthFilter(TokenDecoder tokenDecoder, AuthenticationManager authenticationManager) { + this.tokenDecoder = tokenDecoder; + this.authenticationManager = authenticationManager; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + try { + + String token = request.getHeader(HttpHeaders.AUTHORIZATION); + + UserDetail decodedUserInfo = tokenDecoder.decodeToken(token); + + UsernamePasswordAuthenticationToken authentication = UsernamePasswordAuthenticationToken.unauthenticated( + decodedUserInfo.getUsername(), decodedUserInfo.getPassword()); + + authenticationManager.authenticate(authentication); + + filterChain.doFilter(request, response); + + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + } +} diff --git a/src/main/java/nextstep/security/authentication/filter/FormLoginAuthFilter.java b/src/main/java/nextstep/security/authentication/filter/FormLoginAuthFilter.java new file mode 100644 index 00000000..9b387c9c --- /dev/null +++ b/src/main/java/nextstep/security/authentication/filter/FormLoginAuthFilter.java @@ -0,0 +1,83 @@ +package nextstep.security.authentication.filter; + +import static nextstep.security.util.SecurityConstants.LOGIN_REQUEST_URI; +import static nextstep.security.util.SecurityConstants.SPRING_SECURITY_CONTEXT_KEY; + +import java.io.IOException; +import java.util.Objects; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import nextstep.security.authentication.Authentication; +import nextstep.security.authentication.AuthenticationManager; +import nextstep.security.authentication.UsernamePasswordAuthenticationToken; +import nextstep.security.exception.AuthenticationException; +import nextstep.security.userdetail.UserDetail; +import org.springframework.util.ObjectUtils; +import org.springframework.web.filter.GenericFilterBean; + +public class FormLoginAuthFilter extends GenericFilterBean { + + private final AuthenticationManager authenticationManager; + + public FormLoginAuthFilter(AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, + FilterChain filterChain) throws IOException, ServletException { + try { + var request = (HttpServletRequest) servletRequest; + + if (!Objects.equals(request.getRequestURI(), LOGIN_REQUEST_URI)) { + filterChain.doFilter(servletRequest, servletResponse); + return; + } + + validateParamAndSession(request); + + String username = request.getParameter("username"); + String password = request.getParameter("password"); + + UsernamePasswordAuthenticationToken authentication = UsernamePasswordAuthenticationToken.unauthenticated( + username, password); + + Authentication authenticate = authenticationManager.authenticate(authentication); + + filterChain.doFilter(servletRequest, servletResponse); + } catch (Exception e) { + var response = (HttpServletResponse) servletResponse; + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + } + + private void validateParamAndSession(HttpServletRequest request) { + HttpSession session = request.getSession(); + + String username = request.getParameter("username"); + String password = request.getParameter("password"); + + if (session.getAttribute(SPRING_SECURITY_CONTEXT_KEY) != null) { + session.removeAttribute(SPRING_SECURITY_CONTEXT_KEY); + } + + if (ObjectUtils.isEmpty(username) || ObjectUtils.isEmpty(password)) { + throw new AuthenticationException(); + } + } + + private void verifyUserDetail(UserDetail userDetail, String password) { + if (Objects.isNull(userDetail)) { + throw new AuthenticationException(); + } + + if (!userDetail.verifyPassword(password)) { + throw new AuthenticationException(); + } + } +} diff --git a/src/main/java/nextstep/security/config/DefaultSecurityFilterChain.java b/src/main/java/nextstep/security/config/DefaultSecurityFilterChain.java new file mode 100644 index 00000000..d1d2d112 --- /dev/null +++ b/src/main/java/nextstep/security/config/DefaultSecurityFilterChain.java @@ -0,0 +1,24 @@ +package nextstep.security.config; + +import java.util.List; +import javax.servlet.Filter; +import javax.servlet.http.HttpServletRequest; + +public class DefaultSecurityFilterChain implements SecurityFilterChain { + + private final List filters; + + public DefaultSecurityFilterChain(List filters) { + this.filters = filters; + } + + @Override + public boolean matches(HttpServletRequest request) { + return true; + } + + @Override + public List getFilters() { + return filters; + } +} diff --git a/src/main/java/nextstep/security/config/DelegatingFilterProxy.java b/src/main/java/nextstep/security/config/DelegatingFilterProxy.java new file mode 100644 index 00000000..65be0e89 --- /dev/null +++ b/src/main/java/nextstep/security/config/DelegatingFilterProxy.java @@ -0,0 +1,24 @@ +package nextstep.security.config; + +import java.io.IOException; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import org.springframework.web.filter.GenericFilterBean; + +public class DelegatingFilterProxy extends GenericFilterBean { + + private final Filter delegate; + + public DelegatingFilterProxy(Filter delegate) { + this.delegate = delegate; + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, + FilterChain filterChain) throws IOException, ServletException { + delegate.doFilter(servletRequest, servletResponse, filterChain); + } +} diff --git a/src/main/java/nextstep/security/config/FilterChainProxy.java b/src/main/java/nextstep/security/config/FilterChainProxy.java new file mode 100644 index 00000000..242124d4 --- /dev/null +++ b/src/main/java/nextstep/security/config/FilterChainProxy.java @@ -0,0 +1,73 @@ +package nextstep.security.config; + +import java.io.IOException; +import java.util.List; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import org.springframework.web.filter.GenericFilterBean; + +public class FilterChainProxy extends GenericFilterBean { + + private final List securityFilterChains; + + public FilterChainProxy(List securityFilterChains) { + this.securityFilterChains = securityFilterChains; + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, + FilterChain filterChain) throws IOException, ServletException { + var filters = getFilters(servletRequest); + + VirtualFilterChain virtualFilterChain = new VirtualFilterChain(filterChain, filters); + virtualFilterChain.doFilter(servletRequest, servletResponse); + } + + private List getFilters(ServletRequest servletRequest) { + var request = (HttpServletRequest) servletRequest; + + for (SecurityFilterChain chain : securityFilterChains) { + if (chain.matches(request)) { + return chain.getFilters(); + } + } + return null; + } + + private static final class VirtualFilterChain implements FilterChain { + + private final FilterChain originFilterChain; + + private final List additionalFilters; + + private final int size; + + private int currentPosition; + + private VirtualFilterChain(FilterChain originFilterChain, List additionalFilters) { + this.originFilterChain = originFilterChain; + this.additionalFilters = additionalFilters; + this.size = additionalFilters.size(); + } + + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse) + throws IOException, ServletException { + if (this.currentPosition == this.size) { + this.originFilterChain.doFilter(servletRequest, servletResponse); + return; + } + this.currentPosition++; + + var nexFilter = this.additionalFilters.get(this.currentPosition - 1); + nexFilter.doFilter(servletRequest, servletResponse, this); + + } + } + +} diff --git a/src/main/java/nextstep/security/config/SecurityConfig.java b/src/main/java/nextstep/security/config/SecurityConfig.java new file mode 100644 index 00000000..4d5e3d34 --- /dev/null +++ b/src/main/java/nextstep/security/config/SecurityConfig.java @@ -0,0 +1,49 @@ +package nextstep.security.config; + +import java.util.List; +import nextstep.security.authentication.AuthenticationManager; +import nextstep.security.authentication.DaoAuthenticationProvider; +import nextstep.security.authentication.ProviderManager; +import nextstep.security.authentication.filter.BasicAuthFilter; +import nextstep.security.authentication.filter.FormLoginAuthFilter; +import nextstep.security.userdetail.UserDetailService; +import nextstep.security.util.TokenDecoder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SecurityConfig { + + public SecurityConfig(UserDetailService userDetailService, TokenDecoder tokenDecoder) { + this.userDetailService = userDetailService; + this.tokenDecoder = tokenDecoder; + } + + private final UserDetailService userDetailService; + private final TokenDecoder tokenDecoder; + + @Bean + public DelegatingFilterProxy delegatingFilterProxy( + AuthenticationManager authenticationManager) { + return new DelegatingFilterProxy( + filterChainProxy(List.of(securityFilterChain(authenticationManager)))); + } + + @Bean + public FilterChainProxy filterChainProxy(List securityFilterChains) { + return new FilterChainProxy(securityFilterChains); + } + + @Bean + public SecurityFilterChain securityFilterChain(AuthenticationManager authenticationManager) { + return new DefaultSecurityFilterChain( + List.of(new FormLoginAuthFilter(authenticationManager), + new BasicAuthFilter(tokenDecoder, authenticationManager))); + } + + @Bean + public AuthenticationManager authenticationManager() { + return new ProviderManager(List.of(new DaoAuthenticationProvider(userDetailService))); + } + +} diff --git a/src/main/java/nextstep/security/config/SecurityFilterChain.java b/src/main/java/nextstep/security/config/SecurityFilterChain.java new file mode 100644 index 00000000..39ef43ba --- /dev/null +++ b/src/main/java/nextstep/security/config/SecurityFilterChain.java @@ -0,0 +1,12 @@ +package nextstep.security.config; + +import java.util.List; +import javax.servlet.Filter; +import javax.servlet.http.HttpServletRequest; + +public interface SecurityFilterChain { + + boolean matches(HttpServletRequest request); + + List getFilters(); +} diff --git a/src/main/java/nextstep/app/ui/AuthenticationException.java b/src/main/java/nextstep/security/exception/AuthenticationException.java similarity index 63% rename from src/main/java/nextstep/app/ui/AuthenticationException.java rename to src/main/java/nextstep/security/exception/AuthenticationException.java index f809b6e4..63a51665 100644 --- a/src/main/java/nextstep/app/ui/AuthenticationException.java +++ b/src/main/java/nextstep/security/exception/AuthenticationException.java @@ -1,4 +1,5 @@ -package nextstep.app.ui; +package nextstep.security.exception; public class AuthenticationException extends RuntimeException { + } diff --git a/src/main/java/nextstep/security/userdetail/UserDetail.java b/src/main/java/nextstep/security/userdetail/UserDetail.java new file mode 100644 index 00000000..145058ab --- /dev/null +++ b/src/main/java/nextstep/security/userdetail/UserDetail.java @@ -0,0 +1,25 @@ +package nextstep.security.userdetail; + +public class UserDetail { + + private final String username; + + private final String password; + + public UserDetail(String username, String password) { + this.username = username; + this.password = password; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public boolean verifyPassword(String password) { + return this.password.equals(password); + } +} diff --git a/src/main/java/nextstep/security/userdetail/UserDetailService.java b/src/main/java/nextstep/security/userdetail/UserDetailService.java new file mode 100644 index 00000000..1a6ea929 --- /dev/null +++ b/src/main/java/nextstep/security/userdetail/UserDetailService.java @@ -0,0 +1,6 @@ +package nextstep.security.userdetail; + +public interface UserDetailService { + + UserDetail getUserDetail(String username); +} diff --git a/src/main/java/nextstep/security/util/BasicTokenDecoder.java b/src/main/java/nextstep/security/util/BasicTokenDecoder.java new file mode 100644 index 00000000..68da04cd --- /dev/null +++ b/src/main/java/nextstep/security/util/BasicTokenDecoder.java @@ -0,0 +1,38 @@ +package nextstep.security.util; + +import static nextstep.security.util.SecurityConstants.BASIC_TOKEN_PREFIX; + +import java.nio.charset.StandardCharsets; +import nextstep.security.exception.AuthenticationException; +import nextstep.security.userdetail.UserDetail; +import org.springframework.stereotype.Component; +import org.springframework.util.Base64Utils; + +@Component +public class BasicTokenDecoder implements TokenDecoder { + + @Override + public UserDetail decodeToken(String token) { + String base64Token = token.substring(BASIC_TOKEN_PREFIX.length()); + String decodedToken = new String(Base64Utils.decodeFromString(base64Token), + StandardCharsets.UTF_8); + + validateBasicToken(token); + + String[] parts = decodedToken.split(":"); + if (parts.length != 2) { + throw new AuthenticationException(); + } + return new UserDetail(parts[0], parts[1]); + } + + private void validateBasicToken(String authorization) { + if (authorization == null) { + throw new AuthenticationException(); + } + + if (!authorization.startsWith(BASIC_TOKEN_PREFIX)) { + throw new AuthenticationException(); + } + } +} diff --git a/src/main/java/nextstep/security/util/SecurityConstants.java b/src/main/java/nextstep/security/util/SecurityConstants.java new file mode 100644 index 00000000..f5649bfe --- /dev/null +++ b/src/main/java/nextstep/security/util/SecurityConstants.java @@ -0,0 +1,15 @@ +package nextstep.security.util; + +public class SecurityConstants { + + private SecurityConstants() { + } + + public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; + + public static final String BASIC_TOKEN_PREFIX = "Basic "; + + public static final String LOGIN_REQUEST_URI = "/login"; + + public static final String MEMBER_REQUEST_URI = "/members"; +} diff --git a/src/main/java/nextstep/security/util/TokenDecoder.java b/src/main/java/nextstep/security/util/TokenDecoder.java new file mode 100644 index 00000000..522018df --- /dev/null +++ b/src/main/java/nextstep/security/util/TokenDecoder.java @@ -0,0 +1,8 @@ +package nextstep.security.util; + +import nextstep.security.userdetail.UserDetail; + +public interface TokenDecoder { + + UserDetail decodeToken(String token); +}