# 4 인증 백엔드 통합

## 4.1 REST API 인증 기법

### 4.1.1 Basic 인증

1. **Basic 인증**
   * 모든 HTTP 요청에 아이디와 비밀번호를 같이 보내는 것
   *   최초 로그인한 후 HTTP 요청 헤더의 Authorization: 부분에 ‘Basic \<ID>:\<Password>’처럼 아이디와 비밀번호를 콜론으로 이어붙인 후 Base64로 인코딩한 문자열을 함께 보냄

       ```java
       Authorization: Basic aGVsb93b3JsZEBnbWFpbC5jbC5jb206MTIzNA==
       ```
   * 이 HTTP 요청을 수신한 서버는 인코딩된 문자열을 디코딩해 아이디와 비밀번호를 찾아냄
   * 사용자 정보가 저장된 데이터베이스 또는 인증 서버의 레코드와 비교함
   * 데이터베이스의 레코드는 아이디와 비밀번호가 일치하면 요텅받은 일을 수행하고, 아니면 거부함

*   Basic 인증의 문제점

    * 아이디와 비밀번호를 노출
      * 중간에 누군가 HTTP 요청을 가로채 문자열을 디코딩하면 아이디와 비밀번호를 알아낼 수 있음 (MITM)
      * HTTP와 사용하기엔 취약하여, 반드시 HTTPS와 사용해야 함
    * 사용자를 로그아웃시킬 수 없음
    * 사용자의 계정 정보가 있는 저장 장소, 인증 서버와 인증 DB에 과부하가 걸릴 확률이 높음
      * 서버가 단일 장애점이 됨

    !! 사진사진

### 4.1.2 토큰 기반 인증



1. **토큰 기반 인증**
   * 토큰(Token)은 사용자를 구별할 수 있는 문자열
   *   토큰은 최초 로그인 시 서버가 만들어 줌.

       * 서버가 자기만의 노하우로 토큰을 만들어 반환하면 클라이언트는 이후 요청에 아이디와 비밀번호 대신 토큰을 계속 넘겨 자신이 인증된 사용자임을 알리는 것

       ```java
       Authorization: Bearer Nnfjadshgskdhglkjrnglkjreblgkjerbdgkjsrbg
       ```

       * 토큰을 기반으로 하는 요청은 헤더에 위와같이 Authorization: Bearer \<TOKEN>을 명시
       * 서버는 이 토큰을 받고 어떤 형태로든 인증을 해야 함
       * (토큰 이읆만 바뀐 세션)
   * ex
     * 서버가 랜덤한 문자와 숫자를 섞어 UUID로 토큰을 작성해 넘긴다고 가정
     * 서버는 토큰을 생성해 인증 서버를 통해 저장
     * 요청을 받을 때마다 헤더의 토큰을 서버의 토큰과 비교해 클라이언트를 인증

* Basic Auth vs 토큰 기반 인증
  * 아이디와 비밀번호를 매번 네크워크를 통해 전송해야 할 필요가 없음
    * 이는 보안 측면에서 좀 더 안전함
  * 서버가 토큰을 마음대로 생성할 수 있으므로 사용자의 인가 정보(예 : User, Admin 등) 또는 유효 시간을 정해 관리 가능
    * 디바이스마다 다른 토큰을 생성해 주고 디바이스마다 유효 시간을 다르게 정하거나 임의로 로그아웃을 할 수도 있음

### 4.1.3 JSON 웹 토큰&#x20;

1. **JSON 웹 토큰**
   * 서버에서 **전자 서명된 토큰**을 이용하면 인증에 따른 스케일 문제를 해결 가능
   *   {header}.{payload}.{signature}로 구성

       ```java
       Authorization: Bearer ejsldkf.saeff;dgjad.fejfwiefk
       ```

       ```java
       위를 디코딩하면 
       ```

       * Header
         * typ : Type을 줄인 말로 토큰의 타입
         * alg : Algorithm을 줄인 말로 토큰의 서명을 발행하는 데 사용된 해시 알고리듬의 종류 의미
       * Payload
         * sub : Subject를 줄인 말로 토큰의 주인 의미. 우리 애플리케이션에서는 사용자의 이메일로 토큰의 주인을 판별함. sub는 ID처럼 유일한 식별자여야 함
         * iss : Issuer를 줄인 말로 토큰을 발행한 주체를 의미
         * iat : issued at을 줄인 말로 토큰이 발행된 날짜와 시간
         * exp : expirarion을 줄인 말로 토큰이 만료되는 시간
       * Sugnature
         * 토큰을 발행한 주체 Issuer가 발행한 서명으로 토큰의 유효성 검사에 사용

