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

7주차 : 보안 #86

Open
wants to merge 30 commits into
base: tmxhsk99
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
6290b43
build : SpringSecurity 의존성 추가
tmxhsk99 Aug 13, 2023
d0243f6
feat : SpringSecurity 설정 및 필터 추가
tmxhsk99 Aug 13, 2023
e3d1669
refactor : 인증 인터셉터 관련 로직 삭제
tmxhsk99 Aug 13, 2023
0974cbf
7-1 Spring security, 7-2 인증 까지 보고 진행
tmxhsk99 Aug 14, 2023
f92be3b
기존 TestHelper를 사용하는 계층형 테스트 구조로 수정
tmxhsk99 Aug 15, 2023
ac873fb
fix : 잘못된 테스트 이전 로직 제거
tmxhsk99 Aug 16, 2023
efd50a3
feat : claims 가 null인 경우 에러 처리 로직 추가
tmxhsk99 Aug 16, 2023
a3750b6
feat : 수정 삭제 요청시 Authentication 받도록 매개변수 추가
tmxhsk99 Aug 16, 2023
6178cae
fix : 기존에 하드 코딩되어있던 부분을 수정
tmxhsk99 Aug 16, 2023
6bb268a
feat : 메서드와 경로에 맞게 인증 로직을 수행하도록 관련 로직 추가
tmxhsk99 Aug 16, 2023
4cce0b9
refactor : 불필요한 주석 제거
tmxhsk99 Aug 16, 2023
79bc305
test : 유저 패스워드 기능 테스트 추가 및 해당 변경에 따른 테스트 수정
tmxhsk99 Aug 17, 2023
7e94f4b
feat : 유저 패스워드 암호화 기능 추가
tmxhsk99 Aug 17, 2023
58215a5
build : aop 관련 의존성 추가
tmxhsk99 Aug 18, 2023
3a54892
test : 다른 사용자 토큰 픽스쳐추가
tmxhsk99 Aug 18, 2023
9c49112
fix : 주석 직관적으로 수정, 경로 접근권한 수정
tmxhsk99 Aug 18, 2023
cbdcc48
chore : 불필요한 주석 삭제
tmxhsk99 Aug 18, 2023
1beee7a
test : 상품수정 권한 체크 테스트 추가
tmxhsk99 Aug 18, 2023
887bfb2
feat : 본인권한 관련 AOP 및 어노테이션 추가
tmxhsk99 Aug 18, 2023
9c3aa22
feat : 유저 권한 없음 에러 로직 추가
tmxhsk99 Aug 18, 2023
90903bb
test : secret 값이 서로 다르게 사용하던 것을 통일
tmxhsk99 Aug 19, 2023
3576adf
test : 상품 수정 삭제 권한 테스트 추가
tmxhsk99 Aug 19, 2023
1c516dd
feat : 상품정보에 추가한 유저아이디 속성 추가
tmxhsk99 Aug 19, 2023
d1a71ee
fix : 상품정보의 등록자정보를 가져와 비교하도록 로직 수정, 상품아이디 매개변수식 , 여러개 매개변수있어도 적용되도록 수정
tmxhsk99 Aug 19, 2023
b6eb6eb
feat : 상품 삭제, 상품 수정 시 권한 관련 로직 추가
tmxhsk99 Aug 19, 2023
e1d9b4d
chore : 상품생성 시 메서드 인터페이스 변화에 따른 수정
tmxhsk99 Aug 19, 2023
bf63a41
feat : 상품생성 시 생성자 정보도 추가하도록 로직 수정
tmxhsk99 Aug 19, 2023
29f2d12
chore : 상품생성 시 인터페이스 변경에 따른 수정
tmxhsk99 Aug 19, 2023
17b2f05
chore : 상품생성 시 인터페이스 변경에 따른 수정
tmxhsk99 Aug 19, 2023
5cb56c2
test : 테스트 전용 설정 파일 추가
tmxhsk99 Aug 19, 2023
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
6 changes: 6 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ dependencies {
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
// Spring Security
implementation 'org.springframework.boot:spring-boot-starter-security'

// Spring AOP
implementation 'org.springframework:spring-aop'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AOP를 사용하셨군요. 팀원들이랑 협업한다고 가정했을 때, 이러한 기술 도입에 대해서 충분한 설명이 있어야 될 것 같아요.
만약에 팀원들에게 이 기술 도입에 대해서 설득하려고 할 때 뭐라고 하는게 좋을까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일단 현재 상황을 도입 할 만한 상황인지 고려합니다
프로젝트 일정 및 현재 팀원들의 기술수준이 해당 기술을 소화할만큼의 역량이 되는지
팀원들이 긍정적으로 받아들일 사람인지도 고려합니다.
만약에 프로젝트 일정이 충분하고 팀원들의 상태도
해당기술을 사용할 역량 혹은 긍정적으로 받아들일 여부가 된다면
이런 권한 체크에 관한 로직같은경우 비즈니스로직에 공통으로 필요하지만
해당 기능자체가 핵심 비즈니스로직은 아니니 비즈니스로직은 비즈니스로직만 볼수 있게
AOP를 써서 분리하자고 이야기합니다.
그리고 동시에 관련 지식 혹은
실제 적용할 AOP 코드 부분을 현재 적용하려면 이렇게 적용할것이다 및 주의점을 문서로 작성하여 배포합니다
그리고 커피도 한잔씩 사줍니다

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사람과 사람과의 관계가 더 중요하다고 생각하시는군요 ㅎㅎ 저도 동의합니다


}

