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

[김윤식] 프리코스 미션 제출합니다. #11

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
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
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,29 @@
# spring-security-authentication

## 기능 요구사항
1. 아이디와 비밀번호 기반 로그인 구현
- Basic 인증 구현
- POST /login을 통해 로그인을 한다.
- 아이디/패스워드를 확인하여 인증한다
- 인증에 성공하면 Session에 인증정보를 저장한다.
- Logniest의 모든 테스트를 통과하여야 한다.

2. member 목록 조회 기능 구현
- GET /member를 통해 멤버 목록을 조회한다.
- 조회 시 유저 인증이 되어있는지 먼저 확인힌다.
- Basic 인증을 이용하여 사용자를 식별한다.
- Authorization 헤더에서 Basic 인증정보를 추출하여 식별한다
- 인증 성공 시 Session에 인증정보를 저장한다.
- MemberTest의 모든 테스트를 통과하여야 한다.

3. 인증관련 로직을 Interceptor에서 구현
- 인증 관련 로직을 Controller클래스에서 분리한다.
- 두 인증 방식을 모두 인터셉터에서 처리되도록 구현한다.
- 하나의 인터페이스는 하나의 작업만 수행하도록 설계한다.
- 기존 테스트케이스를 모두 통과하여야 한다.

4. 인증 로직과 서비스 로직간의 패키지 분리
- 서비스 관련 코드는 app, 인증 관련 코드는 security에 위치시킨다.
- 패키지간의 양방향 참조는 단방향으로 리팩토링한다.
- 인증 관련 작업은 security에서 전담하며, 서비스 로직이 인증 로직에 의존하지 않게 만든다.
- 모든 테스트케이스는 지속적으로 통과하여야 한다.
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ repositories {

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.apache.commons:commons-lang3:3.17.0'
testImplementation 'org.springframework.boot:spring-boot-starter-test'

compileOnly "org.projectlombok:lombok"
testCompileOnly "org.projectlombok:lombok"
annotationProcessor "org.projectlombok:lombok"
testAnnotationProcessor "org.projectlombok:lombok"
}

tasks.named('test') {
Expand Down
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
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 lombok.RequiredArgsConstructor;
import nextstep.security.constants.SecurityConstants;
import nextstep.security.interceptor.BasicAuthInterceptor;
import nextstep.security.interceptor.UsernamePasswordInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final UsernamePasswordInterceptor usernamePasswordInterceptor;
private final BasicAuthInterceptor basicAuthInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
// registry.addInterceptor(usernamePasswordInterceptor).addPathPatterns(SecurityConstants.LOGIN_URL);
// registry.addInterceptor(basicAuthInterceptor).excludePathPatterns(SecurityConstants.LOGIN_URL);

}
}
27 changes: 6 additions & 21 deletions src/main/java/nextstep/app/domain/Member.java
Original file line number Diff line number Diff line change
@@ -1,31 +1,16 @@
package nextstep.app.domain;

import lombok.Data;
import nextstep.security.model.UserDetails;

@Data
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;
public UserDetails getUserDetails() {
return new UserDetails(this.email, this.password);
}
}
16 changes: 3 additions & 13 deletions src/main/java/nextstep/app/ui/LoginController.java
Original file line number Diff line number Diff line change
@@ -1,27 +1,17 @@
package nextstep.app.ui;

import nextstep.app.domain.MemberRepository;
import lombok.RequiredArgsConstructor;
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;

