Back-End/React.js, 스프링 부트, AWS로 배우는 웹 개발 101

[React.js, 스프링 부트, AWS로 배우는 웹 개발 101][인증 백엔드 통합] - 스프링 시큐리티

얄루몬 2022. 7. 1. 13:45

본 포스팅은 'React.js, 스프링 부트, AWS로 배우는 웹 개발 101 - 김다정'님의 책을 보고 작성되었습니다.


목차
1. 스프링 시큐리티 설정
2. 인증된 사용자 사용하기
3. 패스워드 암호화
4. 정리

1. 스프링 시큐리티 설정

 

서블릿 필터 사용을 위한 작업 두 가지

  1. 서블릿 필터 구현
  2. 서블릿 컨테이너에 이 서블릿 필터를 사용하라고 알려주는 설정 작업

[서블릿 컨테이너에 이 서블릿 필터 사용하라고 알려주는 설정 작업]

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를 작성했다.