* 토큰 기반 인증 vs JWT 토큰 기반 인증
  * JWT 은 서버가 헤더와 페이로드를 생성한 후 전자 서명을 함
* 전자 서명
  * {헤더}.{페이로드}와 시크릿키를 이용해 해시 함숫에 돌린 암호화한 결과 값
  * 시크릿키란 나만 알고 있는 문자열, 비밀번호 같은 것
* JWT 토큰 생성과 인증
  * 생성
    * 최초 로그인 시 서버는 아이디와 비밀번호를 서버에 저장된 아이디와 비밀번호에 비교해 인증
    * 만약 인증된 사용자인 경우 사용자의 정보를 이용해 {헤더}.{페이로드} 부분을 작성
    * 이후 자신의 시크릿키로 {헤더}.{페이로드}.{서명}으로 이어붙이고 Base64로 인코딩한 후 반환
  * 인증
    * 누군가 토큰으로 리소스 접근을 요청
    * 서버는 토큰을 Base64로 디코딩
    * 디코딩해서 얻은 JSON을 {헤더}.{페이로드}와 {서명} 부분으로 나눔
    * 서버는 {헤더}.{헤이로드}와 자신이 갖고 있는 Secret으로 전자 서명을 만들 후 방금 만든 전자 서명을 HTTP 요청이 갖고 온 {서명} 부분과 비교해 토큰의 유효성 검사
  * 이는 인증 서버에 부하를 일으키지 않음. 즉 더 이상 인증 서버가 단일 장애점이 아님

## 4.2 User 레이어 구현

### 4.2.1 UserEntity.java

```java
package com.todoweb.todoSpringApp.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.GenericGenerator;

import javax.persistence.*;

@Data
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(uniqueConstraints = {@UniqueConstraint(columnNames = "email")})
public class UserEntity {
    @Id
    @GeneratedValue(generator = "system-uuid")
    @GenericGenerator(name = "system-uuid", strategy = "uuid")
    private String id;

    @Column(nullable = false)
    private String username;

    @Column(nullable = false)
    private String email;

    @Column(nullable = false)
    private String password;

```

### 4.2.2 UserRepository.java

```java
package com.todoweb.todoSpringApp.persistence;

import com.todoweb.todoSpringApp.model.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<UserEntity, String> {
    UserEntity findByEmail(String email);
    Boolean existsByEmail(String email);
    UserEntity findByEmailAndPassword(String email, String password);
}

```

### 4.2.3 UserService.java

```java
package com.todoweb.todoSpringApp.service;

import com.todoweb.todoSpringApp.model.UserEntity;
import com.todoweb.todoSpringApp.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;

@Slf4j
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    public UserEntity create(final UserEntity userEntity) {
        if(userEntity == null || userEntity.getEmail() == null) {
            throw new RuntimeException("Invalid arguments");
        }
        final String email = userEntity.getEmail();
        if(userRepository.existsByEmail(email)) {
            log.warn("Email already exists {}", email);
            throw new RuntimeException("Email already exists");
        }

        return userRepository.save(userEntity);
    }

    public UserEntity getByCredentials(final String email, final String password, final PasswordEncoder encoder) {
        final UserEntity originalUser = userRepository.findByEmail(email);
        if(originalUser != null && encoder.matches(password, originalUser.getPassword())) {
            return originalUser;
        }
        return null;
    }
}

```

### 4.2.4 UserController.java

