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

[이준현] 프리코스 미션 제출합니다. #15

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

## 기능 요구 사항

1. 아이디 비밀번호 기반 로그인 기능 구현
2. Basic 인증 및 사용자를 식별하는 기능 구현

## 구현 기능 목록

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

1.사용자가 입력한 아이디와 비밀번호를 바탕으로 사용자 정보를 읽어 온 후 인증
2.로그인 성공 시 Session에 인증 정보를 저장

### Basic 인증 구현

1. Basic Token을 디코딩하는 기능
2. 디코딩된 내용을 바탕으로 사용자를 식별하는 기능

### 리팩토링 사항

인증 로직과 서비스 로직 사이의 패키지 분리
패키지 사이의 의존성이 단반향으로 흐르도록 변경


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
28 changes: 28 additions & 0 deletions src/main/java/nextstep/app/application/UserDetailServiceImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package nextstep.app.application;

import nextstep.app.domain.Member;
import nextstep.app.domain.MemberRepository;
import nextstep.security.userdetail.UserDetail;
import nextstep.security.userdetail.UserDetailService;
import org.springframework.stereotype.Service;

@Service
public class UserDetailServiceImpl implements UserDetailService {

private final MemberRepository memberRepository;

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

@Override
public UserDetail getUserDetail(String username) {
return memberRepository.findByEmail(username)
.map(this::convertToUserDetail)
.orElse(null);
}

public UserDetail convertToUserDetail(Member member) {
return new UserDetail(member.getEmail(), member.getPassword());
}
}
29 changes: 29 additions & 0 deletions src/main/java/nextstep/app/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package nextstep.app.config;

import nextstep.security.authentication.BasicAuthInterceptor;
import nextstep.security.authentication.FormLoginAuthInterceptor;
import nextstep.security.userdetail.UserDetailService;
import nextstep.security.util.TokenDecoder;
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 UserDetailService userDetailService;
private final TokenDecoder tokenDecoder;

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

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new FormLoginAuthInterceptor(userDetailService))
.addPathPatterns("/login");
registry.addInterceptor(new BasicAuthInterceptor(tokenDecoder, userDetailService))
.addPathPatterns("/members");
}
}
20 changes: 2 additions & 18 deletions src/main/java/nextstep/app/ui/LoginController.java
Original file line number Diff line number Diff line change
@@ -1,32 +1,16 @@
package nextstep.app.ui;

import nextstep.app.domain.MemberRepository;
import org.springframework.http.HttpStatus;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
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) {
return ResponseEntity.ok().build();
}

@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<Void> handleAuthenticationException() {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package nextstep.security.authentication;

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

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import nextstep.security.exception.AuthenticationException;
import nextstep.security.userdetail.UserDetail;
import nextstep.security.userdetail.UserDetailService;
import nextstep.security.util.TokenDecoder;
import org.springframework.http.HttpHeaders;
import org.springframework.web.servlet.HandlerInterceptor;

public class BasicAuthInterceptor implements HandlerInterceptor {

private final TokenDecoder tokenDecoder;
private final UserDetailService userDetailService;


public BasicAuthInterceptor(TokenDecoder tokenDecoder, UserDetailService userDetailService) {
this.tokenDecoder = tokenDecoder;
this.userDetailService = userDetailService;
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
try {
String token = request.getHeader(HttpHeaders.AUTHORIZATION);

UserDetail decodedUserInfo = tokenDecoder.decodeToken(token);
UserDetail userDetail = userDetailService.getUserDetail(decodedUserInfo.getUsername());

if (!userDetail.verifyPassword(decodedUserInfo.getPassword())) {
throw new AuthenticationException();
}

request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, userDetail);
return true;
} catch (Exception e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package nextstep.security.authentication;

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

import java.util.Objects;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import nextstep.security.exception.AuthenticationException;
import nextstep.security.userdetail.UserDetail;
import nextstep.security.userdetail.UserDetailService;
import org.springframework.util.ObjectUtils;
import org.springframework.web.servlet.HandlerInterceptor;

public class FormLoginAuthInterceptor implements HandlerInterceptor {

private final UserDetailService userDetailService;

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

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
try {
validateParamAndSession(request);

String username = request.getParameter("username");
String password = request.getParameter("password");

UserDetail userDetail = userDetailService.getUserDetail(username);
verifyUserDetail(userDetail, password);
request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, userDetail);

return true;
} catch (Exception e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
}

private void validateParamAndSession(HttpServletRequest request) {
HttpSession session = request.getSession();

String username = request.getParameter("username");
String password = request.getParameter("password");

if (session.getAttribute(SPRING_SECURITY_CONTEXT_KEY) != null) {
session.removeAttribute(SPRING_SECURITY_CONTEXT_KEY);
}

if (ObjectUtils.isEmpty(username) || ObjectUtils.isEmpty(password)) {
throw new AuthenticationException();
}
}

private void verifyUserDetail(UserDetail userDetail, String password) {
if (Objects.isNull(userDetail)) {
throw new AuthenticationException();
}

if (!userDetail.verifyPassword(password)) {
throw new AuthenticationException();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
package nextstep.app.ui;
package nextstep.security.exception;

public class AuthenticationException extends RuntimeException {

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

public class UserDetail {

private final String username;

private final String password;

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

public String getUsername() {
return username;
}

public String getPassword() {
return password;
}

public boolean verifyPassword(String password) {
return this.password.equals(password);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package nextstep.security.userdetail;

public interface UserDetailService {

UserDetail getUserDetail(String username);
}
38 changes: 38 additions & 0 deletions src/main/java/nextstep/security/util/BasicTokenDecoder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package nextstep.security.util;

import static nextstep.security.util.SecurityConstants.BASIC_TOKEN_PREFIX;

import java.nio.charset.StandardCharsets;
import nextstep.security.exception.AuthenticationException;
import nextstep.security.userdetail.UserDetail;
import org.springframework.stereotype.Component;
import org.springframework.util.Base64Utils;

@Component
public class BasicTokenDecoder implements TokenDecoder {

@Override
public UserDetail decodeToken(String token) {
String base64Token = token.substring(BASIC_TOKEN_PREFIX.length());
String decodedToken = new String(Base64Utils.decodeFromString(base64Token),
StandardCharsets.UTF_8);

validateBasicToken(token);

String[] parts = decodedToken.split(":");
if (parts.length != 2) {
throw new AuthenticationException();
}
return new UserDetail(parts[0], parts[1]);
}

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

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

public class SecurityConstants {

private SecurityConstants() {
}

public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT";

public static final String BASIC_TOKEN_PREFIX = "Basic ";
}
8 changes: 8 additions & 0 deletions src/main/java/nextstep/security/util/TokenDecoder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package nextstep.security.util;

import nextstep.security.userdetail.UserDetail;

public interface TokenDecoder {

UserDetail decodeToken(String token);
}