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에 저장하면 안전할 것 같지만 다음과 같은 문제점들이 있다.
- 동일한 메시지는 동일한 다이제스트를 갖는다.
- 해커들이 여러 값들을 대입해보면서 얻었던 다이제스트들을 모아둔 테이블을 레인보우 테이블이라고 한다.
- 많이 사용하는 Password나 복잡하지 않은 암호의 경우 이미 해당 패스워드의 다이제스트가 레인보우 테이블에 있을 가능성이 높다.
- 무차별 대입 공격 (브루트포스)
- 해시함수는 원래 빠른 데이터 검색을 위한 목적으로 설계된 것이다.
- 그래서 해시 함수를 써도 원문의 다이제스트는 금방 얻어진다.
- 해커는 무작위의 데이터들을 계속 대입해보면서 얻은 다이제스트와 해킹할 대상의 다이제스트를 계속 비교해서 찾아낸다.
단방향 해시 함수 보완
- 해시 함수 여러 번 수행하기 - 키 스트레칭
- SHA-256을 돌려서 나온 다이제스트를 다시 SHA-256에 돌린다.
- 해시 함수를 여러번 돌리는 만큼 최종 다이제스트를 얻는데 그만큼 시간이 소요된다.
- 솔트 (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);
}
}