{% tabs %}
{% tab title="UserController.java" %}
```java
package com.todoweb.todoSpringApp.controller;

import com.todoweb.todoSpringApp.dto.ResponseDTO;
import com.todoweb.todoSpringApp.dto.UserDTO;
import com.todoweb.todoSpringApp.model.UserEntity;
import com.todoweb.todoSpringApp.security.TokenProvider;
import com.todoweb.todoSpringApp.service.UserService;
import lombok.extern.slf4j.Slf4j;
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;

@Slf4j
@RestController
@RequestMapping("/auth")
public class UserController {

    @Autowired
    private UserService userService;

    @PostMapping("/signup")
    public ResponseEntity<?> registerUser(@RequestBody UserDTO userDTO) {
        try {
            // 리퀘스트를 이용해 저장할 유저 만들기
            UserEntity user = UserEntity.builder()
                    .email(userDTO.getEmail())
                    .username(userDTO.getUsername())
                    .password(userDTO.getPassword())
                    .build();
            // 서비스를 이용해 리파지토리에 유저 저장
            UserEntity registeredUser = userService.create(user);
            UserDTO responseUserDTO = UserDTO.builder()
                    .email(registeredUser.getEmail())
                    .id(registeredUser.getId())
                    .username(registeredUser.getUsername())
                    .build();
            // 유저 정보는 항상 하나이므로 그냥 리스트로 만들어야하는 ResponseDTO를 사용하지 않고 그냥 UserDTO 리턴.
            return ResponseEntity.ok(responseUserDTO);
        } catch (Exception e) {
            // 예외가 나는 경우 bad 리스폰스 리턴.
            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();

        if(user != null) {
            final UserDTO responseUserDTO = UserDTO.builder()
                    .email(user.getUsername())
                    .id(user.getId())
                    .build();
            return ResponseEntity.ok().body(responseUserDTO);
        } else {
            ResponseDTO responseDTO = ResponseDTO.builder()
                    .error("Login failed")
                    .build();
            return ResponseEntity.badRequest().body(responseDTO);
        }
    }
}

```
{% endtab %}

{% tab title="UserDTO.java" %}
```java
package com.todoweb.todoSpringApp.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserDTO {
    private String token;
    private String email;
    private String username;
    private String password;
    private String id;
}

```
{% endtab %}
{% endtabs %}

## 4.3 Spring Security 통합



4.2에서 인증 및 인가를 구현하려고 사용자에 관련된 클래스을 작성

* 문제점
  * 로그인 여부를 저장하지 않음&#x20;
  * API 들이 사용자를 인증하지 않음
  * 패스워드 암호화하지 않음

### 4.3.1 JWT 생성 및 반환 구현&#x20;

* <mark style="background-color:purple;">토큰 발행</mark> <mark style="background-color:purple;"></mark><mark style="background-color:purple;">**: JWT library**</mark>
* 로그인 시 토큰을 반환

JWT를 이용해 인증하는 방법 -> 사진



**1.jjwt 라이브러리 디펜던시에 추가**

```markup
// <https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt>
implementation group: 'io.jsonwebtoken', name:'jjwt', version: '0.9.1'
```

* security 패키지 생성
  * 인증을 위한 클래스 관리
  * 인가를 위한 클래스 관리

**2. 토큰 발행**&#x20;

```java
package com.todoweb.todoSpringApp.security;

import com.todoweb.todoSpringApp.model.UserEntity;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.time.Instant;
import java.time.temporal.ChronoUnit;

@Slf4j
@Service
public class TokenProvider {
    private static final String SECRET_KEY = "NMA8JPctFuna59f5";

    public String create(UserEntity userEntity) {
        Date expiryDate = Date.from(
                Instant.now()
                        .plus(1, ChronoUnit.DAYS));

		/*
		{ // header
		  "alg":"HS512"
		}.
		{ // payload
		  "sub":"40288093784915d201784916a40c0001",
		  "iss": "demo app",
		  "iat":1595733657,
		  "exp":1596597657
		}.
		// SECRET_KEY를 이용해 서명한 부분
		Nn4d1MOVLZg79sfFACTIpCPKqWmpZMZQsbNrXdJJNWkRv50_l7bPLQPwhMobT4vBOG6Q3JYjhDrKFlBSaUxZOg
		 */
        // JWT Token 생성
        return Jwts.builder()
                .signWith(SignatureAlgorithm.HS512, SECRET_KEY)
                .setSubject(userEntity.getId())
                .setIssuer("demo app")
                .setIssuedAt(new Date())
                .setExpiration(expiryDate)
                .compact();
    }

    public String validateAndGetUserId(String token) {

        Claims claims = Jwts.parser()
                .setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody();

        return claims.getSubject();
    }
}
```



**3. 로그인 시 토큰 반환**&#x20;

```java
package com.todoweb.todoSpringApp.controller;

/* import 생략 */

@Slf4j
@RestController
@RequestMapping("/auth")
public class UserController {

    @Autowired
    private UserService userService;

    @Autowired
    private TokenProvider tokenProvider;

    @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.getUsername())
                    .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);
        }
    }
}
```

### 4.3.2 스프링 시큐리티와 서블릿 구현&#x20;



* API 가 실행될 때마다 사용자를 인증해 주는 부분 구현 : 스프링 시큐리티



* 스프링 시큐리티
  * 토큰 인증을 위해 컨트롤러 메서드의 첫 부분마다 인증 코드 작성해야하는 문제점
  * 이증과 인가를 위한 다양한 기능 제공
  * 서블릿 필터의 집합



