Skip to content
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

[박수빈/김세영] 프리코스 미션 제출합니다. #12

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
68 changes: 68 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,69 @@
# spring-security-authentication

## 인증과 서비스 로직간의 분리

- [x] 인증은 security 패키지에 위치해야한다.
- [x] 서비스는 app 패키지에 위치해야한다.

## 기능 요구 사항

1. 아이디와 비밀번호 기반의 로그인 기능 구현
2. Basic 인증을 사용하여 사용자를 식별할 수 있도록 스프링 프래임워크를 통한 웹 앱 구현

### 아이디와 비밀번호 기반 로그인 구현

- [x] 사용자가 입력한 아이디와 비밀번호를 확인하여 인증한다.
- [x] 로그인 성공시 Session 을 사용하여 인증 정보를 저장한다.
- [x] LoginTest 의 모든 테스트가 성공해야한다.

### Basic 인증 구현

- [x] 요청의 Authorization 헤더에 Basic 인증 정보를 추출 하여 인증을 추출한다.
- [x] 인증을 성공한 경우 Session 을 사용하여 인증 정보를 저장한다.
- [x] MemberTest 의 모든 테스트가 통과해야한다.

## 프로그래밍 요구사항

- [x] 자바 코드 컨벤션을 준수한다.
- [x] 들여쓰기는 depth 가 3 이 넘지 않도록 한다.
- [x] 3항 연산자를 사용하지 않는다.
- [x] 함수의 길이가 15 라인을 넘지 않도록 한다.
- [x] else 예약어를 사용하지 않는다.
- [x] 정리한 기능 목록이 정상적으로 동작하는지 테스트 코드를 구현한다.

# 페어 코딩

- `SecurityFilterChain` : 보안 필터의 묶음을 정의하는 인터페이스
- `DefaultSecurityFilterChain` : `SecurityFilterChain` 의 구현체
- `FilterChainProxy` : 보안 필터의 묶음을 관리하는 객체
- `DelegatingFilterProxy` : 스프링에 등록한 필터를 실행을 위임할 객체

패키지 분리

## 1. Interceptor 에서 Filter 로 변경하기

### `GenericFilterBean` 와 `OncePerRequestFilter` 의 차이점

실행 횟수: `GenericFilterBean` 은 요청마다 실행될 수 있지만, `OncePerRequestFilter` 는 요청당 한 번만 실행됩니다.
사용 목적: 요청별로 한 번만 실행되어야 하는 필터링 로직에는 `OncePerRequestFilter` 가 적합하며, 그렇지 않으면 `GenericFilterBean` 을 사용할
수 있습니다.

- [x] `BasicAuthorizationInterceptor` -> `BasicAuthenticationFilter`
- [x] `FormLoginAuthorizationInterceptor` -> `UsernamePasswordAuthenticationFilter`
- [x] `WebMvcConfigurer` -> `SecurityConfig` 로 변경

## 2. AuthenticationManager 로 인증 추상화 하기

- [x] `AuthenticationManager` 구현
- [x] `ProviderManager` 구현
- [x] `AuthenticationProvider` 구현
- [x] `DaoAuthenticationProvider` 구현

## SecurityContextHolder 로 인증 정보 객체 저장하기

- [x] `SecurityContext` 구현
- [x] `SecurityContextHolder` 구현
- [x] seesion 에서 `SecurityContext` 로 인증 정보 저장하도록 변경

## SecurityContextHolderFilter 구현하기

45 changes: 45 additions & 0 deletions src/main/java/nextstep/app/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package nextstep.app.config;

import java.util.List;
import nextstep.security.BasicAuthenticationFilter;
import nextstep.security.UsernamePasswordAuthenticationFilter;
import nextstep.security.authentication.AuthenticationManager;
import nextstep.security.filter.DefaultSecurityFilterChain;
import nextstep.security.filter.DelegatingFilterProxy;
import nextstep.security.filter.FilterChainProxy;
import nextstep.security.filter.SecurityFilterChain;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SecurityConfig {

private final AuthenticationManager authenticationManager;

public SecurityConfig(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}

@Bean
public SecurityFilterChain securityFilterChain() {
return new DefaultSecurityFilterChain(
List.of(
new BasicAuthenticationFilter(authenticationManager),
new UsernamePasswordAuthenticationFilter(authenticationManager)
)
);
}

@Bean
public FilterChainProxy filterChainProxy(List<SecurityFilterChain> securityFilterChains) {
// 여러 개의 시큐리티 필터 체인을 목록으로 가진다.
return new FilterChainProxy(securityFilterChains);
}

@Bean
public DelegatingFilterProxy delegatingFilterProxy() {
// 필터 체인 프록시에게 위임하는 역할을 한다.
return new DelegatingFilterProxy(filterChainProxy(List.of(securityFilterChain())));
}

}
32 changes: 32 additions & 0 deletions src/main/java/nextstep/app/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package nextstep.app.config;

