diff --git a/README.md b/README.md index 1e7ba652..8221704c 100644 --- a/README.md +++ b/README.md @@ -1 +1,21 @@ # spring-security-authentication + +기능 요구 사항 + +아이디와 비밀번호를 기반으로 로그인 기능울 구현, Basic 인증을 사용하여 사용자를 식별 할 수 있도록 프레임워크 사용. + +웹앱으로 구현 + +아이디, 비밀번호 기반 로그인 구현 +- POST /login 경로로 로그인 요청 +- 사용자의 아이디 비밀번호르 확인하여 인증 +- 로그인 성공시 session을 사용해 인증 정보 저장 +- LoginTest의 모든 테스트가 통과해야함. + +Basic 인증 구현 +- GET /member 요청 시 사용자 목록을 조회 +- 단 member로 등록되어 있느 ㄴ사용자만 간으하도록 +- 이를 위해 basic 인증을 사용해 사용자 식별 +- Authorization 헤더에서 Basic 인증정보를 추출하여 인증처리 +- 인증 성공 시 session에 에 인증 정보 저장 +- MemberTest의 모든 테스트가 통과 \ No newline at end of file diff --git a/build.gradle b/build.gradle index 99766160..fca42495 100644 --- a/build.gradle +++ b/build.gradle @@ -15,6 +15,9 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' testImplementation 'org.springframework.boot:spring-boot-starter-test' + + compileOnly 'org.projectlombok:lombok:1.18.30' // Use the latest version + annotationProcessor 'org.projectlombok:lombok:1.18.30' } tasks.named('test') { diff --git a/src/main/java/nextstep/app/config/SpringSecurityConfig.java b/src/main/java/nextstep/app/config/SpringSecurityConfig.java new file mode 100644 index 00000000..a5b86bd1 --- /dev/null +++ b/src/main/java/nextstep/app/config/SpringSecurityConfig.java @@ -0,0 +1,54 @@ +package nextstep.app.config; + +import lombok.RequiredArgsConstructor; +import nextstep.security.core.DefaultSecurityFilterChain; +import nextstep.security.core.authentication.AuthenticationManager; +import nextstep.security.core.FilterChainProxy; +import nextstep.security.core.authentication.provider.AbstractUserDetailsAuthenticationProvider; +import nextstep.security.core.userdetails.UserDetailService; +import nextstep.security.filter.BasicTokenAuthenticationFilter; +import nextstep.security.filter.UsernamePasswordAuthenticationFilter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.DelegatingFilterProxy; + +import javax.servlet.Filter; +import java.util.List; + +@Configuration +@RequiredArgsConstructor +public class SpringSecurityConfig { + private final UserDetailService userDetailService; + private AuthenticationManager authenticationManager; + + @Bean + public DelegatingFilterProxy delegatingFilterProxy() { + final List filters = List.of( + new UsernamePasswordAuthenticationFilter(authenticationManager()) + ); + + final List basicTokenFilters = List.of( + new BasicTokenAuthenticationFilter(authenticationManager()) + ); + + FilterChainProxy filterChainProxy = new FilterChainProxy( + List.of( + new DefaultSecurityFilterChain(new String[]{"/members"}, basicTokenFilters), + new DefaultSecurityFilterChain(new String[]{"/login"}, filters) + )); + + return new DelegatingFilterProxy(filterChainProxy); + } + + private AuthenticationManager authenticationManager() { + if (this.authenticationManager == null) { + authenticationManager = new AuthenticationManager(List.of(usernamePasswordAuthenticationProvider())); + } + return this.authenticationManager; + } + + private AbstractUserDetailsAuthenticationProvider usernamePasswordAuthenticationProvider() { + return new AbstractUserDetailsAuthenticationProvider(userDetailService); + } + +} diff --git a/src/main/java/nextstep/app/config/WebConfig.java b/src/main/java/nextstep/app/config/WebConfig.java new file mode 100644 index 00000000..f14c29fa --- /dev/null +++ b/src/main/java/nextstep/app/config/WebConfig.java @@ -0,0 +1,19 @@ +package nextstep.app.config; + +import nextstep.app.domain.MemberService; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + private final MemberService memberService; + + public WebConfig(MemberService memberService) { + this.memberService = memberService; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + } +} diff --git a/src/main/java/nextstep/app/domain/Member.java b/src/main/java/nextstep/app/domain/Member.java index 6cafa9c7..cc52becb 100644 --- a/src/main/java/nextstep/app/domain/Member.java +++ b/src/main/java/nextstep/app/domain/Member.java @@ -1,31 +1,13 @@ package nextstep.app.domain; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter @AllArgsConstructor public class Member { private final String email; private final String password; private final String name; private final String imageUrl; - public Member(String email, String password, String name, String imageUrl) { - this.email = email; - this.password = password; - this.name = name; - this.imageUrl = imageUrl; - } - - public String getEmail() { - return email; - } - - public String getPassword() { - return password; - } - - public String getName() { - return name; - } - - public String getImageUrl() { - return imageUrl; - } } diff --git a/src/main/java/nextstep/app/domain/MemberService.java b/src/main/java/nextstep/app/domain/MemberService.java new file mode 100644 index 00000000..62c510d8 --- /dev/null +++ b/src/main/java/nextstep/app/domain/MemberService.java @@ -0,0 +1,62 @@ +package nextstep.app.domain; + +import nextstep.app.domain.dto.MemberListResponse; +import nextstep.security.core.userdetails.UserDetail; +import nextstep.security.core.userdetails.UserDetailService; +import nextstep.security.exception.AuthErrorCodes; +import nextstep.security.exception.AuthenticationException; +import org.springframework.stereotype.Service; + +import javax.servlet.http.HttpSession; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.Optional; + +@Service +public class MemberService implements UserDetailService { + + private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT"; + private final MemberRepository memberRepo; + + + public MemberService(MemberRepository memberRepo) { + this.memberRepo = memberRepo; + } + + public void login(HttpSession session, String email, String password) { + Member member = findUserByCredential(email, password); + session.setAttribute(SPRING_SECURITY_CONTEXT, member); + } + + public List findAllMembers(){ + List response = new ArrayList<>(); + memberRepo.findAll().stream().map(MemberListResponse::of).forEach(response::add); + return response; + } + + public void validate(String basicToken){ + String decoded = new String(Base64.getDecoder().decode(basicToken.replace("Basic ", ""))); + String email; + String password; + try { + email = decoded.substring(0, decoded.indexOf(":")); + password = decoded.substring(decoded.indexOf(":") + 1); + findUserByCredential(email, password); + } + catch (StringIndexOutOfBoundsException e){ + throw new AuthenticationException(AuthErrorCodes.WRONG_BASIC_TOKEN_FORMAT); + } + } + private Member findUserByCredential(String email, String pw) { + return memberRepo.findByEmail(email) + .filter(user -> user.getPassword().equals(pw)) + .orElseThrow(() -> new AuthenticationException(AuthErrorCodes.UNAUTHORIZED_LOGIN_REQUEST)); + } + + @Override + public UserDetail findUserByUsername(String username) { + Optional member = memberRepo.findByEmail(username); + return member.map(value -> new UserDetail(value.getEmail(), value.getPassword())).orElse(null); + } +} diff --git a/src/main/java/nextstep/app/domain/dto/MemberListResponse.java b/src/main/java/nextstep/app/domain/dto/MemberListResponse.java new file mode 100644 index 00000000..f598c3c0 --- /dev/null +++ b/src/main/java/nextstep/app/domain/dto/MemberListResponse.java @@ -0,0 +1,33 @@ +package nextstep.app.domain.dto; + +import nextstep.app.domain.Member; + +public class MemberListResponse { + private final String email; + private final String name; + private final String imageUrl; + + public MemberListResponse(String email, String name, String imageUrl) { + this.email = email; + this.name = name; + this.imageUrl = imageUrl; + } + + public static MemberListResponse of(Member member){ + return new MemberListResponse( + member.getEmail(), member.getName(), member.getImageUrl() + ); + } + + public String getEmail() { + return email; + } + + public String getName() { + return name; + } + + public String getImageUrl() { + return imageUrl; + } +} diff --git a/src/main/java/nextstep/app/ui/AuthenticationException.java b/src/main/java/nextstep/app/ui/AuthenticationException.java deleted file mode 100644 index f809b6e4..00000000 --- a/src/main/java/nextstep/app/ui/AuthenticationException.java +++ /dev/null @@ -1,4 +0,0 @@ -package nextstep.app.ui; - -public class AuthenticationException extends RuntimeException { -} diff --git a/src/main/java/nextstep/app/ui/BaseController.java b/src/main/java/nextstep/app/ui/BaseController.java new file mode 100644 index 00000000..7ff93c2a --- /dev/null +++ b/src/main/java/nextstep/app/ui/BaseController.java @@ -0,0 +1,14 @@ +package nextstep.app.ui; + +import nextstep.security.exception.AuthenticationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; + +public class BaseController { + + @ExceptionHandler(AuthenticationException.class) + public ResponseEntity handleAuthenticationException(AuthenticationException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getResponseBody()); + } +} diff --git a/src/main/java/nextstep/app/ui/LoginController.java b/src/main/java/nextstep/app/ui/LoginController.java index 0ea94f1b..7635094a 100644 --- a/src/main/java/nextstep/app/ui/LoginController.java +++ b/src/main/java/nextstep/app/ui/LoginController.java @@ -1,9 +1,7 @@ package nextstep.app.ui; -import nextstep.app.domain.MemberRepository; -import org.springframework.http.HttpStatus; +import nextstep.app.domain.MemberService; 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; @@ -11,22 +9,18 @@ import javax.servlet.http.HttpSession; @RestController -public class LoginController { - public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; +public class LoginController extends BaseController { + private final MemberService memberService; - private final MemberRepository memberRepository; - - public LoginController(MemberRepository memberRepository) { - this.memberRepository = memberRepository; + public LoginController(MemberService memberService) { + this.memberService = memberService; } @PostMapping("/login") public ResponseEntity login(HttpServletRequest request, HttpSession session) { + String email = request.getParameter("username"); + String password = request.getParameter("password"); + memberService.login(session, email, password); return ResponseEntity.ok().build(); } - - @ExceptionHandler(AuthenticationException.class) - public ResponseEntity handleAuthenticationException() { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } } diff --git a/src/main/java/nextstep/app/ui/MemberController.java b/src/main/java/nextstep/app/ui/MemberController.java index c8cc74d6..c5a6d597 100644 --- a/src/main/java/nextstep/app/ui/MemberController.java +++ b/src/main/java/nextstep/app/ui/MemberController.java @@ -1,7 +1,7 @@ package nextstep.app.ui; -import nextstep.app.domain.Member; -import nextstep.app.domain.MemberRepository; +import nextstep.app.domain.MemberService; +import nextstep.app.domain.dto.MemberListResponse; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @@ -9,17 +9,17 @@ import java.util.List; @RestController -public class MemberController { +public class MemberController extends BaseController { - private final MemberRepository memberRepository; + private final MemberService memberService; - public MemberController(MemberRepository memberRepository) { - this.memberRepository = memberRepository; + public MemberController(MemberService memberService) { + this.memberService = memberService; } @GetMapping("/members") - public ResponseEntity> list() { - List members = memberRepository.findAll(); + public ResponseEntity> list() { + List members = memberService.findAllMembers(); return ResponseEntity.ok(members); } diff --git a/src/main/java/nextstep/security/core/DefaultSecurityFilterChain.java b/src/main/java/nextstep/security/core/DefaultSecurityFilterChain.java new file mode 100644 index 00000000..366f7773 --- /dev/null +++ b/src/main/java/nextstep/security/core/DefaultSecurityFilterChain.java @@ -0,0 +1,24 @@ +package nextstep.security.core; + +import lombok.RequiredArgsConstructor; + +import javax.servlet.Filter; +import javax.servlet.http.HttpServletRequest; +import java.util.Arrays; +import java.util.List; + +@RequiredArgsConstructor +public class DefaultSecurityFilterChain implements SecurityFilterChain{ + + private final String[] paths; + private final List filters; + @Override + public boolean matches(HttpServletRequest request) { + return Arrays.stream(paths).anyMatch(path -> request.getPathInfo().matches(path)); + } + + @Override + public List getFilters() { + return filters; + } +} diff --git a/src/main/java/nextstep/security/core/FilterChainProxy.java b/src/main/java/nextstep/security/core/FilterChainProxy.java new file mode 100644 index 00000000..224ed101 --- /dev/null +++ b/src/main/java/nextstep/security/core/FilterChainProxy.java @@ -0,0 +1,64 @@ +package nextstep.security.core; + +import nextstep.security.core.SecurityFilterChain; +import org.springframework.web.filter.GenericFilterBean; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.List; + +public class FilterChainProxy extends GenericFilterBean { + private final List filterChains; + + public FilterChainProxy(List filterChains) { + this.filterChains = filterChains; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + List filters = getFilters((HttpServletRequest) request); + + VirtualFilterChain virtualFilterChain = new VirtualFilterChain(chain, filters); + virtualFilterChain.doFilter(request, response); + } + + private List getFilters(HttpServletRequest request) { + for (SecurityFilterChain chain : this.filterChains) { + if (chain.matches(request)) { + logger.info("SecurityChainMatched"+chain.getClass().getName()); + return chain.getFilters(); + } + } + return List.of(); + } + + private static final class VirtualFilterChain implements FilterChain { + + private final FilterChain originalChain; + + private final List additionalFilters; + + private final int size; + + private int currentPosition = 0; + + private VirtualFilterChain(FilterChain chain, List additionalFilters) { + this.originalChain = chain; + this.additionalFilters = additionalFilters; + this.size = additionalFilters.size(); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { + if (this.currentPosition == this.size) { + this.originalChain.doFilter(request, response); + return; + } + this.currentPosition++; + Filter nextFilter = this.additionalFilters.get(this.currentPosition - 1); + nextFilter.doFilter(request, response, this); + } + + } +} \ No newline at end of file diff --git a/src/main/java/nextstep/security/core/SecurityFilterChain.java b/src/main/java/nextstep/security/core/SecurityFilterChain.java new file mode 100644 index 00000000..e9204197 --- /dev/null +++ b/src/main/java/nextstep/security/core/SecurityFilterChain.java @@ -0,0 +1,12 @@ +package nextstep.security.core; + +import javax.servlet.Filter; +import javax.servlet.http.HttpServletRequest; +import java.util.List; + +public interface SecurityFilterChain { + + boolean matches(HttpServletRequest request); + + List getFilters(); +} diff --git a/src/main/java/nextstep/security/core/SecurityPrincipal.java b/src/main/java/nextstep/security/core/SecurityPrincipal.java new file mode 100644 index 00000000..70472936 --- /dev/null +++ b/src/main/java/nextstep/security/core/SecurityPrincipal.java @@ -0,0 +1,14 @@ +package nextstep.security.core; + +public enum SecurityPrincipal { + + USERNAME_PASSWORD_AUTHENTICATION("username password auth"), + BASIC_TOKEN_AUTHENTICATION("basic token auth"); + ; + + final String expression; + + SecurityPrincipal(String expression) { + this.expression = expression; + } +} diff --git a/src/main/java/nextstep/security/core/authentication/Authentication.java b/src/main/java/nextstep/security/core/authentication/Authentication.java new file mode 100644 index 00000000..90a4d21c --- /dev/null +++ b/src/main/java/nextstep/security/core/authentication/Authentication.java @@ -0,0 +1,13 @@ +package nextstep.security.core.authentication; + +import lombok.AllArgsConstructor; +import lombok.Data; +import nextstep.security.core.SecurityPrincipal; + +@AllArgsConstructor +@Data +public class Authentication { + private Object credential; + private SecurityPrincipal principal; + private boolean isAuthenticated; +} diff --git a/src/main/java/nextstep/security/core/authentication/AuthenticationManager.java b/src/main/java/nextstep/security/core/authentication/AuthenticationManager.java new file mode 100644 index 00000000..3d61160b --- /dev/null +++ b/src/main/java/nextstep/security/core/authentication/AuthenticationManager.java @@ -0,0 +1,19 @@ +package nextstep.security.core.authentication; + +import lombok.RequiredArgsConstructor; +import nextstep.security.core.SecurityPrincipal; +import nextstep.security.exception.AuthErrorCodes; +import nextstep.security.exception.AuthenticationException; + +import java.util.List; + +@RequiredArgsConstructor +public class AuthenticationManager { + private final List authenticationProviders; + + public AuthenticationProvider getAuthenticationProvider(SecurityPrincipal principal){ + return authenticationProviders.stream().filter(provider -> provider.supports(principal)).findAny().orElseThrow( + ()-> new AuthenticationException(AuthErrorCodes.UNAUTHORIZED_LOGIN_REQUEST) + ); + } +} diff --git a/src/main/java/nextstep/security/core/authentication/AuthenticationProvider.java b/src/main/java/nextstep/security/core/authentication/AuthenticationProvider.java new file mode 100644 index 00000000..26eb6076 --- /dev/null +++ b/src/main/java/nextstep/security/core/authentication/AuthenticationProvider.java @@ -0,0 +1,11 @@ +package nextstep.security.core.authentication; + +import nextstep.security.core.SecurityPrincipal; +import nextstep.security.exception.AuthenticationException; + +public interface AuthenticationProvider { + Authentication authenticate(Authentication authentication) throws AuthenticationException; + + boolean supports(SecurityPrincipal securityPrincipal); + +} diff --git a/src/main/java/nextstep/security/core/authentication/provider/AbstractUserDetailsAuthenticationProvider.java b/src/main/java/nextstep/security/core/authentication/provider/AbstractUserDetailsAuthenticationProvider.java new file mode 100644 index 00000000..89e689a3 --- /dev/null +++ b/src/main/java/nextstep/security/core/authentication/provider/AbstractUserDetailsAuthenticationProvider.java @@ -0,0 +1,42 @@ +package nextstep.security.core.authentication.provider; + +import lombok.RequiredArgsConstructor; +import nextstep.security.core.SecurityPrincipal; +import nextstep.security.core.authentication.Authentication; +import nextstep.security.core.authentication.AuthenticationProvider; +import nextstep.security.core.userdetails.UserDetail; +import nextstep.security.core.userdetails.UserDetailService; +import nextstep.security.exception.AuthenticationException; + +import java.util.List; + +@RequiredArgsConstructor +public class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider { + + static private final List supports = List.of(SecurityPrincipal.BASIC_TOKEN_AUTHENTICATION, SecurityPrincipal.USERNAME_PASSWORD_AUTHENTICATION); + + private final UserDetailService userDetailService; + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + try { + UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication.getCredential(); + UserDetail user = userDetailService.findUserByUsername(token.getUsername()); + authentication.setAuthenticated(false); + if (user != null && user.getPassword().equals(token.getPassword())) { + authentication.setAuthenticated(true); + } + } + catch (Exception e){ + authentication.setAuthenticated(false); + } + finally { + return authentication; + } + } + + @Override + public boolean supports(SecurityPrincipal securityPrincipal) { + return supports.contains(securityPrincipal); + } +} diff --git a/src/main/java/nextstep/security/core/authentication/provider/BasicTokenAuthenticationToken.java b/src/main/java/nextstep/security/core/authentication/provider/BasicTokenAuthenticationToken.java new file mode 100644 index 00000000..41e90dec --- /dev/null +++ b/src/main/java/nextstep/security/core/authentication/provider/BasicTokenAuthenticationToken.java @@ -0,0 +1,27 @@ +package nextstep.security.core.authentication.provider; + +import nextstep.security.exception.AuthErrorCodes; +import nextstep.security.exception.AuthenticationException; + +import java.util.Arrays; +import java.util.Base64; + +public class BasicTokenAuthenticationToken extends UsernamePasswordAuthenticationToken{ + + + public BasicTokenAuthenticationToken(String basic){ + super(null, null); + try{ + if(basic.startsWith("Basic ")){ + String base64 = basic.replace("Basic ", ""); + String original = new String(Base64.getDecoder().decode(base64)); + this.setUsername(original.split(":")[0]); + this.setPassword(original.split(":")[1]); + } + }catch (Exception e){ + e.printStackTrace(); + throw new AuthenticationException(AuthErrorCodes.WRONG_BASIC_TOKEN_FORMAT); + } + + } +} diff --git a/src/main/java/nextstep/security/core/authentication/provider/UsernamePasswordAuthenticationToken.java b/src/main/java/nextstep/security/core/authentication/provider/UsernamePasswordAuthenticationToken.java new file mode 100644 index 00000000..d61d40c7 --- /dev/null +++ b/src/main/java/nextstep/security/core/authentication/provider/UsernamePasswordAuthenticationToken.java @@ -0,0 +1,14 @@ +package nextstep.security.core.authentication.provider; + + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class UsernamePasswordAuthenticationToken { + + private String username; + private String password; + +} diff --git a/src/main/java/nextstep/security/core/userdetails/UserDetail.java b/src/main/java/nextstep/security/core/userdetails/UserDetail.java new file mode 100644 index 00000000..af15da55 --- /dev/null +++ b/src/main/java/nextstep/security/core/userdetails/UserDetail.java @@ -0,0 +1,12 @@ +package nextstep.security.core.userdetails; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class UserDetail { + private String username; + private String password; + +} diff --git a/src/main/java/nextstep/security/core/userdetails/UserDetailService.java b/src/main/java/nextstep/security/core/userdetails/UserDetailService.java new file mode 100644 index 00000000..02504569 --- /dev/null +++ b/src/main/java/nextstep/security/core/userdetails/UserDetailService.java @@ -0,0 +1,5 @@ +package nextstep.security.core.userdetails; + +public interface UserDetailService { + UserDetail findUserByUsername(String username); +} diff --git a/src/main/java/nextstep/security/exception/AuthErrorCodes.java b/src/main/java/nextstep/security/exception/AuthErrorCodes.java new file mode 100644 index 00000000..7dd5eb24 --- /dev/null +++ b/src/main/java/nextstep/security/exception/AuthErrorCodes.java @@ -0,0 +1,18 @@ +package nextstep.security.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public enum AuthErrorCodes { + UNAUTHORIZED_LOGIN_REQUEST(HttpStatus.UNAUTHORIZED, "Username not exist or Wrong password"), + WRONG_BASIC_TOKEN_FORMAT(HttpStatus.UNAUTHORIZED, "Wrong basic token format"); + + private final HttpStatus statusCode; + private final String message; + + AuthErrorCodes(HttpStatus statusCode, String message) { + this.statusCode = statusCode; + this.message = message; + } +} diff --git a/src/main/java/nextstep/security/exception/AuthenticationException.java b/src/main/java/nextstep/security/exception/AuthenticationException.java new file mode 100644 index 00000000..090910bf --- /dev/null +++ b/src/main/java/nextstep/security/exception/AuthenticationException.java @@ -0,0 +1,20 @@ +package nextstep.security.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +import java.util.Map; + +@Getter +public class AuthenticationException extends RuntimeException{ + private final HttpStatus status; + + public AuthenticationException(AuthErrorCodes exceptions) { + super(exceptions.getMessage()); + this.status = exceptions.getStatusCode(); + } + + public Map getResponseBody() { + return Map.of("detail", this.getMessage()); + } +} diff --git a/src/main/java/nextstep/security/filter/BasicTokenAuthenticationFilter.java b/src/main/java/nextstep/security/filter/BasicTokenAuthenticationFilter.java new file mode 100644 index 00000000..ac4a60ec --- /dev/null +++ b/src/main/java/nextstep/security/filter/BasicTokenAuthenticationFilter.java @@ -0,0 +1,58 @@ +package nextstep.security.filter; + +import lombok.RequiredArgsConstructor; +import nextstep.security.core.SecurityPrincipal; +import nextstep.security.core.authentication.Authentication; +import nextstep.security.core.authentication.AuthenticationManager; +import nextstep.security.core.authentication.provider.BasicTokenAuthenticationToken; +import nextstep.security.exception.AuthErrorCodes; +import nextstep.security.exception.AuthenticationException; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@RequiredArgsConstructor +public class BasicTokenAuthenticationFilter implements Filter { + private final AuthenticationManager authenticationManager; + + private static final SecurityPrincipal PRINCIPAL = SecurityPrincipal.BASIC_TOKEN_AUTHENTICATION; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + HttpServletRequest httpServletRequest = (HttpServletRequest) request; + String basic = httpServletRequest.getHeader("authorization"); + try { + Authentication authentication = new Authentication( + new BasicTokenAuthenticationToken(basic), + SecurityPrincipal.BASIC_TOKEN_AUTHENTICATION, + false + ); + + authentication = authenticationManager.getAuthenticationProvider(PRINCIPAL).authenticate(authentication); + if (authentication.isAuthenticated()) { + chain.doFilter(request, response); + }else{ + HttpServletResponse httpServletResponse = (HttpServletResponse) response; + httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + httpServletResponse.flushBuffer(); + } + }catch (Exception e){ + e.printStackTrace(); + HttpServletResponse httpServletResponse = (HttpServletResponse) response; + httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + httpServletResponse.flushBuffer(); + } + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + Filter.super.init(filterConfig); + } + + @Override + public void destroy() { + Filter.super.destroy(); + } +} diff --git a/src/main/java/nextstep/security/filter/UsernamePasswordAuthenticationFilter.java b/src/main/java/nextstep/security/filter/UsernamePasswordAuthenticationFilter.java new file mode 100644 index 00000000..a5d299e5 --- /dev/null +++ b/src/main/java/nextstep/security/filter/UsernamePasswordAuthenticationFilter.java @@ -0,0 +1,51 @@ +package nextstep.security.filter; + +import lombok.RequiredArgsConstructor; +import nextstep.security.core.authentication.provider.UsernamePasswordAuthenticationToken; +import nextstep.security.core.SecurityPrincipal; +import nextstep.security.core.authentication.Authentication; +import nextstep.security.core.authentication.AuthenticationManager; +import nextstep.security.core.authentication.AuthenticationProvider; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@RequiredArgsConstructor +public class UsernamePasswordAuthenticationFilter implements Filter { + private static final SecurityPrincipal PRINCIPAL = SecurityPrincipal.USERNAME_PASSWORD_AUTHENTICATION; + private final AuthenticationManager manager; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + HttpServletRequest httpServletRequest = (HttpServletRequest) request; + String username = httpServletRequest.getParameter("username"); + String password = httpServletRequest.getParameter("password"); + Authentication usernamePasswordAuth = new Authentication( + new UsernamePasswordAuthenticationToken(username, password), + PRINCIPAL, + false + ); + + AuthenticationProvider provider = manager.getAuthenticationProvider(PRINCIPAL); + Authentication result = provider.authenticate(usernamePasswordAuth); + + if(result.isAuthenticated()) + chain.doFilter(request, response); + + HttpServletResponse httpServletResponse = (HttpServletResponse) response; + httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + httpServletResponse.flushBuffer(); + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + Filter.super.init(filterConfig); + } + + @Override + public void destroy() { + Filter.super.destroy(); + } +}