* 서블릿 필터
  * 서블릿 실행 전에 실행되는 클래스
  * 스프링이 구현하는 디스패처 서블릿이 실행되기 전에 항상 실행
  * 1\) 개발자는 서블릿 필터를 구현하고 2) 서플릿 필터를 서블릿 컨테이너가 실행하도록 설정

<서블릿 컨테이너의 서블릿 필터 pic>

* 서블릿 필터에서 스프링 시큐리티의 위치와 우리가 구현할 필터의 위치
  * 스프링 시큐리티가 FilterChainProxy라는 필터를 서블릿 필터에 끼워 줌
  * 이 FilterChainProxy 클래스 안에서 내부적으로 필터 실행
  * 이 필터들이 스프링이 관리하는 스프링 빈 필터
    * 상속하는 필터는 OncePerRequestFilter
    * 필터 설정하기 위해 상속하는 클래스는 WebSecurityConfigurerAdapter

### 4.3.3 JWT를 이용한 인증 구현

<mark style="background-color:purple;">서블릿 필터 구현</mark>

1. 스프링 시큐리티 디펜던시를 build.gradle에 추가

```java
implementation 'org.springframework.boot:spring-boot-starter-security'
```

1. OncePerRequestFilter를 상속하는 JwtAuthenticationFilter 구현

```java
package com.todoweb.todoSpringApp.security;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Slf4j
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private TokenProvider tokenProvider;

    @Override
    protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            String token = parseBearerToken(request);
            log.info("Filter is running ... ");
        if (token != null && !token.equalsIgnoreCase("null")) {
            String userId = tokenProvider.validateAndGetUserId(token);
            log.info("Authenticated user ID : " + userId);
            AbstractAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                    userId,
                    null, AuthorityUtils.NO_AUTHORITIES
            );
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
            securityContext.setAuthentication(authentication);
            SecurityContextHolder.setContext(securityContext);

        }
        } catch (Exception ex) {
            logger.error("Could not set user authentication in security context", ex);
        }
        filterChain.doFilter(request, response);
    }

    private String parseBearerToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");

        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}
```

* parseBearerToken() : 요청의 헤더에서 Bearer 토큰을 가져옴
* TokenProvider를 이용해 토큰을 인증하고 UsernamePasswordAuthenticationToken을 작성. 이 오브젝트에 사용자의 인증 정보를 저장하고 SecurityContext에 인증된 사용자 등록
  * 요청을 처리하는 과정에서 사용자가 인증됐는지 여부 혹은 인증괸 사용자가 누군지 알아야 할 때가 있기 때문
* SecurityContext
  * SecurityContextHolder의 createEmptyContext() 메서드를 이용해 생성
  * 생성한 컨텍스트에 인증 정보인 authentication을 넣고 다시 SecurityContextHolder에 컨텍스트로 등록
  * SecirutyContextHolder는 기본적으로 ThreadLocal에 저장
    * ThreadLocal에 저장되므로 Thread마다 하나의 컨텍스트를 관리할 수 있음
    * 같은 스레드 내라면 어디에서든 접근 가능

### 4.3.4 스프링 시큐리티 설정

<mark style="background-color:purple;">서블릿 컨테이너에 이 서블릿 필터를 사용하라고 알려줘야 함</mark>

* 스프링 시큐리티에 JwtAuthenticationFilter를 사용하라고 알려줘야 함

```java
package com.todoweb.todoSpringApp.config;

import com.todoweb.todoSpringApp.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.cors()
                .and()
                .csrf()
                .disable()
                .httpBasic()
                .disable()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/", "/auth/**").permitAll()
                .anyRequest()
                .authenticated();

        http.addFilterAfter(
                jwtAuthenticationFilter,
                CorsFilter.class
        );
    }
}
```

### 4.3.5 TodoController에서 인증된 사용자 사용하기