import java.util.List;
import nextstep.app.service.UserDetailService;
import nextstep.security.authentication.AuthenticationManager;
import nextstep.security.authentication.AuthenticationProvider;
import nextstep.security.authentication.DaoAuthenticationProvider;
import nextstep.security.authentication.ProviderManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

private final UserDetailService userDetailService;

public WebConfig(UserDetailService userDetailService) {
this.userDetailService = userDetailService;
}

@Bean
public AuthenticationManager authenticationManager() {
return new ProviderManager(authenticationProviders());
}

@Bean
public List<AuthenticationProvider> authenticationProviders() {
return List.of(new DaoAuthenticationProvider(userDetailService));
}

}
20 changes: 20 additions & 0 deletions src/main/java/nextstep/app/service/UserDetail.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package nextstep.app.service;

public class UserDetail {

private final String email;
private final String password;

public UserDetail(String email, String password) {
this.email = email;
this.password = password;
}

public String getEmail() {
return email;
}

public String getPassword() {
return password;
}
}
21 changes: 21 additions & 0 deletions src/main/java/nextstep/app/service/UserDetailService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package nextstep.app.service;

import nextstep.app.domain.MemberRepository;
import org.springframework.stereotype.Service;

@Service
public class UserDetailService {

private final MemberRepository memberRepository;

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

public UserDetail findUserDetail(String email) {
return memberRepository.findByEmail(email)
.map(member -> new UserDetail(member.getEmail(), member.getPassword()))
.orElse(null);
}

}
13 changes: 2 additions & 11 deletions src/main/java/nextstep/app/ui/LoginController.java
Original file line number Diff line number Diff line change
@@ -1,24 +1,15 @@
package nextstep.app.ui;

import nextstep.app.domain.MemberRepository;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
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;

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<Void> login(HttpServletRequest request, HttpSession session) {
Expand Down
107 changes: 107 additions & 0 deletions src/main/java/nextstep/security/BasicAuthenticationFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package nextstep.security;

import static nextstep.security.SecurityConstants.BASIC_TOKEN_PREFIX;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import nextstep.app.ui.AuthenticationException;
import nextstep.security.authentication.Authentication;
import nextstep.security.authentication.AuthenticationManager;
import nextstep.security.authentication.UsernamePasswordAuthenticationToken;
import nextstep.security.context.SecurityContext;
import nextstep.security.context.SecurityContextHolder;
import org.springframework.http.HttpHeaders;
import org.springframework.util.Base64Utils;
import org.springframework.web.filter.OncePerRequestFilter;

public class BasicAuthenticationFilter extends OncePerRequestFilter {

private final AuthenticationManager authenticationManager;

private final List<String> ACCEPTED_URIS = List.of(
"/members"
);

public BasicAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
if (!ACCEPTED_URIS.contains(request.getRequestURI())) {
filterChain.doFilter(request, response);
return;
}

try {
checkAuthentication(request);
filterChain.doFilter(request, response);
} catch (Exception ex) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
SecurityContextHolder.clearContext();
}
}

private void checkAuthentication(HttpServletRequest request) {
String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);

validateBasicToken(authorization);

String decodedToken = decodeToken(authorization);

Authentication authentication = authenticationManager.authenticate(
createAuthentication(decodedToken));

validateAuthentication(authentication);

SecurityContext ctx = SecurityContextHolder.createEmptyContext();
ctx.setAuthentication(authentication);
SecurityContextHolder.setContext(ctx);
}

private void validateBasicToken(String authorization) {
if (authorization == null) {
throw new AuthenticationException();
}

if (!authorization.startsWith(BASIC_TOKEN_PREFIX)) {
throw new AuthenticationException();
}
}

private String decodeToken(String authorization) {
String encodedToken = authorization.substring(BASIC_TOKEN_PREFIX.length());

if (encodedToken.isBlank()) {
throw new AuthenticationException();
}
return new String(Base64Utils.decodeFromString(encodedToken), StandardCharsets.UTF_8);
}

private Authentication createAuthentication(String decodedToken) {
String[] emailAndPassword = decodedToken.split(":");

if (emailAndPassword.length != 2) {
throw new AuthenticationException();
}

return UsernamePasswordAuthenticationToken.unauthenticated(emailAndPassword[0],
emailAndPassword[1]);
}

private void validateAuthentication(Authentication authentication) {
if (authentication == null) {
throw new AuthenticationException();
}

if (!authentication.isAuthenticated()) {
throw new AuthenticationException();
}
}
}
13 changes: 13 additions & 0 deletions src/main/java/nextstep/security/SecurityConstants.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package nextstep.security;

public class SecurityConstants {

private SecurityConstants() {
throw new IllegalStateException("Utility class");
}

public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT";

public static final String BASIC_TOKEN_PREFIX = "Basic ";

}
Loading