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주차 과제 - Spring Security 인가(Authorization) 구현하기 #74

Merged
merged 32 commits into from
Sep 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
3d1e47f
Spring Security 추가
jdalma Sep 19, 2022
48a38f6
SpringSecurity Filter 추가로 인해 주석 처리
jdalma Sep 19, 2022
1d029ac
advice에서 InvalidTokenException 예외 주석 처리
jdalma Sep 19, 2022
aeb3fb1
TODO 추가
jdalma Sep 19, 2022
6d7e5b8
상품 삭제 시 토큰 없는 경우 테스트 코드 추가
jdalma Sep 19, 2022
79e26d0
SpringSecurity 인증 및 인가 추가
jdalma Sep 19, 2022
7d0ffd6
단일 import문으로 변경
jdalma Sep 19, 2022
4f9695f
User Password 암호화 , bcrypt 사용
jdalma Sep 19, 2022
2ae9974
Formatter 추가
jdalma Sep 20, 2022
f8f1731
authenticate() 테스트 코드 수정
jdalma Sep 20, 2022
b89ad93
User update() 테스트 코드 작성 중
jdalma Sep 20, 2022
43ef954
주석 삭제
jdalma Sep 21, 2022
0cb6ec0
try 수정
jdalma Sep 21, 2022
4587c85
사용자 테스트 코드 수정
jdalma Sep 21, 2022
9d7538d
DozerMapper 제거
jdalma Sep 22, 2022
05fc813
테스트 코드 DisplayName 수정
jdalma Sep 22, 2022
80d8717
DozerMapper 제거
jdalma Sep 22, 2022
f4614c9
ProductData toProduct() maker 필드 추가
jdalma Sep 22, 2022
bf1ef94
AuthenticationFilter filterWithPathAndMethod() 수정
jdalma Sep 22, 2022
15e299c
ProductController 테스트 코드 content 부분 수정
jdalma Sep 22, 2022
d803878
JwtAuthenticationFilter JavaDoc 작성
jdalma Sep 23, 2022
e5e9e6c
JwtUtil.encode() 수정
jdalma Sep 23, 2022
ef37c86
UserController create() 테스트 코드 추가
jdalma Sep 23, 2022
9e54f15
사용자별 권한 작업 중
jdalma Sep 23, 2022
3fd2645
JwtAuthenticationFilter 주석 수정
jdalma Sep 25, 2022
0d5abc2
user 자신의 정보만 수정 가능
jdalma Sep 25, 2022
edc9fcf
UserController 기존 테스트 통과하도록 수정
jdalma Sep 25, 2022
6c44e40
jwt secret 수정
jdalma Sep 25, 2022
63ea12a
DTO 기본 생성자 추가
jdalma Sep 25, 2022
e69315f
User 삭제는 관리자만 가능하게 수정
jdalma Sep 25, 2022
c585ae8
ddl-auto: update 수정
jdalma Sep 25, 2022
9079fd1
과제 풀이 - 권한 적용
jdalma Sep 25, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ Spring Security를 이용해서 구현해 주세요.
내부에서 비밀번호를 탈취를 하더라도 원래 비밀번호를 알아낼 수 없도록 비밀번호를 암호화하여
저장해 주세요.

### TODO

1. 자신의 정보만 수정 가능하게
2. 사용자 삭제는 관리자만 가능하게
3. userId에 따라서 권한을 따로 부여

### 로그인이 필요없는 API

* 로그인 - `POST /session`
Expand Down
6 changes: 3 additions & 3 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,6 @@ dependencies {
compileOnly 'org.projectlombok:lombok:1.18.16'
annotationProcessor 'org.projectlombok:lombok:1.18.16'

// DozerMapper
implementation 'com.github.dozermapper:dozer-core:6.4.0'

// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
runtime 'io.jsonwebtoken:jjwt-impl:0.11.2'
Expand All @@ -69,6 +66,9 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'

// Spring Security
implementation 'org.springframework.boot:spring-boot-starter-security'

// Spring Developer Tools
developmentOnly 'org.springframework.boot:spring-boot-devtools'

Expand Down
8 changes: 4 additions & 4 deletions app/src/main/java/com/codesoom/assignment/App.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.codesoom.assignment;

import com.github.dozermapper.core.DozerBeanMapperBuilder;
import com.github.dozermapper.core.Mapper;
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 @@ -13,7 +13,7 @@ public static void main(String[] args) {
}

@Bean
public Mapper dozerMapper() {
return DozerBeanMapperBuilder.buildDefault();
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,37 @@
package com.codesoom.assignment.application;

import com.codesoom.assignment.domain.RoleRepository;
import com.codesoom.assignment.domain.User;
import com.codesoom.assignment.domain.UserRepository;
import com.codesoom.assignment.errors.LoginFailException;
import com.codesoom.assignment.domain.Role;
import com.codesoom.assignment.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.List;

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

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

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);
}

Expand All @@ -33,4 +42,8 @@ public Long parseToken(String accessToken) {
Claims claims = jwtUtil.decode(accessToken);
return claims.get("userId", Long.class);
}

