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

[노현욱] 프리코스 미션 제출합니다. #16

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,21 @@
# spring-security-authentication

기능 요구 사항

아이디와 비밀번호를 기반으로 로그인 기능울 구현, Basic 인증을 사용하여 사용자를 식별 할 수 있도록 프레임워크 사용.

웹앱으로 구현

아이디, 비밀번호 기반 로그인 구현
- POST /login 경로로 로그인 요청
- 사용자의 아이디 비밀번호르 확인하여 인증
- 로그인 성공시 session을 사용해 인증 정보 저장
- LoginTest의 모든 테스트가 통과해야함.

Basic 인증 구현
- GET /member 요청 시 사용자 목록을 조회
- 단 member로 등록되어 있느 ㄴ사용자만 간으하도록
- 이를 위해 basic 인증을 사용해 사용자 식별
- Authorization 헤더에서 Basic 인증정보를 추출하여 인증처리
- 인증 성공 시 session에 에 인증 정보 저장
- MemberTest의 모든 테스트가 통과
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
54 changes: 54 additions & 0 deletions src/main/java/nextstep/app/config/SpringSecurityConfig.java
Original file line number Diff line number Diff line change
@@ -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<Filter> filters = List.of(
new UsernamePasswordAuthenticationFilter(authenticationManager())
);

final List<Filter> 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);
}

}
19 changes: 19 additions & 0 deletions src/main/java/nextstep/app/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -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) {
}
}
26 changes: 4 additions & 22 deletions src/main/java/nextstep/app/domain/Member.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
62 changes: 62 additions & 0 deletions src/main/java/nextstep/app/domain/MemberService.java
Original file line number Diff line number Diff line change
@@ -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<MemberListResponse> findAllMembers(){
List<MemberListResponse> 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> member = memberRepo.findByEmail(username);
return member.map(value -> new UserDetail(value.getEmail(), value.getPassword())).orElse(null);
}
}
33 changes: 33 additions & 0 deletions src/main/java/nextstep/app/domain/dto/MemberListResponse.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
4 changes: 0 additions & 4 deletions src/main/java/nextstep/app/ui/AuthenticationException.java

This file was deleted.

14 changes: 14 additions & 0 deletions src/main/java/nextstep/app/ui/BaseController.java
Original file line number Diff line number Diff line change
@@ -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());
}
}
22 changes: 8 additions & 14 deletions src/main/java/nextstep/app/ui/LoginController.java
Original file line number Diff line number Diff line change
@@ -1,32 +1,26 @@
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;

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";
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<Void> 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<Void> handleAuthenticationException() {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
16 changes: 8 additions & 8 deletions src/main/java/nextstep/app/ui/MemberController.java
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
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;

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<Member>> list() {
List<Member> members = memberRepository.findAll();
public ResponseEntity<List<MemberListResponse>> list() {
List<MemberListResponse> members = memberService.findAllMembers();
return ResponseEntity.ok(members);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Filter> filters;
@Override
public boolean matches(HttpServletRequest request) {
return Arrays.stream(paths).anyMatch(path -> request.getPathInfo().matches(path));
}

@Override
public List<Filter> getFilters() {
return filters;
}
}
Loading