0315 - 0321

0315

Controller에서 데이터 받아오는 방법

@RequestParam

  • query string 방식으로 url을 통해 parameter로 값을 받아온다.

@PathVariable

@GetMapping("/test/{email}")
public String test(@PathVariable("email") String email) {
	return "test";
}
  • {email} 이라는 부분에 매핑된다.

@RequestBody

  • 클라이언트가 전송하는 Http 요청의 Body 내용을 Java Object로 변환시켜주는 역할을 한다.
  • Get 방식에는 http body가 존재하지 않기 때문에, 반드시 Post 방식을 사용해야 한다.
  • Post방식으로 Json의 형태로 넘겨온 데이터를 객체로 바인딩하기 위해 사용한다.

@ModelAttribute

  • @RequestParam을 사용하고 DTO/VO와 같이 객체 전체를 받을 경우 사용한다.

0316 ~ 0318

새로운 노트북 macboook m1에 환경설정을 하고 jekyll을 설치하는데 시간을 보냈다.

0319

예외처리

기타 여러 예외 처리들을 적용하다보면 코드가 엄청나게 복잡해져서 비즈니스 로직에 집중하기가 어렵다. -> 이런 문제를 개선하기 위해 @ExceptionHandler와 @ControllerAdvice를 사용한다.

@ExceptionHandler

@Controller @RestController가 적용된 Bean내에서 발생하는 예외를 잡아서 하나의 메서드에서 처리해주는 기능을 한다.

@RestController public class MyRestController {
	...
	...
	@ExceptionHandler(NullPointerException.class)
	public Object nullex(Exception e) {
		System.err.println(e.getClass());
		return "myService";
	}
}
  • ExceptionHandler라는 어노테이션을 쓰고 인자로 캐치하고 싶은 예외클래스를 등록해주면 된다.
  • @ExceptionHandler({ Exception1.class, Exception2.class}) 이런식으로 두 개 이상 등록도 가능하다.

특징

  • Controller, RestController에만 적용가능하다.
  • 리턴 타입은 자유롭게 해도 된다.
  • @ExceptionHandler를 등록한 Controller에만 적용된다. 다른 Controller에서 NullPointerException이 발생하더라도 예외를 처리할 수 없다.

ControllerAdvice

@ExceptionHandler가 하나의 클래스에 대한 것이라면, @ControllerAdvice는 모든 @Controller 즉, 전역에서 발생할 수 있는 예외를 잡아 처리해주는 annotation이다.

@RestControllerAdvice
public class MyAdvice {
	@ExceptionHandler(CustomException.class)
	public String custom() {
		return "hello custom";
	}
}
  • 클래스파일을 만들어서 annotation을 붙이기만 하면 된다. 그 다음에 @ExceptionHandler로 처리하고 싶은 예외를 잡아 처리하면 된다.
  • 별도의 속성값이 없이 사용하면 모든 패키지 전역에 있는 컨트롤러를 담당하게 된다.
  • @RestControllerAdvice와 @ControllerAdvice가 존재하는데 ViewResolver를 통해서 예외 처리 페이지로 리다이렉트 시키려면 @ControllerAdvice만 써도 되고, API서버여서 에러 응답으로 객체를 리턴해야한다면 @ResponseBody 어노테이션이 추가된 @RestControllerAdvice를 적용하면 된다.

0320

MessageSource

ApplicationContext가 상속받는 것중 하나로 메시지 설정 파일을 모아 놓고 각 국가에 맞는 메시지를 제공할 수 있다.

  • Spring Boot에서는 ResourceBundleMessageSource가 Bean으로 등록되어 있어 .properties만 설정되어 있으면 메시지를 가져와 사용할 수 있다.
//messages.properties
greeting=Hello, my name is {0}

//message_ko_KR.properties
greeting=안녕하세요,  이름은 {0}

//Runner
MessageSource.getMessage("greeting", new String[]{"richard"}, Locale.KOREA);

MessageSource.getMessage

  • MessageSource.getMessage(String code, Object[] args, String default, Locale loc);
    -> 가장 기본적인 것으로, 특정 로케일 내에서 발견되는 메시지가 없으면 default message가 반환된다.
  • MessageSource.getMessage(String code, Object[] args, Locale loc);
    -> 해당 메소드는 default message가 없어 특정 로케일 내에서 발견되는 메시지가 없으면 NoSuchMessageException이 발생한다.

0321

해싱

