본 포스팅은 'React.js, 스프링 부트, AWS로 배우는 웹 개발 101 - 김다정'님의 책을 보고 작성되었습니다.
목차
1. 스프링 시큐리티 설정
2. 인증된 사용자 사용하기
3. 패스워드 암호화
4. 정리
1. 스프링 시큐리티 설정
서블릿 필터 사용을 위한 작업 두 가지
- 서블릿 필터 구현
- 서블릿 컨테이너에 이 서블릿 필터를 사용하라고 알려주는 설정 작업
[서블릿 컨테이너에 이 서블릿 필터 사용하라고 알려주는 설정 작업]
package com.example.demo.config;
import com.example.demo.security.JwtAuthenticationFilter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.web.filter.CorsFilter;
@EnableWebSecurity
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
// http 시큐리티 빌더
http.cors() // WebMvcConfig에서 이미 설정했으므로 기본 cors 설정.
.and()
.csrf()// csrf는 현재 사용하지 않으므로 disable
.disable()
.httpBasic()// token을 사용하므로 basic 인증 disable
.disable()
.sessionManagement() // session 기반이 아님을 선언
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests() // /와 /auth/** 경로는 인증 안해도 됨.
.antMatchers("/", "/auth/**").permitAll()
.anyRequest() // /와 /auth/**이외의 모든 경로는 인증 해야됨.
.authenticated();
// filter 등록.
// 매 리퀘스트마다
// CorsFilter 실행한 후에
// jwtAuthenticationFilter 실행한다.
http.addFilterAfter(
jwtAuthenticationFilter,
CorsFilter.class
);
}
}
- 현재 WebSecurityConfigurerAdapter는 deprecated된 상태다.
- HttpSecurity - 시큐리티 설정을 위한 오브젝트로 이 오브젝트는 빌더를 제공하며 빌더를 사용해 설정을 할 수 있다.
- web.xml대신 HttpSecurity를 사용해 시큐리티 관련 설정을 하는 것이다.
- addFilterAfter( ) 메서드를 마지막에 실행하는데 이는 실제 실행이 아닌 JwtAuthenticationFilter를 CorsFilter 이후 실행하라고 설정하는 것이다.(이는 완전히 고정된 방법이 아님 그렇게 하는 것이 바람직해보여 그렇게 한 것임)
2. 인증된 사용자 사용하기
package com.example.demo.controller;
import com.example.demo.dto.ResponseDTO;
import com.example.demo.dto.TodoDTO;
import com.example.demo.model.TodoEntity;
import com.example.demo.service.TodoService;
import lombok.Builder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("todo")
@Builder
public class TodoController {
//빈을 알아서 찾고 그 빈을 이 인스턴스 멤버 변수로 연결하라는 뜻이다.
@Autowired
private TodoService service;
@GetMapping("/test")
public ResponseEntity<?> testTodo(){
String str = service.testService();//테스트 서비스 사용
List<String> list = new ArrayList<>();
list.add(str);
ResponseDTO<String> response = ResponseDTO.<String>builder().data(list).build();
return ResponseEntity.ok().body(response);
}
@PostMapping
public ResponseEntity<?> createTodo(
@AuthenticationPrincipal String userId,
@RequestBody TodoDTO dto){
try {
//String temporaryUserId = "temporary-user"; // 임시 유저 아이디
//1 TOdoEntity 변환
TodoEntity entity = TodoDTO.toEntity(dto);
//2. id를 null로 초기화 해준다. 생성 당시에 id가 없어야 하기 때문이다.
entity.setId(null);
//3.임시 사용자 아이디를 설정해 준다. 지금은 당장 인가가 필요 없는 부분이기에 추후에 수정해줄 예정
//entity.setUserId(temporaryUserId);
//3. 설정해둔 임시 사용자가 아닌 @AuthenticationPrincipal에서 넘어온 userId로 설정해준다.
entity.setUserId(userId);
//4. 서비스를 사용해 Todo 엔티티를 생성해준다.
List<TodoEntity> entities = service.create(entity);
//5. 자바 스트림을 이용해 리턴된 엔티티 리스트를 TodoDTO 리스트로 변환한다.
List<TodoDTO> dtos = entities.stream().map(TodoDTO::new).collect(Collectors.toList());
//6. 변환된 TodoDTO 리스트를 이용해 ResponseDTO를 초기화한다.
ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().data(dtos).build();
//7. ResponseDTO를 리턴
return ResponseEntity.ok().body(response);
}catch (Exception e){
//8. 혹시 예외가 있는 경우 dto 대신 error에 메시지를 넣어 리턴해준다.
String errorMessage = e.getMessage();
ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().error(errorMessage).build();
return ResponseEntity.badRequest().body(response);
}
}
@GetMapping
public ResponseEntity<?> retrieveTodoList(
@AuthenticationPrincipal String userId){
//String temporaryUserId = "temporary-user"; // 임시 유저 아이디
//1. 서비스 메서드의 retrieve() 메서드를 사용해 Todo 리스트를 가져온다.
List<TodoEntity> entities = service.retrieve(userId);
//2. 자바 스트림을 사용해 리턴된 엔티티 리스트를 TodoDTD 리스트로 변환한다.
List<TodoDTO> dtos = entities.stream().map(TodoDTO::new).collect(Collectors.toList());
//3. 뱐환된 TodoDTO 리스트를 이용해서 ResponseDTO를 초기화한다.
ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().data(dtos).build();
//4. ResponseDTO 리턴
return ResponseEntity.ok().body(response);
}
@PutMapping
public ResponseEntity<?> updateTodo(
@RequestBody TodoDTO dto,
@AuthenticationPrincipal String userId
){
//String temporaryUserId = "temporary-user"; // 임시 유저 아이디
//1. dto를 entity로 변환
TodoEntity entity = TodoDTO.toEntity(dto);
//2. id를 임시 유저 아이디로 초기화한다. 여기 역시 수정할 예정
entity.setUserId(userId);
//3. 서비스를 이용해 entity를 업데이트한다.
List<TodoEntity> entities = service.update(entity);
//4. 자바 스트림을 사용해서 리턴된 엔티티 리스트를 TodoDTO 리스트로 변환한다.
List<TodoDTO> dtos = entities.stream().map(TodoDTO::new).collect(Collectors.toList());
//5. 변환된 TodoDTO 리스트를 이용해서 ResponseDTO를 초기화 한다.
ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().data(dtos).build();
return ResponseEntity.ok().body(response);
}
@DeleteMapping
public ResponseEntity<?> deleteTodo(
@RequestBody TodoDTO dto,
@AuthenticationPrincipal String userId){
try{
//String temporaryUserId = "temporary-user"; // 임시 유저 아이디
//1. TodoEntity 변환
TodoEntity entity = TodoDTO.toEntity(dto);
//2. 임시 사용자 아이디 설정, 이 부분도 수정 예정 인증, 인가가 들어오면 바꿀 부분
entity.setUserId(userId);
//3. 서비스를 이용해 entity 삭제
List<TodoEntity> entities = service.delete(entity);
//4. 자바 스트림을 사용해 리턴된 엔티티 리스트를 TodoDTO 리스트로 변환
List<TodoDTO> dtos = entities.stream().map(TodoDTO::new).collect(Collectors.toList());
//5.변환된 TodoDTO 리스트를 이용하여 ResponseDTO 초기화
ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().data(dtos).build();
//6. ResponseDTO 리턴
return ResponseEntity.ok().body(response);
}catch (Exception e){
String errorMessage = e.getMessage();
ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().error(errorMessage).build();
return ResponseEntity.badRequest().body(response);
}
}
}
- 기존 temporaryUserId가 아닌 @AuthenticationPrincipal에서 넘어온 userId를 사용해 임시 사용자를 저장해준다.
@AuthenticationPrincipal이란?
AbstractAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(
userId,
null,
AuthorityUtils.NO_AUTHORITIES
);
- UsernamePasswordAuthenticationToken을 생성해 첫번째 매개변수로 넣은 것이 AuthenticationPrincipal이다.
- String 형의 userId를 넣어 이 오브젝트를 SeurityContext에 등록해주었다.
- 간단하게 @AuthenticationPrincipal를 사용하면 UsernamePasswordAuthenticationToken를 가져와 컨트롤러 메서드에 넘겨준다.
3. 패스워드 암호화
패스워드 암호화 부분은 스프링 시큐리티가 제공하는 BCryptPasswordEncoder의 사용할 예정이다.
로그인 인증에 암호화 부분이 있으니 서비스 로직부터 수정해준다.
package com.example.demo.service;
import com.example.demo.model.UserEntity;
import com.example.demo.persistence.UserRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class UserService {
@Autowired
private UserRepository userRepository;
//UserRepository를 이용해 사용자 생성
public UserEntity create(final UserEntity userEntity){
if(userEntity == null || userEntity.getEmail() == null){
throw new RuntimeException("Invalid arguments");
}
//만약 이메일이 있는 경우라면? RuntimeException을 던진다.
final String email = userEntity.getEmail();
if(userRepository.existsByEmail(email)){
log.warn("Email already exist");
throw new RuntimeException("Email already exist");
}
return userRepository.save(userEntity);
}
//로그인 시 인증에 사용할 메서드
public UserEntity getByCredentials(final String email, final String password,
PasswordEncoder encoder){
final UserEntity originalUser = userRepository.findByEmail(email);
//matches 메서드를 사용해 패스워드가 같은지 확인
if(originalUser != null &&
encoder.matches(password, originalUser.getPassword())){
return originalUser;
}
return null;
}
}
패스워드 인코딩
package com.example.demo.controller;
import com.example.demo.dto.ResponseDTO;
import com.example.demo.dto.UserDTO;
import com.example.demo.model.UserEntity;
import com.example.demo.security.TokenProvider;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
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.RestController;
@RequestMapping("/auth")
@RestController
public class UserController {
@Autowired
private UserService userService;
@Autowired
private TokenProvider tokenProvider;
private PasswordEncoder passwordEncoder= new BCryptPasswordEncoder();
@PostMapping("/signup")
public ResponseEntity<?> registerUser(@RequestBody UserDTO userDTO) {
try{
//요청을 이용해서 저장할 사용자 만들기
UserEntity user = UserEntity.builder()
.email(userDTO.getEmail())
.username(userDTO.getUsername())
.password(userDTO.getPassword())
.build();
//서비스를 사용해 레포지토리에 사용자 저장
UserEntity registerUser = userService.create(user);
UserDTO responseUserDTO = UserDTO.builder()
.email(registerUser.getEmail())
.username(registerUser.getUsername())
.id(registerUser.getId())
.build();
return ResponseEntity.ok().body(responseUserDTO);
} catch (Exception e){
ResponseDTO responseDTO = ResponseDTO.builder()
.error(e.getMessage())
.build();
return ResponseEntity.badRequest().body(responseDTO);
}
}
@PostMapping("signin")
public ResponseEntity<?> authenticate(@RequestBody UserDTO userDTO){
UserEntity user = userService.getByCredentials(
userDTO.getEmail(),
userDTO.getPassword(),
passwordEncoder
);
if(user != null){
final String token = tokenProvider.create(user);
final UserDTO responseUserDTO = UserDTO.builder()
.email(user.getEmail())
.id(user.getId())
.token(token)
.build();
return ResponseEntity.ok().body(responseUserDTO);
} else {
ResponseDTO responseDTO = ResponseDTO.builder()
.error("Login Failed")
.build();
return ResponseEntity.badRequest().body(responseDTO);
}
}
}
public ResponseEntity<?> authenticate(@RequestBody UserDTO userDTO){
UserEntity user = userService.getByCredentials(
userDTO.getEmail(),
userDTO.getPassword(),
passwordEncoder
);
4. 정리
- API 서비스 레벨에서 인증을 구현하였다.
- 인증, 인가에 대한 기본 개념과 방법을 살펴보았다.
- 실제 사용자 관리를 위한 User Layer을 앞 포스팅부터 계속 구현했다.
- JwtAuthenticationFilter를 작성했다.