Skip to content

[손한비] 프리코스 미션 제출합니다. #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,26 @@
# spring-security-authentication

## 기능 요구 사항
- 아이디/비밀번호 기반 로그인 구현
+ ```POST/login``` 경로로 로그인 요청
+ 사용자가 입력한 아이디와 비밀번호를 확인하여 인증
+ 로그인 성공 시 Session 을 사용하여 인증 정보를 저장
+ ```LoginTest```의 모든 테스트가 통과해야 한다.
- Basic 인증 구현
+ ```GET /member``` 요청 시 사용자 목록을 조회한다.
+ 단, ```Member```로 등록되어있는 사용자만 가능하도록 한다.
+ 이를 위해 Basic 인증을 사용하여 사용자를 식별한다.
+ 요청의 Authorization 헤더에서 Basic 인증 정보를 추출하여 인증을 처리한다.
+ 인증 성공 시 을 사용하여 인증 정보를 저장한다.
+ ```MemberTest```의 모든 테스트가 통과해야 한다.
- 인터셉터 분리
+ ```HandlerInterceptor```를 사용하여 인증 관련 로직을 Controller 클래스에서 분리한다.
- 앞서 구현한 두 인증 방식(아이디 비밀번호 로그인 방식과 Basic 인증 방식) 모두 인터셉터에서 처리되도록 구현한다.
- 가급적이면 하나의 인터셉터는 하나의 작업만 수행하도록 설계한다.
- 인증 로직과 서비스 로직 간의 패키지 분리
+ 서비스 코드와 인증 코드를 명확히 분리하여 관리하도록 한다.
- 서비스 관련 코드는 ```app``` 패키지에 위치시키고, 인증 관련 코드는 ```security``` 패키지에 위치시킨다.
+ 리팩터링 과정에서 패키지 간의 양방향 참조가 발생한다면 단방향 참조로 리팩터링한다.
- ```app``` 패키지는 ```security``` 패키지에 의존할 수 있지만, 반대로 ```security``` 패키지는 ```app``` 패키지에 의존하지 않도록 한다.
+ 인증 관련 작업은 ```security``` 패키지에서 전담하도록 설계하여, 서비스 로직이 인증 세부 사항에 의존하지 않게 만든다.
+ ```LoginTest```와 ```MemberTest```의 모든 테스트는 지속해서 통과해야 한다.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package nextstep.app;
package nextstep;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
Expand Down
27 changes: 27 additions & 0 deletions src/main/java/nextstep/app/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package nextstep.app.config;

import nextstep.security.interceptor.BasicAuthenticationInterceptor;
import nextstep.security.interceptor.FormLoginAuthenticationInterceptor;
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 FormLoginAuthenticationInterceptor formLoginAuthenticationInterceptor;
private final BasicAuthenticationInterceptor basicAuthenticationInterceptor;

public WebConfig(FormLoginAuthenticationInterceptor formLoginAuthenticationInterceptor,
BasicAuthenticationInterceptor basicAuthenticationInterceptor) {
this.formLoginAuthenticationInterceptor = formLoginAuthenticationInterceptor;
this.basicAuthenticationInterceptor = basicAuthenticationInterceptor;
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(basicAuthenticationInterceptor)
.addPathPatterns("/members");

registry.addInterceptor(formLoginAuthenticationInterceptor);
}
}
40 changes: 40 additions & 0 deletions src/main/java/nextstep/app/domain/UserDetailsImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package nextstep.app.domain;

import nextstep.security.domain.UserDetails;

public class UserDetailsImpl implements UserDetails {
private final String email;
private final String password;

private UserDetailsImpl(String email, String password) {
this.email = email;
this.password = password;
}

public static UserDetailsImpl of(Member member) {
return new UserDetailsImpl(member.getEmail(), member.getPassword());
}

public static UserDetailsImpl empty() {
return new UserDetailsImpl(null, null);
}

@Override
public String getUsername() {
return email;
}

@Override
public String getPassword() {
return password;
}

@Override
public boolean isEmpty() {
return email == null && password == null;
}

public boolean verifyPassword(String password) {
return this.password.equals(password);
}
}
29 changes: 29 additions & 0 deletions src/main/java/nextstep/app/domain/UserDetailsServiceImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package nextstep.app.domain;

import nextstep.security.domain.UserDetails;
import nextstep.security.domain.UserDetailsService;
import org.springframework.stereotype.Service;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final MemberRepository memberRepository;

public UserDetailsServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}

@Override
public UserDetails loadUserByUsernameAndPassword(String username, String password) {
return memberRepository.findByEmail(username)
.map(UserDetailsImpl::of)
.filter(userDetails -> userDetails.verifyPassword(password))
.orElse(UserDetailsImpl.empty());
}