application {
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/java/com/codesoom/assignment/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@SpringBootApplication
public class App {
Expand All @@ -16,4 +18,9 @@ public static void main(String[] args) {
public Mapper dozerMapper() {
return DozerBeanMapperBuilder.buildDefault();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.codesoom.assignment.aop;

import com.codesoom.assignment.application.ProductService;
import com.codesoom.assignment.domain.Product;
import com.codesoom.assignment.errors.InvalidTokenException;
import com.codesoom.assignment.errors.ProductNotFoundException;
import com.codesoom.assignment.errors.UserNoPermission;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class OwnerCheckAspect {
private final ProductService productService;

public OwnerCheckAspect(ProductService productService) {
this.productService = productService;
}

@Before("@annotation(com.codesoom.assignment.aop.annotation.CheckOwner) && args(id,..)")
public void checkOwner(Long id) throws InvalidTokenException, UserNoPermission, ProductNotFoundException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

authenticationValidate(authentication);

Long loginUserId = (Long) authentication.getPrincipal();

Product product = productService.getProduct(id);

Long createdUserId = product.getCreateUserId();

if (loginUserId != createdUserId) {
throw new UserNoPermission("You do not have permission.");
}
}

private void authenticationValidate(Authentication authentication) {
if (authentication == null) {
throw new InvalidTokenException("AccessToken is Invalid.");
}
if (authentication.getPrincipal().equals("anonymousUser")) {
throw new AccessDeniedException("You do not have permission.");
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.codesoom.assignment.aop.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CheckOwner {
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,30 @@
import com.codesoom.assignment.domain.UserRepository;
import com.codesoom.assignment.errors.LoginFailException;
import com.codesoom.assignment.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class AuthenticationService {
private final UserRepository userRepository;
private final JwtUtil jwtUtil;
private PasswordEncoder passwordEncoder;

public AuthenticationService(UserRepository userRepository,
JwtUtil jwtUtil) {
public AuthenticationService(UserRepository userRepository, JwtUtil jwtUtil, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.jwtUtil = jwtUtil;
this.passwordEncoder = passwordEncoder;
}

public String login(String email, String password) {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new LoginFailException(email));

if (!user.authenticate(password)) {
if (!user.authenticate(password, passwordEncoder)) {
throw new LoginFailException(email);
}

return jwtUtil.encode(1L);
return jwtUtil.encode(user.getId());
}

public Long parseToken(String accessToken) {
Claims claims = jwtUtil.decode(accessToken);
return claims.get("userId", Long.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ public Product getProduct(Long id) {
return findProduct(id);
}

public Product createProduct(ProductData productData) {
public Product createProduct(ProductData productData, Long createUserId) {
Product product = mapper.map(productData, Product.class);
product.setCreateUserId(createUserId);
return productRepository.save(product);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.codesoom.assignment.errors.UserEmailDuplicationException;
import com.codesoom.assignment.errors.UserNotFoundException;
import com.github.dozermapper.core.Mapper;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;
Expand All @@ -17,9 +18,12 @@ public class UserService {
private final Mapper mapper;
private final UserRepository userRepository;

public UserService(Mapper dozerMapper, UserRepository userRepository) {
this.mapper = dozerMapper;
private final PasswordEncoder passwordEncoder;

public UserService(Mapper mapper, UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.mapper = mapper;
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}

public User registerUser(UserRegistrationData registrationData) {
Expand All @@ -29,6 +33,9 @@ public User registerUser(UserRegistrationData registrationData) {
}

User user = mapper.map(registrationData, User.class);

user.changePassword(registrationData.getPassword(),passwordEncoder);

return userRepository.save(user);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package com.codesoom.assignment.config;

import com.codesoom.assignment.filters.AuthenticationErrorFilter;
import com.codesoom.assignment.filters.JwtAuthenticationFilter;
import com.codesoom.assignment.utils.JwtUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;

import javax.servlet.Filter;
import javax.servlet.http.HttpServletRequest;

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityJavaConfig extends WebSecurityConfigurerAdapter {
private final JwtUtil jwtUtil;
public SecurityJavaConfig(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}


@Override
protected void configure(HttpSecurity http) throws Exception {

Filter authenticationFilter = new JwtAuthenticationFilter(authenticationManager(), jwtUtil);
Filter authenticationErrorFilter = new AuthenticationErrorFilter();

http
.csrf().disable()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서 csrf는 왜 disable하는지 이번 기회에 알아보면 좋습니다

.addFilter(authenticationFilter)
.addFilterBefore(authenticationErrorFilter, JwtAuthenticationFilter.class)
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and();

configureAuthorizations(http);

http
.authorizeRequests()
.anyRequest().permitAll()
.and()
.exceptionHandling()
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED));
}

private void configureAuthorizations(HttpSecurity http) throws Exception {
http.authorizeRequests()
.requestMatchers(this::matchesPostProductRequest).authenticated()
.and()
.authorizeRequests()
.requestMatchers(this::matchesPatchProductRequest).authenticated()
.and()
.authorizeRequests()
.requestMatchers(this::matchesDeleteProductRequest).authenticated()
.and()
.authorizeRequests()
.requestMatchers(this::matchesPostUserRequest).authenticated()
.and()
.authorizeRequests()
.requestMatchers(this::matchesDeleteUserRequest).authenticated();
}

/**
* 요청이 상품 생성 요청인 경우 true를 아닌경우 false를 반환합니다.
* @param req
* @return
*/
private boolean matchesPostProductRequest(HttpServletRequest req) {
return req.getMethod().equals("POST") &&
(req.getRequestURI().matches("^/products$") ||
req.getRequestURI().matches("^/products/[0-9]+$"));
}

/**
* 요청이 상품 수정 요청인 경우 true를 아닌경우 false를 반환합니다.
* @param req
* @return
*/
private boolean matchesPatchProductRequest(HttpServletRequest req) {
return req.getMethod().equals("PATCH") &&
req.getRequestURI().matches("^/products/[0-9]+$");
}

/**
* 요청이 상품 삭제 요청인 경우 true를 아닌경우 false를 반환합니다.
* @param req
* @return
*/
private boolean matchesDeleteProductRequest(HttpServletRequest req) {
return req.getMethod().equals("DELETE") &&
req.getRequestURI().matches("^/products/[0-9]+$");
}

/**
* 요청이 유저 생성 요청인 경우 true를 아닌경우 false를 반환합니다.
* @param req
* @return
*/
private boolean matchesPostUserRequest(HttpServletRequest req) {
return req.getMethod().equals("POST") && req.getRequestURI().matches("^/users/[0-9]+$");
}

/**
* 요청이 유저 삭제 요청인 경우 true를 아닌경우 false를 반환합니다.
* @param req
* @return
*/
private boolean matchesDeleteUserRequest(HttpServletRequest req) {
return req.getMethod().equals("DELETE") && req.getRequestURI().matches("^/users/[0-9]+$");
}


}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,6 @@ public ErrorResponse handleLoginFailException() {
return new ErrorResponse("Log-in failed");
}

@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler(InvalidTokenException.class)
public ErrorResponse handleInvalidAccessTokenException() {
return new ErrorResponse("Invalid access token");
}

@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(ConstraintViolationException.class)
Expand All @@ -55,6 +49,13 @@ public ErrorResponse handleConstraintValidateError(
return new ErrorResponse(messageTemplate);
}

@ResponseBody
@ResponseStatus(HttpStatus.FORBIDDEN)
@ExceptionHandler(UserNoPermission.class)
public ErrorResponse handleUserNoPermission(UserNoPermission exception) {
return new ErrorResponse(exception.getMessage());
}

private String getViolatedMessage(ConstraintViolationException exception) {
String messageTemplate = null;
Set<ConstraintViolation<?>> violations = exception.getConstraintViolations();
Expand Down
Loading
Loading