왜 수동으로 등록해야할까? 같은 부류의 bean을 제어 가능 : 특정한 설정이나 제어가 필요한경우 묶을수 있음. 공통적인 관심사를 처리할때 (기술 지원 bean) : 로깅 보안등 공통된 관심사를 처리 문제가 발생했을때 문제 추적 용이때문에
그외에는 자동으로 등록하는게 좋고, 편하기도 하다.
수동으로 bean을 정의하는법 프로젝트 최상단 부분에 config 파일을 만든다
@Configuration
public class PasswordConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
그리고 Configuration어노테이션 을 정의해준다 configuration으로 정의하면 spring이 시작될때 자동으로 @bean을 정의한다.
@SpringBootTest
public class PasswordEncoderTest {
@Autowired
PasswordEncoder passwordEncoder;
@Test
@DisplayName("수동 등록한 passwordEncoder를 주입 받아와 문자열 암호화")
void test1() {
String password = "Robbie's password";
// 암호화
String encodePassword = passwordEncoder.encode(password);
System.out.println("encodePassword = " + encodePassword);
String inputPassword = "Robbie";
// 해시된 비밀번호와 사용자가 입력한 비밀번호를 해싱한 값을 비교
boolean matches = passwordEncoder.matches(inputPassword, encodePassword);
System.out.println("matches = " + matches); // 암호화할 때 사용된 값과 다른 문자열과 비교했기 때문에 false
}
}
문자열을 암호화 하는 메서드를 정의해보자. @Autowired를 통해 이미 등록된 bean을 등록한다
passwordEncoder의 .encode로 암호화를 진행한다 matchs로 암호화를 해싱한값과 사용자가 입력한 비밀번호를 비교
같은 타입의 bean이2개 이상이 있을때.
package com.sparta.springauth.food;
public interface Food {
void eat();
}
package com.sparta.springauth.food;
import org.springframework.stereotype.Component;
@Component
public class Chicken implements Food {
@Override
public void eat() {
System.out.println("치킨을 먹습니다.");
}
}
package com.sparta.springauth.food;
import org.springframework.stereotype.Component;
@Component
public class Pizza implements Food {
@Override
public void eat() {
System.out.println("피자를 먹습니다.");
}
}
인터페이스 food를 상속받은 chiken과 pizza가 component 어노테이션으로 인해 자동으로 bean에 주입되었을때
spring은 어떤 food의 객체를 가져와야할지 모른다 어떻게 해결해야할까?
1. 필드명으로 명시해주기.
@Autowired
Food pizza;
@Autowired
Food chicken;
이럴때는 명시를 해주면 자동으로 bean을 받아올 수 있다. 필드명에 주입하는 방식은 직관적인 장점이 있다.
기본적으로 Autowired는 bean을 주입할때 타입으로 찾는데 만약에 찾지 못했을경우 필드명을 이용하여 매칭을 시도한다.
2. primary 어노테이션 적용
@Component
@Primary
public class Chicken implements Food {
@Override
public void eat() {
System.out.println("치킨을 먹습니다.");
}
}
@SpringBootTest
public class BeanTest {
@Autowired
Food food;
}
primary 어노테이션을 사용하면 같은 타입의 bean중에 기본으로 선택될 bean을 지정한다.
3. qualifier 사용
@Component
@Qualifier("pizza")
public class Pizza implements Food {
@Override
public void eat() {
System.out.println("피자를 먹습니다.");
}
}
@SpringBootTest
public class BeanTest {
@Autowired
@Qualifier("pizza")
Food food;
}
또한 qualifier 어노테이션으로 지정해주면 주입하고자 하는 필드에 qulifier를 지정해주면 해당 객체가 주입된다.
qulifier의 우선순위가 primary 보다 더 높다 같은 타입의 bean이 여러개 있을때 범용적으로 사용되는 bean 객체에는 primary를 설정하고 지엽적으로 사용되는 bnean 객체에는 qualifier를 사용한다
인증과 인가
인증 해당 유저가 실제 유저인지 확인하는것 로그인 같은 개념
인가 해당 유저가 특정 권한을 가지고 있는지 관리자 메뉴 같은것
인증이 필요한 이유 클라이언트와 서버의 연결문제가 있다.
서버와 클라이언트 간의 네트워킹에는 http라는 프로토콜로 통신을하는데. 비연결성과 무상태로 이루어진다
비연결성 서버와 클라이언트는 연결이 되어있지 않다는 뜻이다 게임 같은 경우에는 실시간으로 연결상태를 확인해야 하지만 일반적인 웹의 경우에는 그때그때 확인을 하는것이 리소스 절약에 좋은편이다 즉, 서버는 하나의 요청에 응답을 보낸뒤 연결을 끊어놓는다고 생각하면 된다.
무상태 서버는 클라이언트의 상태를 저장하지 않는다는것이다 이는 restful 원칙에도 적용되듯이 서버는 클라이언트의 그때그때의 요청에만 집중하는것이 리소스 소모를 줄일 수있다.
그렇다면, 우리는 로그인하고 웹 페이지를 이리저리 옮겨다니는데 로그인은 유지되어있다! 를 설명해보겠다.
쿠키 - 세션방식. 서버가 특정 사용자의 로그인 상태를 확인하기위해 최소한의 정보를 가지고 로그인을 유지시키는 방법 출처 : 스파르타코딩클럽 스프링 숙련 1주차 4강사용자가 로그인을 하면 서버는 그 회원정보를 바탕으로 로그인을 확인하고 유저 정보와 관련이 없는 난수인 세션을 생성해주며, 클라이언트에게 보내준다. 클라이언트는 받은 세션ID를 쿠키라는 저장소에 저장해주며, 클라이언트는 요청을 보낼때 쿠키를 http 헤더에 함께 보내 요청을 하고 서버는 그 쿠키를 검증하여, 해당 사용자를 판별하는것이다.
쿠키 클라이언트에 저장될 목적으로 생성한 사용자의 정보를 담은 파일 구성요소 1. Name : 쿠키를 식별하는데 사용되는 키 (중복 불가) 2. value : 쿠키에 저장되는 값 3. Domain : 쿠키가 저장된 도메인 4. Path : 쿠키가 사용되는 경로 5. Expires : 쿠키의 만료기한
세션 서버에서 관리되는 사용자의 상태정보이다. 출처 : 스파르타코딩클럽 스프링 숙련 1주차 4강
1. 클라이언트가 서버에 로그인 요청을 함 2. 서버가 세션ID를 생성후 쿠키에 담아 헤더에 전달 3. 클라이언트가 쿠키에 세션ID 저장 4. 클라이언트가 서버에게 요청을함 여기서 쿠키값을 포함하여 요청함 5. 서버가 세션 ID를 확인하고 1번과 같은 클라이언트임을 확인
쿠키 예제
public static void addCookie(String cookieValue, HttpServletResponse res) {
try {
cookieValue = URLEncoder.encode(cookieValue, "utf-8").replaceAll("\\+", "%20"); // Cookie Value 에는 공백이 불가능해서 encoding 진행
Cookie cookie = new Cookie(AUTHORIZATION_HEADER, cookieValue); // Name-Value
cookie.setPath("/");
cookie.setMaxAge(30 * 60);
// Response 객체에 Cookie 추가
res.addCookie(cookie);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e.getMessage());
}
}
addCookie Cookie Value는 공백이 불가능하기때문에 URLEncoder로 공백을 치환해준다. cookie 생성자에 name과 value를 담는다 setPath로 쿠키가 적용되는 경로를 지정하고 MaxAge를 이용해 수명을 정한다 여기서는 1800초 response객체에 데이터를 넣으면 클라이언트로 반환이 된다
CookieValue 어노테이션을 사용하여 AUTHORIZATION_HEADER 이름을 가진 쿠키의 값을 가져온다.
세션 예제 세션은 사용자의 정보를 확인하고 세션id를 만들어줘야한다.
@GetMapping("/create-session")
public String createSession(HttpServletRequest req) {
// 세션이 존재할 경우 세션 반환, 없을 경우 새로운 세션을 생성한 후 반환
HttpSession session = req.getSession(true);
// 세션에 저장될 정보 Name - Value 를 추가합니다.
session.setAttribute(AUTHORIZATION_HEADER, "Robbie Auth");
return "createSession";
}
클라이언트의 요청을 처리하기위해 HttpServletRequest 객체를 사용한다. HttpSesstion 객체를 만들어 getSeetion 메서드를 요청하면 세션이 존재할 경우 세션을 반환 없을경우 새로운 세션을 만들어준다.
setAttribute를 사용하여 name과 value를 넣어준다.
@GetMapping("/get-session")
public String getSession(HttpServletRequest req) {
// 세션이 존재할 경우 세션 반환, 없을 경우 null 반환
HttpSession session = req.getSession(false);
String value = (String) session.getAttribute(AUTHORIZATION_HEADER); // 가져온 세션에 저장된 Value 를 Name 을 사용하여 가져옵니다.
System.out.println("value = " + value);
return "getSession : " + value;
}
getSettion을 위와 반대로 세션이 존재할경우 세션을 반환, 세션이 없을 경우 null을 반환한다 , 새로만들 필요는 없으니까.
또 getAttribute(name)을 하면 value값을 받아올 수 있다.
물론 위 예제는 기본적인 session과 coockie 생성 예제이다
쿠키의 문제점 쿠키와 세션은 사용자의 로그인 상태를 유지하는데 유용하지만. 크로스 사이트 스크립팅(XSS)와 같은 보안 취약점에 노출될 수 있다. 공격자가 상대방의 브라우저에 스크립트가 실행되도록 유도해 사용자의 세션을 가로채거나 악의적 콘텐츠 등을 삽입하는 행위이다
xss의 종류에는 크게 세가지 유형이 있다. DOM-based Xss 클라이언트 측에서 발생하는 xss 유형이며, 웹 페이지의 DOM을 조작하여 발생한다. 자바스크립트가 DOM을 변경하는 과정에서 악성 스크립트가 삽입된다는 것이다. 클라이언트 쪽에서 악성 스크립트가 담긴 URL을 방물할때와 같은 경우 발생된다. 클라이언트는 서버쪽에 데이터를 전달할때 관련 스크립트를 전송하지않고, 데이터를 전송 받았을때 해당 스크립트를 매핑하여 공격자가 데이터를 받는 유형이라 서버측에서 탐지하기는 어렵다.
Reflected Xss 서버에서 즉시 반영되는 사용자의 입력을 악용
Stored Xss 해커가 db에 악의적인 스크립트를 저장하여, 클라이언트가 악의적인 스크립트를 참조할때 예를들어 사용자가 투명한 글씨로 악성 스크립트를 적어 댓글 같은곳에다 넣는경우, 그 글을 클릭한 클라이언트는 그 스크립트를 그대로 받게된다
JWT 기반 인증 JWT는 인증에 필요한 정보를 서명한 토큰이다. 쿠키 저장소를 통하여 JWT를 저장한다. JWT토큰을 HTTP헤더에 실어서 서버가 클라이언트를 식별한다
쿠키의 문제점 쿠키는 그때그때의 세션을 서버가 가지고 있어야한다. 서버 1에 클라이언트의 세션 정보를 가지고있는데 서버2나 3에 요청을한다면? 그러기 위해서 세션 스토리지를 두었다.
하지만 세션은 그때그때 사용할 각 유저의 세션을 위해 데이터베이스에 저장해야한다 JWT 토큰은 서버에서 생성되어 클라이언트에 보낸후 클라이언트에만 정보를 보관한다 서버는 토큰을 클라이언트에 전달할때 암호화, 받은 JWT토큰을 원래 값과 같은지 검증하는것이다.
서버는 무결성 검증만 수행하여 부담을 줄이게 된다. 사용자가 로그인을 하면 서버는 그 회원정보를 바탕으로 로그인을 확인하고 유저에 대한 정보를 JWT로 암호화를 하여, 응답에 실어서 보내준다. 클라이언트는 데이터요청을 했을때 JWT를 실어서 보낸다 서버는 JWT를 검증하여 응답을 내준다
출처 : 스파르타코딩클럽 스프링 숙련 1주차 4강
JWT의 흐름 클라이언트가 로그인을 요청 서버는 비밀키를 사용하여 JWT로 암호화 서버에서 JWT를 담아 클라이언트 응답에 전달함 로그인 완료 클라이언트에서 API 요청 서버는 받은 데이터에서 JWT토큰 분석 토큰이 일치하면 데이터를 반환해줌
JWT의 장단점 장점 Stateless(무상태) 인증을 가능하게 한다. 서버는 사용자의 정보를 관리 할 필요가 없게되므로 메모리 사용량이 줄어든다 클라이언트와 서버가 다른 도메인을 사용할때
단점 구현의 복잡도가 증가한다 네트워크에 담기는 커진다는 단점 무상태이기 때문에 서버에서 무효화가 어려운 문제가 있다. key 유출시 조작 가능
구현 방법
클라이언트에서 jwt를 통해 인증하는 방법
// HttpServletRequest 에서 Cookie Value : JWT 가져오기
public String getTokenFromRequest(HttpServletRequest req) {
Cookie[] cookies = req.getCookies();
if(cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(AUTHORIZATION_HEADER)) {
try {
return URLDecoder.decode(cookie.getValue(), "UTF-8"); // Encode 되어 넘어간 Value 다시 Decode
} catch (UnsupportedEncodingException e) {
return null;
}
}
}
}
return null;
}
쿠키에 담긴 정보가 여러개일 수 있기때문에 쿠키를 순회하며 AUTHORIZETION_HEADER과 동일한지 검증한다.