@Override
public UserDetails loadUserByUsername(String username) {
return memberRepository.findByEmail(username)
.map(UserDetailsImpl::of)
.orElse(null);
}
}
9 changes: 0 additions & 9 deletions src/main/java/nextstep/app/ui/LoginController.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package nextstep.app.ui;

import nextstep.app.domain.MemberRepository;
import org.springframework.http.HttpStatus;
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;

Expand All @@ -12,8 +10,6 @@

@RestController
public class LoginController {
public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT";

private final MemberRepository memberRepository;

public LoginController(MemberRepository memberRepository) {
Expand All @@ -24,9 +20,4 @@ public LoginController(MemberRepository memberRepository) {
public ResponseEntity<Void> login(HttpServletRequest request, HttpSession session) {
return ResponseEntity.ok().build();
}

@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<Void> handleAuthenticationException() {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
2 changes: 0 additions & 2 deletions src/main/java/nextstep/app/ui/MemberController.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

@RestController
public class MemberController {

private final MemberRepository memberRepository;

public MemberController(MemberRepository memberRepository) {
Expand All @@ -22,5 +21,4 @@ public ResponseEntity<List<Member>> list() {
List<Member> members = memberRepository.findAll();
return ResponseEntity.ok(members);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package nextstep.security.authentication;

public interface Authentication {
Object getCredentials();
Object getPrincipal();
boolean isAuthenticated();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package nextstep.security.authentication;

public interface AuthenticationManager {
Authentication authenticate(Authentication authentication);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package nextstep.security.authentication;

import nextstep.security.exception.AuthenticationException;

public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> authentication);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package nextstep.security.authentication;

import nextstep.security.domain.UserDetails;
import nextstep.security.domain.UserDetailsService;
import nextstep.security.exception.AuthenticationException;

import java.util.Objects;

public class DaoAuthenticationProvider implements AuthenticationProvider {

private final UserDetailsService userDetailsService;

public DaoAuthenticationProvider(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UserDetails userDetails = userDetailsService.loadUserByUsername(authentication.getPrincipal().toString());
if(!Objects.equals(userDetails.getPassword(), authentication.getCredentials())) {
throw new AuthenticationException();
}

return UsernamePasswordAuthenticationToken.authenticated(userDetails.getUsername(), userDetails.getPassword());
}

@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package nextstep.security.authentication;

import java.util.List;

public class ProviderManager implements AuthenticationManager {

private final List<AuthenticationProvider> providers;

public ProviderManager(List<AuthenticationProvider> providers) {
this.providers = providers;
}

@Override
public Authentication authenticate(Authentication authentication) {
for (AuthenticationProvider provider : providers) {
if (provider.supports(authentication.getClass())) {
return provider.authenticate(authentication);
}
}

return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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 credentials;
}

@Override
public Object getPrincipal() {
return principal;
}

@Override
public boolean isAuthenticated() {
return authenticated;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package nextstep.security.config;

import javax.servlet.Filter;
import javax.servlet.http.HttpServletRequest;
import java.util.List;

public class DefaultSecurityFilterChain implements SecurityFilterChain {
private final List<Filter> filters;

public DefaultSecurityFilterChain(List<Filter> filters) {
this.filters = filters;
}

@Override
public boolean matches(HttpServletRequest request) {
return true;
}

@Override
public List<Filter> getFilters() {
return filters;
}
}
19 changes: 19 additions & 0 deletions src/main/java/nextstep/security/config/DelegatingFilterProxy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package nextstep.security.config;

import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.*;
import java.io.IOException;

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);
}
}
59 changes: 59 additions & 0 deletions src/main/java/nextstep/security/config/FilterChainProxy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package nextstep.security.config;

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<SecurityFilterChain> filterChains;

public FilterChainProxy(List<SecurityFilterChain> filterChains) {
this.filterChains = filterChains;
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
List<Filter> filters = getFilters((HttpServletRequest) servletRequest);
VirtualFilterChain virtualFilterChain = new VirtualFilterChain(filterChain, filters);
virtualFilterChain.doFilter(servletRequest, servletResponse);
}

private List<Filter> getFilters(HttpServletRequest request) {
for (SecurityFilterChain filterChain : filterChains) {
if (filterChain.matches(request)) {
return filterChain.getFilters();
}
}

return null;
}

private static final class VirtualFilterChain implements FilterChain {

private final FilterChain originalChain;
private final List<Filter> additionalFilters;

private final int size;
private int currentPosition = 0;

public VirtualFilterChain(FilterChain originalChain, List<Filter> additionalFilters) {
this.originalChain = originalChain;
this.additionalFilters = additionalFilters;
this.size = additionalFilters.size();
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException, ServletException {
if (currentPosition == size) {
originalChain.doFilter(servletRequest, servletResponse);
return;
}
this.currentPosition++;
Filter nextFilter = additionalFilters.get(currentPosition - 1);
nextFilter.doFilter(servletRequest, servletResponse, this);
}
}
}
Loading