@RequiredArgsConstructor
@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) {
public ResponseEntity<Void> login() {
return ResponseEntity.ok().build();
}

Expand Down
14 changes: 10 additions & 4 deletions src/main/java/nextstep/app/ui/MemberController.java
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
package nextstep.app.ui;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import nextstep.app.domain.Member;
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.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@Slf4j
@RequiredArgsConstructor
@RestController
public class MemberController {

private final MemberRepository memberRepository;

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

@GetMapping("/members")
public ResponseEntity<List<Member>> list() {
List<Member> members = memberRepository.findAll();
return ResponseEntity.ok(members);
}

@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<Void> handleAuthenticationException() {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
Binary file added src/main/java/nextstep/security/.DS_Store
Binary file not shown.
37 changes: 37 additions & 0 deletions src/main/java/nextstep/security/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package nextstep.security.config;

import lombok.RequiredArgsConstructor;
import nextstep.security.filters.*;
import nextstep.security.service.UserDetailService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;

@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

private final UserDetailService userDetailService;

@Bean
public DelegatingFilterProxy delegatingFilterProxy() {
return new DelegatingFilterProxy(filterChainProxy(List.of(securityFilterChain())));
}

@Bean
public SecurityFilterChain securityFilterChain() {
return new DefaultSecurityFilterChain(
List.of(
new SecurityContextHolderFilter(),
new UsernamePasswordAuthFilter(userDetailService),
new BasicAuthenticationFilter(userDetailService)
)
);
}


private FilterChainProxy filterChainProxy(List<SecurityFilterChain> securityFilterChains) {
return new FilterChainProxy(securityFilterChains);
}
}
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 SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT";
public static final String USERNAME = "username";
public static final String PASSWORD = "password";
public static final String LOGIN_URL = "/login";
public static final String AUTHENTICATION_SCHEME_BASIC = "Basic ";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package nextstep.security.context;

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

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

public class HttpSessionSecurityContextRepository {
public SecurityContext loadContext(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null) {
return null;
}
return (SecurityContext) session.getAttribute(SPRING_SECURITY_CONTEXT_KEY);
}
public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
HttpSession session = request.getSession();
session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, context);
}
}
15 changes: 15 additions & 0 deletions src/main/java/nextstep/security/context/SecurityContext.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package nextstep.security.context;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import nextstep.security.model.SecurityAuthentication;

import java.io.Serializable;

@AllArgsConstructor
@NoArgsConstructor
@Data
public class SecurityContext implements Serializable {
private SecurityAuthentication securityAuthentication;
}
30 changes: 30 additions & 0 deletions src/main/java/nextstep/security/context/SecurityContextHolder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package nextstep.security.context;

public class SecurityContextHolder {
private static ThreadLocal<SecurityContext> contextHolder;

static {
contextHolder = new ThreadLocal<>();
}

public static SecurityContext getContext() {
SecurityContext ctx = contextHolder.get();
if (ctx == null) {
ctx = createEmptyContext();
contextHolder.set(ctx);
}
return ctx;
}
public static void setContext(SecurityContext context) {
if (context != null) {
contextHolder.set(context);
}
}
public static SecurityContext createEmptyContext() {
return new SecurityContext();
}

public static void clearContext() {
contextHolder.remove();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package nextstep.security.credential;

import lombok.Data;
import nextstep.security.model.SecurityAuthentication;

@Data
public class UsernamePasswordAuthenticationToken implements SecurityAuthentication {
private final Object principal;
private final Object credentials;
private final boolean 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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package nextstep.security.filters;

import nextstep.app.ui.AuthenticationException;
import nextstep.security.constants.SecurityConstants;
import nextstep.security.credential.UsernamePasswordAuthenticationToken;
import nextstep.security.model.SecurityAuthentication;
import nextstep.security.provider.AuthenticationManager;
import nextstep.security.provider.ProviderManager;
import nextstep.security.provider.UsernameProvider;
import nextstep.security.service.UserDetailService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.List;

public class BasicAuthenticationFilter extends OncePerRequestFilter {
private final AuthenticationManager authenticationManager;

public BasicAuthenticationFilter(UserDetailService userDetailsService) {
this.authenticationManager = new ProviderManager(
List.of(new UsernameProvider(userDetailsService))
);
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
SecurityAuthentication authentication = convert(request);
if (authentication == null) {
filterChain.doFilter(request, response);
return;
}
this.authenticationManager.authenticate(authentication);
filterChain.doFilter(request, response);
} catch (Exception e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
}

private SecurityAuthentication convert(HttpServletRequest request) {
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (header == null) {
return null;
}

header = header.trim();
if (!StringUtils.startsWithIgnoreCase(header, SecurityConstants.AUTHENTICATION_SCHEME_BASIC)) {
return null;
}
if (header.equalsIgnoreCase(SecurityConstants.AUTHENTICATION_SCHEME_BASIC)) {
throw new AuthenticationException();
}

byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8);
byte[] decoded = decode(base64Token);
String token = new String(decoded, StandardCharsets.UTF_8);
int delim = token.indexOf(":");
if (delim == -1) {
throw new AuthenticationException();
}

return UsernamePasswordAuthenticationToken
.unauthenticated(token.substring(0, delim), token.substring(delim + 1));
}

private byte[] decode(byte[] base64Token) {
try {
return Base64.getDecoder().decode(base64Token);
} catch (IllegalArgumentException ex) {
throw new AuthenticationException();
}
}
}
Loading