public List<Role> roles(Long userId) {
return roleRepository.findAllByUserId(userId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import com.codesoom.assignment.domain.ProductRepository;
import com.codesoom.assignment.dto.ProductData;
import com.codesoom.assignment.errors.ProductNotFoundException;
import com.github.dozermapper.core.Mapper;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;
Expand All @@ -13,14 +12,9 @@
@Service
@Transactional
public class ProductService {
private final Mapper mapper;
private final ProductRepository productRepository;

public ProductService(
Mapper dozerMapper,
ProductRepository productRepository
) {
this.mapper = dozerMapper;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}

Expand All @@ -33,14 +27,14 @@ public Product getProduct(Long id) {
}

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

public Product updateProduct(Long id, ProductData productData) {
Product product = findProduct(id);

product.changeWith(mapper.map(productData, Product.class));
product.changeWith(productData.toProduct());

return product;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,18 @@
import com.codesoom.assignment.dto.UserRegistrationData;
import com.codesoom.assignment.errors.UserEmailDuplicationException;
import com.codesoom.assignment.errors.UserNotFoundException;
import com.github.dozermapper.core.Mapper;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;
import java.util.Objects;

@Service
@Transactional
public class UserService {
private final Mapper mapper;
private final UserRepository userRepository;

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

Expand All @@ -28,14 +27,17 @@ public User registerUser(UserRegistrationData registrationData) {
throw new UserEmailDuplicationException(email);
}

User user = mapper.map(registrationData, User.class);
User user = registrationData.toUser();
return userRepository.save(user);
}

public User updateUser(Long id, UserModificationData modificationData) {
public User updateUser(Long id, UserModificationData modificationData , Long userId) throws AccessDeniedException {
if(!Objects.equals(id, userId)){
throw new AccessDeniedException("자원 접근이 거부되었습니다.");
Copy link
Collaborator

Choose a reason for hiding this comment

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

만약 여러 나라의 언어로 서비스를 한다면 어떻게 대응하면 좋을지에 대해서 생각해 보세요.

}
User user = findUser(id);

User source = mapper.map(modificationData, User.class);
User source = modificationData.toUser();
user.changeWith(source);

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

import com.codesoom.assignment.application.AuthenticationService;
import com.codesoom.assignment.filters.AuthenticationErrorFilter;
import com.codesoom.assignment.filters.JwtAuthenticationFilter;
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.web.authentication.HttpStatusEntryPoint;

import javax.servlet.Filter;

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityJavaConfig extends WebSecurityConfigurerAdapter {

private final AuthenticationService authenticationService;

public SecurityJavaConfig(AuthenticationService authenticationService) {
this.authenticationService = authenticationService;
}

@Override
public void configure(HttpSecurity http) throws Exception {
Filter authenticationFilter = new JwtAuthenticationFilter(authenticationManager(), authenticationService);
Filter authenticationErrorFilter = new AuthenticationErrorFilter();
http.csrf()
.disable()
.headers()
.frameOptions()
.disable()
.and()
.addFilter(authenticationFilter)
.addFilterBefore(authenticationErrorFilter , JwtAuthenticationFilter.class)
.exceptionHandling()
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class WebJavaConfig implements WebMvcConfigurer {

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authenticationInterceptor);
// SpringSecurity Filter 추가로 인해 주석 처리
// registry.addInterceptor(authenticationInterceptor);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package com.codesoom.assignment.controllers;

import com.codesoom.assignment.dto.ErrorResponse;
import com.codesoom.assignment.errors.*;
import com.codesoom.assignment.errors.LoginFailException;
import com.codesoom.assignment.errors.ProductNotFoundException;
import com.codesoom.assignment.errors.UserEmailDuplicationException;
import com.codesoom.assignment.errors.UserNotFoundException;
import com.codesoom.assignment.filters.AuthenticationErrorFilter;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
Expand Down Expand Up @@ -39,12 +43,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 Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,35 @@

package com.codesoom.assignment.controllers;

import com.codesoom.assignment.application.AuthenticationService;
import com.codesoom.assignment.application.ProductService;
import com.codesoom.assignment.domain.Product;
import com.codesoom.assignment.dto.ProductData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;
import java.util.List;

@Slf4j
@RestController
@RequestMapping("/products")
@CrossOrigin
public class ProductController {
private final ProductService productService;

private final AuthenticationService authenticationService;

public ProductController(ProductService productService,
AuthenticationService authenticationService) {
public ProductController(ProductService productService) {
this.productService = productService;
this.authenticationService = authenticationService;
}

@GetMapping
Expand All @@ -40,26 +47,26 @@ public Product detail(@PathVariable Long id) {

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
@PreAuthorize("isAuthenticated()")
public Product create(
@RequestAttribute Long userId,
@RequestBody @Valid ProductData productData
) {
return productService.createProduct(productData);
}

@PatchMapping("{id}")
@PreAuthorize("hasRole('ADMIN')")
public Product update(
@RequestAttribute Long userId,
@PathVariable Long id,
@RequestBody @Valid ProductData productData
) {
return productService.updateProduct(id, productData);
}

@DeleteMapping("{id}")
@PreAuthorize("isAuthenticated()")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void destroy(
@RequestAttribute Long userId,
@PathVariable Long id
) {
productService.deleteProduct(id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@
import com.codesoom.assignment.dto.SessionRequestData;
import com.codesoom.assignment.dto.SessionResponseData;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/session")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,20 @@
import com.codesoom.assignment.dto.UserRegistrationData;
import com.codesoom.assignment.dto.UserResultData;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;
import java.nio.file.AccessDeniedException;

@RestController
@RequestMapping("/users")
Expand All @@ -28,15 +39,19 @@ UserResultData create(@RequestBody @Valid UserRegistrationData registrationData)
}

@PatchMapping("{id}")
@PreAuthorize("isAuthenticated()")
UserResultData update(
Authentication authentication,
@PathVariable Long id,
@RequestBody @Valid UserModificationData modificationData
) {
User user = userService.updateUser(id, modificationData);
Long userId = (Long) authentication.getPrincipal();
User user = userService.updateUser(id, modificationData , userId);
return getUserResultData(user);
}

@DeleteMapping("{id}")
@PreAuthorize("isAuthenticated() and hasAuthority('ADMIN')")
@ResponseStatus(HttpStatus.NO_CONTENT)
void destroy(@PathVariable Long id) {
userService.deleteUser(id);
Expand Down