해싱과 암호화의 가장 큰 차이는 ‘방향성’이다. 단방향, 즉 복호화가 불가능하다는 것이고 이를 ‘해싱’이라 부른다. 반면에 ‘암호화’는 해싱과는 다르게 역으로 복호화도 가능한 양방향이다.

단방향 해시 함수

  • 평문으로 패스워드를 저장하면 보안적으로 매우 취약하다.
  • 단방향 해시 함수는 어떤 수학적 연산에 의해 원본 데이터를 매핑시켜 다른 암호화된 데이터로 변환시키는 것
  • 이 변환을 해시라고 하고, 해시에 의해 암호화된 데이터를 다이제스트라고 한다.
  • 단방향 해시 함수는 다이제스트를 복호화할 수 없어야 한다.

SHA-256

대표적인 단방향 해시 함수 알고리즘 중 하나

  • 사용자로부터 입력받은 정보를 그대로 저장하는게 아니라 해싱을 해서 저장하기 때문에 DB를 해킹당해서 값을 얻었다고 한들 기존 원래 패스워드를 유추하기 힘들게 된다.

단방향 해시 함수의 한계점

SHA-256 같은 해시 함수를 이용하여 다이제스트를 DB에 저장하면 안전할 것 같지만 다음과 같은 문제점들이 있다.

  1. 동일한 메시지는 동일한 다이제스트를 갖는다.
    • 해커들이 여러 값들을 대입해보면서 얻었던 다이제스트들을 모아둔 테이블을 레인보우 테이블이라고 한다.
    • 많이 사용하는 Password나 복잡하지 않은 암호의 경우 이미 해당 패스워드의 다이제스트가 레인보우 테이블에 있을 가능성이 높다.
  2. 무차별 대입 공격 (브루트포스)
    • 해시함수는 원래 빠른 데이터 검색을 위한 목적으로 설계된 것이다.
    • 그래서 해시 함수를 써도 원문의 다이제스트는 금방 얻어진다.
    • 해커는 무작위의 데이터들을 계속 대입해보면서 얻은 다이제스트와 해킹할 대상의 다이제스트를 계속 비교해서 찾아낸다.

단방향 해시 함수 보완

  1. 해시 함수 여러 번 수행하기 - 키 스트레칭
    • SHA-256을 돌려서 나온 다이제스트를 다시 SHA-256에 돌린다.
    • 해시 함수를 여러번 돌리는 만큼 최종 다이제스트를 얻는데 그만큼 시간이 소요된다.
  2. 솔트 (Salt)
    • 해시함수를 돌리기 전에 원문에 임의의 문자열을 덧붙이는 것
    • 사용자마다 다른 Salt를 사용한다면 설령 같은 비밀번호더라도 다이제스트의 값은 다르다.
public class SHA256Util {

    /**
     * 난수 SALT를 발생시킴
     * @return
     */
    public static String generateSalt() {

        Random random = new Random();

        byte[] salt = new byte[8];
        random.nextBytes(salt);

        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < salt.length; i++) {
            // byte 값을 Hex 값으로 바꾸기.
            sb.append(String.format("%02x",salt[i]));
        }

        return sb.toString();
    }

    /**
     * 비밀번호와 salt를 조합해 암호화 처리
     * @param pwd - 비밀번호
     * @param salt - 솔트 값
     * @return
     */
    public static String getEncrypt(String pwd, String salt) {

        String result = "";

        byte[] pwdBytes = pwd.getBytes();
        byte[] saltBytes = salt.getBytes();
        byte[] bytes = new byte[pwdBytes.length + saltBytes.length];

        System.arraycopy(pwdBytes, 0, bytes, 0, pwdBytes.length);
        System.arraycopy(saltBytes, 0, bytes, pwdBytes.length, saltBytes.length);

        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            md.update(bytes);

            byte[] b = md.digest();

            StringBuffer sb = new StringBuffer();

            for(int i=0; i<b.length; i++) {
                sb.append(Integer.toString((b[i] & 0xFF) + 256, 16).substring(1));
            }

            result = sb.toString();

        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }

        return result;
    }

    /**
     * 난수 솔트를 생성하여 저장하고, 비밀번호를 암호화하여 저장한다.
     * @param member - 비밀번호를 암호화할 유저
     * @return
     */
    public static void setEncrypt (Member member) {

        String salt = generateSalt();
        member.setSalt(salt);

        String password = getEncrypt(member.getPassword(), salt);
        member.setPassword(password);
    }
}

태그:

카테고리:

업데이트: