4 인증 백엔드 통합

4.1 REST API 인증 기법

4.1.1 Basic 인증

  1. Basic 인증

    • 모든 HTTP 요청에 아이디와 비밀번호를 같이 보내는 것

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

      Authorization: Basic aGVsb93b3JsZEBnbWFpbC5jbC5jb206MTIzNA==
    • 이 HTTP 요청을 수신한 서버는 인코딩된 문자열을 디코딩해 아이디와 비밀번호를 찾아냄

    • 사용자 정보가 저장된 데이터베이스 또는 인증 서버의 레코드와 비교함

    • 데이터베이스의 레코드는 아이디와 비밀번호가 일치하면 요텅받은 일을 수행하고, 아니면 거부함

  • Basic 인증의 문제점

    • 아이디와 비밀번호를 노출

      • 중간에 누군가 HTTP 요청을 가로채 문자열을 디코딩하면 아이디와 비밀번호를 알아낼 수 있음 (MITM)

      • HTTP와 사용하기엔 취약하여, 반드시 HTTPS와 사용해야 함

    • 사용자를 로그아웃시킬 수 없음

    • 사용자의 계정 정보가 있는 저장 장소, 인증 서버와 인증 DB에 과부하가 걸릴 확률이 높음

      • 서버가 단일 장애점이 됨

    !! 사진사진

4.1.2 토큰 기반 인증

  1. 토큰 기반 인증

    • 토큰(Token)은 사용자를 구별할 수 있는 문자열

    • 토큰은 최초 로그인 시 서버가 만들어 줌.

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

      Authorization: Bearer Nnfjadshgskdhglkjrnglkjreblgkjerbdgkjsrbg
      • 토큰을 기반으로 하는 요청은 헤더에 위와같이 Authorization: Bearer <TOKEN>을 명시

      • 서버는 이 토큰을 받고 어떤 형태로든 인증을 해야 함

      • (토큰 이읆만 바뀐 세션)

    • ex

      • 서버가 랜덤한 문자와 숫자를 섞어 UUID로 토큰을 작성해 넘긴다고 가정

      • 서버는 토큰을 생성해 인증 서버를 통해 저장

      • 요청을 받을 때마다 헤더의 토큰을 서버의 토큰과 비교해 클라이언트를 인증

  • Basic Auth vs 토큰 기반 인증

    • 아이디와 비밀번호를 매번 네크워크를 통해 전송해야 할 필요가 없음

      • 이는 보안 측면에서 좀 더 안전함

    • 서버가 토큰을 마음대로 생성할 수 있으므로 사용자의 인가 정보(예 : User, Admin 등) 또는 유효 시간을 정해 관리 가능

      • 디바이스마다 다른 토큰을 생성해 주고 디바이스마다 유효 시간을 다르게 정하거나 임의로 로그아웃을 할 수도 있음

4.1.3 JSON 웹 토큰

  1. JSON 웹 토큰

    • 서버에서 전자 서명된 토큰을 이용하면 인증에 따른 스케일 문제를 해결 가능

    • {header}.{payload}.{signature}로 구성

      Authorization: Bearer ejsldkf.saeff;dgjad.fejfwiefk
      위를 디코딩하면 
      • 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

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

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

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

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

4.3 Spring Security 통합

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

  • 문제점

    • 로그인 여부를 저장하지 않음

    • API 들이 사용자를 인증하지 않음

    • 패스워드 암호화하지 않음

4.3.1 JWT 생성 및 반환 구현

  • 토큰 발행 : JWT library

  • 로그인 시 토큰을 반환

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

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

// <https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt>
implementation group: 'io.jsonwebtoken', name:'jjwt', version: '0.9.1'
  • security 패키지 생성

    • 인증을 위한 클래스 관리

    • 인가를 위한 클래스 관리

2. 토큰 발행

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. 로그인 시 토큰 반환

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 스프링 시큐리티와 서블릿 구현

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

  • 스프링 시큐리티

    • 토큰 인증을 위해 컨트롤러 메서드의 첫 부분마다 인증 코드 작성해야하는 문제점

    • 이증과 인가를 위한 다양한 기능 제공

    • 서블릿 필터의 집합

  • 서블릿 필터

    • 서블릿 실행 전에 실행되는 클래스

    • 스프링이 구현하는 디스패처 서블릿이 실행되기 전에 항상 실행

    • 1) 개발자는 서블릿 필터를 구현하고 2) 서플릿 필터를 서블릿 컨테이너가 실행하도록 설정

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

  • 서블릿 필터에서 스프링 시큐리티의 위치와 우리가 구현할 필터의 위치

    • 스프링 시큐리티가 FilterChainProxy라는 필터를 서블릿 필터에 끼워 줌

    • 이 FilterChainProxy 클래스 안에서 내부적으로 필터 실행

    • 이 필터들이 스프링이 관리하는 스프링 빈 필터

      • 상속하는 필터는 OncePerRequestFilter

      • 필터 설정하기 위해 상속하는 클래스는 WebSecurityConfigurerAdapter

4.3.3 JWT를 이용한 인증 구현

서블릿 필터 구현

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

implementation 'org.springframework.boot:spring-boot-starter-security'
  1. OncePerRequestFilter를 상속하는 JwtAuthenticationFilter 구현

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 스프링 시큐리티 설정

서블릿 컨테이너에 이 서블릿 필터를 사용하라고 알려줘야 함

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

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에서 인증된 사용자 사용하기

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 패스워드 암호화

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

  • UserService

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

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

Last updated