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

[최주형] 프리코스 미션 제출합니다. #13

Open
wants to merge 6 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
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,35 @@
# spring-security-authentication

## 기능 요구 사항
- 아이디와 비밀번호를 기반으로 로그인 기능 구현
- Basic 인증 사용
- 스프링 프레임워크를 사용해 웹 앱으로 구현

### 아이디와 비밀번호 기반 로그인 구현
- POST /login 경로로 로그인 요청
- 사용자가 입력한 아이디와 비밀번호를 확인하여 인증
- 로그인 성공 시 Session을 사용하여 인증 정보 저장
- LoginTest 통과


### Basic 인증 구현
- GET /member 요청 시 사용자 목록 조회
- 단, Member로 등록되어 있는 사용자만 가능하도록 한다.
- 이를 위해 Basic 인증을 사용하여 사용자 식별
- 요청의 Authorization 헤더에서 Basic 인증 정보를 추출하여 인증 처리
- 인증 성공 시 Session을 사용하여 인증 정보 저장
- MemberTest 통과

### 인터셉터 분리
- HandlerInterceptor를 사용하여 인증 관련 로직을 Controller 클래스에서 분리
- 앞서 구현한 두 인증 방식 모두 인터셉터에서 처리되도록 구현
- 가급적이면 하나의 인터셉터는 하나의 작업만 수행하도록 한다.

### 인증 로직과 서비스 로직 간의 패키지 분리
- 서비스 코드와 인증 코드를 명확히 분리하여 관리
- 서비스 관련 코드는 app 패키지에 위치시키고, 인증 관련 코드는 security 패키지에 위치
- 리팩터링 과정에서 패키지 간의 양방향 참조가 발생한다면 단방향 참조로 리팩터링
- app 패키지는 security 패키지에 의존할 수 있지만,
- security 패키지는 app 패키지에 의존하지 않도록 한다.
- 인증 관련 작업은 security 패키지에서 전담하도록 설계하여, 서비스 로직이 인증 세부 사항에 의존하지 않게 만든다.
- LoginTest, MemberTest 의 모든 테스트는 지속해서 통과하여야 한다.
12 changes: 12 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ plugins {
id 'org.springframework.boot' version '2.7.1'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
id 'org.jetbrains.kotlin.jvm'
}

group = 'nextstep'
Expand All @@ -15,8 +16,19 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
}

tasks.named('test') {
useJUnitPlatform()
}
compileKotlin {
kotlinOptions {
jvmTarget = "11"
}
}
compileTestKotlin {
kotlinOptions {
jvmTarget = "11"
}
}
5 changes: 5 additions & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
pluginManagement {
plugins {
id 'org.jetbrains.kotlin.jvm' version '1.9.21'
}
}
rootProject.name = 'spring-security-authentication'
23 changes: 23 additions & 0 deletions src/main/java/nextstep/app/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package nextstep.app.config;

import nextstep.security.interceptor.BasicAuthenticationInterceptor;
import nextstep.security.interceptor.FormLoginAuthorizationInterceptor;
import nextstep.security.service.UserDetailsService;
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 UserDetailsService userDetailsService;

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

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new FormLoginAuthorizationInterceptor(userDetailsService)).addPathPatterns("/login");
registry.addInterceptor(new BasicAuthenticationInterceptor(userDetailsService)).addPathPatterns("/members");
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package nextstep.app.domain;
package nextstep.app.domain.member.param;