```java
package com.todoweb.todoSpringApp.controller;

import com.todoweb.todoSpringApp.dto.ResponseDTO;
import com.todoweb.todoSpringApp.dto.TodoDTO;
import com.todoweb.todoSpringApp.model.TodoEntity;
import com.todoweb.todoSpringApp.service.TodoService;
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.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("todo")
public class TodoController {

    @Autowired
    private TodoService service;

    @PostMapping
    public ResponseEntity<?> createTodo(@AuthenticationPrincipal String userId, @RequestBody TodoDTO dto) {
        try {

            TodoEntity entity = TodoDTO.toEntity(dto);

            entity.setId(null);
            entity.setUserId(userId);

            List<TodoEntity> entities = service.create(entity);
            List<TodoDTO> dtos = entities.stream().map(TodoDTO::new).collect(Collectors.toList());
            ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().data(dtos).build();
            return ResponseEntity.ok().body(response);
        } catch (Exception e) {
            String error = e.getMessage();
            ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().error(error).build();
            return ResponseEntity.badRequest().body(response);
        }
    }

    @GetMapping("/findAll")
    public ResponseEntity<?> findAll() {
        try{
            List<TodoEntity> entities = service.findAll();
            List<TodoDTO> dtos = entities.stream().map(TodoDTO::new).collect(Collectors.toList());
            ResponseDTO response = ResponseDTO.<TodoDTO>builder().data(dtos).build();
            return ResponseEntity.ok().body(response);
        } catch (Exception e) {
            String error = e.getMessage();
            ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().error(error).build();
            return ResponseEntity.badRequest().body(response);
        }
    }

    @GetMapping
    public ResponseEntity<?> findById(@AuthenticationPrincipal String userId) {
        System.out.println("UserID : " + userId);

        List<TodoEntity> entities = service.findByUserId(userId);

        List<TodoDTO> dtos = entities.stream().map(TodoDTO::new).collect(Collectors.toList());

        ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().data(dtos).build();

        return ResponseEntity.ok(response);
    }

    @PutMapping
    public ResponseEntity<?> updateTodo(@AuthenticationPrincipal String userId,@RequestBody TodoDTO dto) {

        TodoEntity entity = TodoDTO.toEntity(dto);
        entity.setUserId(userId);
        List<TodoEntity> entities = service.update(entity);
        List<TodoDTO> dtos = entities.stream().map(TodoDTO::new).collect(Collectors.toList());
        ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().data(dtos).build();

        return ResponseEntity.ok().body(response);
    }

    @DeleteMapping
    public ResponseEntity<?> deleteTodo(@AuthenticationPrincipal String userId, @RequestBody TodoDTO dto) {
        try {

            TodoEntity entity = TodoDTO.toEntity(dto);
            entity.setUserId(userId);

            List<TodoEntity> entities = service.deleteById(entity);
            List<TodoDTO> dtos = entities.stream().map(TodoDTO::new).collect(Collectors.toList());
            ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().data(dtos).build();

            return ResponseEntity.ok().body(response);
        } catch (Exception e) {
            String error = e.getMessage();
            ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().error(error).build();
            return ResponseEntity.badRequest().body(response);
        }
    }
}
```

### 4.3.6 패스워드 암호화&#x20;



패스워드 암호화 부분은 스프링 시큐리티가 제공하는 BCryptPasswordEncoder 사용

* UserService

```java
package com.todoweb.todoSpringApp.service;

import com.todoweb.todoSpringApp.model.UserEntity;
import com.todoweb.todoSpringApp.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;

@Slf4j
@Service
public class UserService {

		/* 기존 코드 . . .*/
    public UserEntity getByCredentials(final String email, final String password, final PasswordEncoder encoder) {
        final UserEntity originalUser = userRepository.findByEmail(email);
        if(originalUser != null && encoder.matches(password, originalUser.getPassword())) {
            return originalUser;
        }
        return null;
    }
}
```

* BCryptPasswordEncoder는 값은 값을 인코딩하더라도 할 때마다 값이 다르고 패스워드에 랜덤하게 의미 없는 값을 붙여 결과를 생성
* 이런 의미 없는 값을 보안 용어로 Salt라고 하고, Salt를 붙여 인코딩하는 것을 Salting이라고 함
* matches() 메서드 : Salt를 고려해 두 값을 비교
* UserController

```java
package com.todoweb.todoSpringApp.controller;

import com.todoweb.todoSpringApp.dto.ResponseDTO;
import com.todoweb.todoSpringApp.dto.UserDTO;
import com.todoweb.todoSpringApp.model.UserEntity;
import com.todoweb.todoSpringApp.security.TokenProvider;
import com.todoweb.todoSpringApp.service.UserService;
import lombok.extern.slf4j.Slf4j;
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;

@Slf4j
@RestController
@RequestMapping("/auth")
public class UserController {

    @Autowired
    private UserService userService;

    @Autowired
    private TokenProvider tokenProvider;

    private PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

		/*기존 코드 . .*/

    @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.getUsername())
                    .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);
        }
    }
}
```
