Skip to content
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
# spring-security-authentication
## ID-PW 기반 로그인 구현
## Basic 인증 구현
## 인터셉터 분리
## 인증 로직 - 서비스 로직 간 패키지 분리:wq
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ repositories {

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@ComponentScan(basePackages = {"nextstep.app", "nextstep.security"})
public class SecurityAuthenticationApplication {

public static void main(String[] args) {
Expand Down
27 changes: 27 additions & 0 deletions src/main/java/nextstep/app/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package nextstep.app.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf().disable() // CSRF 비활성화 (테스트 용도)
.authorizeRequests()
.antMatchers("/login", "/members").permitAll() // /login 경로는 인증 없이 접근 허용
.anyRequest().authenticated(); // 그 외의 요청은 인증 필요
return http.build();
}
}
30 changes: 30 additions & 0 deletions src/main/java/nextstep/app/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package nextstep.app.config;

import nextstep.security.interceptor.BasicAuthInterceptor;
import nextstep.security.interceptor.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
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 LoginInterceptor loginInterceptor;
private final BasicAuthInterceptor basicAuthInterceptor;

@Autowired
public WebConfig(LoginInterceptor loginInterceptor, BasicAuthInterceptor basicAuthInterceptor) {
this.loginInterceptor = loginInterceptor;
this.basicAuthInterceptor = basicAuthInterceptor;
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/login"); // ID/비밀번호 인증에 대한 경로
registry.addInterceptor(basicAuthInterceptor)
.addPathPatterns("/members"); // Basic 인증에 대한 경로
}
}
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.security.service.MemberRepository;
import org.springframework.stereotype.Repository;

import java.util.HashMap;
Expand Down
10 changes: 3 additions & 7 deletions src/main/java/nextstep/app/ui/LoginController.java
Original file line number Diff line number Diff line change
@@ -1,27 +1,23 @@
package nextstep.app.ui;

import nextstep.app.domain.MemberRepository;
import nextstep.security.service.MemberRepository;
import nextstep.security.exception.AuthenticationException;
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) {
public ResponseEntity<Void> login() throws AuthenticationException {
return ResponseEntity.ok().build();
}

Expand Down
2 changes: 1 addition & 1 deletion 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.security.service.MemberRepository;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/nextstep/security/SecurityConstants.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package nextstep.security;

public class SecurityConstants {
public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT";
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package nextstep.app.ui;
package nextstep.security.exception;

public class AuthenticationException extends RuntimeException {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package nextstep.security.interceptor;

import nextstep.app.domain.Member;
import nextstep.security.service.MemberRepository;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

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

import static nextstep.security.SecurityConstants.SPRING_SECURITY_CONTEXT_KEY;

@Component
public class BasicAuthInterceptor implements HandlerInterceptor {

private final MemberRepository memberRepository;

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

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("베이직 인터셉터");
String authHeader = request.getHeader("Authorization");
if (!(authHeader != null && authHeader.startsWith("Basic "))) { // invalid header
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
String base64Credentials = authHeader.substring("Basic ".length()).trim();
String credentials = new String(Base64.getDecoder().decode(base64Credentials), StandardCharsets.UTF_8);
if (!credentials.contains(":")) { // invalid header
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}

final String[] values = credentials.split(":", 2);
String username = values[0];
String password = values[1];
Optional<Member> memberOptional = memberRepository.findByEmail(username);
if (memberOptional.isPresent()) {
Member member = memberOptional.get();

// 1-2. 비밀번호 확인
if (password.equals(member.getPassword())) {
// 1-3. 세션에 인증 정보 저장
HttpSession session = request.getSession();
session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, member);
return true;
}
}
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return false; // 다음으로 요청을 진행
}
}
49 changes: 49 additions & 0 deletions src/main/java/nextstep/security/interceptor/LoginInterceptor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package nextstep.security.interceptor;

import nextstep.app.domain.Member;
import nextstep.security.service.MemberRepository;
import nextstep.security.exception.AuthenticationException;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

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

import static nextstep.security.SecurityConstants.SPRING_SECURITY_CONTEXT_KEY;

@Component
public class LoginInterceptor implements HandlerInterceptor {

private final MemberRepository memberRepository;

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

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("로그인 인터셉터");
String username = request.getParameter("username");
String password = request.getParameter("password");

if (username == null || password == null) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}

Member member = memberRepository.findByEmail(username)
.orElseThrow(AuthenticationException::new);

if (!password.equals(member.getPassword())) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}

// 세션에 인증 정보 저장
HttpSession session = request.getSession();
session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, member);

return true; // 다음으로 요청을 진행
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
package nextstep.app.domain;
package nextstep.security.service;

import nextstep.app.domain.Member;

import java.util.List;
import java.util.Optional;
Expand Down
2 changes: 1 addition & 1 deletion 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.security.service.MemberRepository;
import nextstep.app.infrastructure.InmemoryMemberRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
Expand Down