public class Member {
import nextstep.security.param.UserDetails;

public class Member implements UserDetails {
private final String email;
private final String password;
private final String name;
Expand All @@ -13,10 +15,12 @@ public Member(String email, String password, String name, String imageUrl) {
this.imageUrl = imageUrl;
}

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

@Override
public String getPassword() {
return password;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
package nextstep.app.domain;
package nextstep.app.domain.member.repository;

import nextstep.app.domain.member.param.Member;

import java.util.List;
import java.util.Optional;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package nextstep.app.domain.member.service;

import nextstep.app.domain.member.param.Member;
import nextstep.app.domain.member.repository.MemberRepository;
import nextstep.security.service.UserDetailsService;
import org.springframework.stereotype.Service;

@Service
public class MemberService implements UserDetailsService {

private final MemberRepository memberRepository;

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

@Override
public Member retrieveMemberByEmailAndPassword(String email, String password) {
return memberRepository.findByEmail(email)
.filter(member -> member.getPassword().equals(password))
.orElse(null);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package nextstep.app.infrastructure;

import nextstep.app.domain.Member;
import nextstep.app.domain.MemberRepository;
import nextstep.app.domain.member.param.Member;
import nextstep.app.domain.member.repository.MemberRepository;
import org.springframework.stereotype.Repository;

import java.util.HashMap;
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/nextstep/app/ui/LoginController.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package nextstep.app.ui;

import nextstep.app.domain.MemberRepository;
import nextstep.app.domain.member.repository.MemberRepository;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/nextstep/app/ui/MemberController.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package nextstep.app.ui;

import nextstep.app.domain.Member;
import nextstep.app.domain.MemberRepository;
import nextstep.app.domain.member.param.Member;
import nextstep.app.domain.member.repository.MemberRepository;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package nextstep.security.constants;

public class SecurityConstants {
public static final String BASIC_AUTH_HEADER = "Basic";
public static final String USERNAME = "username";
public static final String PASSWORD = "password";
public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT";

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

import nextstep.security.param.UserDetails;
import nextstep.security.service.UserDetailsService;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

import static nextstep.security.constants.SecurityConstants.BASIC_AUTH_HEADER;

public class BasicAuthenticationInterceptor implements HandlerInterceptor {
private final UserDetailsService userDetailService;

public BasicAuthenticationInterceptor(UserDetailsService userDetailService) {
this.userDetailService = userDetailService;
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (authorizationHeader == null || !authorizationHeader.startsWith(BASIC_AUTH_HEADER)) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "인증되지 않은 사용자입니다.");
return false;
}

String[] values = getBasicValues(authorizationHeader);
if (values.length != 2) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "인증되지 않은 사용자입니다.");
return false;
}

String username = values[0];
String password = values[1];

UserDetails userDetails = userDetailService.retrieveMemberByEmailAndPassword(username, password);
if (userDetails == null) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return false;
}

return true;
}

private String[] getBasicValues(String authorizationHeader) {
String base64Credentials = authorizationHeader.substring(BASIC_AUTH_HEADER.length()).trim();
String credentials = new String(Base64.getDecoder().decode(base64Credentials), StandardCharsets.UTF_8);
return credentials.split(":", 2);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package nextstep.security.interceptor;

import nextstep.security.param.UserDetails;
import nextstep.security.service.UserDetailsService;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import static nextstep.security.constants.SecurityConstants.*;

public class FormLoginAuthorizationInterceptor implements HandlerInterceptor {
private final UserDetailsService userDetailService;

public FormLoginAuthorizationInterceptor(UserDetailsService userDetailService) {
this.userDetailService = userDetailService;
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String username = request.getParameter(USERNAME);
String password = request.getParameter(PASSWORD);

if (username == null || password == null) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "username, password가 필요합니다.");
return false;
}

UserDetails userDetails = userDetailService.retrieveMemberByEmailAndPassword(username, password);
if (userDetails == null) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "username, password가 일치하지 않슴니다.");
return false;
}

request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, userDetails);
return true;
}
}
7 changes: 7 additions & 0 deletions src/main/java/nextstep/security/param/UserDetails.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package nextstep.security.param;

public interface UserDetails {
String getEmail();

String getPassword();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package nextstep.security.service;

import nextstep.security.param.UserDetails;

public interface UserDetailsService {
UserDetails retrieveMemberByEmailAndPassword(String email, String password);
}
3 changes: 1 addition & 2 deletions src/test/java/nextstep/app/LoginTest.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package nextstep.app;

import nextstep.app.domain.Member;
import nextstep.app.domain.member.param.Member;
import nextstep.app.infrastructure.InmemoryMemberRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
Expand All @@ -9,7 +9,6 @@
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions;

import javax.servlet.http.HttpSession;
Expand Down
4 changes: 2 additions & 2 deletions src/test/java/nextstep/app/MemberTest.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package nextstep.app;

import nextstep.app.domain.Member;
import nextstep.app.domain.MemberRepository;
import nextstep.app.domain.member.param.Member;
import nextstep.app.domain.member.repository.MemberRepository;
import nextstep.app.infrastructure.InmemoryMemberRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
Expand Down