📢 오늘의 목표 📢
✔️ 알고리즘, SQL 문제 풀이
✔️ 알고리즘 코드카타
✔️ SQL 코드카타
✔️ 프로그래머스 Level 2
✔️ 스프링 강의 숙련 주차
✔️ 수준별 수업 - 챌린지반
⏱️ 오늘의 일정 ⏱️
9:00 ~ 10:00 - 알고리즘 코드 카타
10:00 ~ 11:00 - 스프링 숙련 주차 발제
11:00 ~ 13:00 - 스프링 숙련 강의 듣기
13:00 ~ 14:00 - 점심시간
14:00 ~ 18:00 - 스프링 숙련 강의 듣기
18:00 ~ 19:00 - 저녁 시간
19:00 ~ 20:00 - TIL 작성
20:00 ~ 21:00 - 수준별 수업 : 챌린지반
📜 Chapter 1. 알고리즘 코드 카타
📜 Chapter 2. 스프링 숙련 주차 발제
들어야 할 강의들과 과제의 양을 보니 머리가 아프다...
그러나! 스프링 시큐리티, AOP 같은 건 이미 해 본 부분이고 역시 예습의 효과가 있으니 그렇게 어려울 것 같진 않다.
물론 JWT같은 건 처음 보는 개념이지만!
그래서 일단 이번 주에 전부 완료하기라는 터무니 없어 보이는 목표를 잡을 것이다 ㅋㅋㅋ
당연히 12단계까지 다 끝내기!
일단 오늘은 3주차 강의를 다 듣는 걸로 목표를 잡았다.
📜 Chapter 3. 스프링 숙련 강의 듣기
✔️ 3-1 Bean을 수동으로 등록하는 방법
보통 @Component를 사용하여 Bean을 자동으로 등록하나, 기술적인 문제나 공통적인 관심사를 처리할 때 사용하는 객체들은 수동으로 등록하는 것이 좋다. 이런 기능을 기술 지원 Bean이라 부르는데 비즈니스 로직 Bean 보다는 수가 적어 수동으로 등록하기 힘들지 않다. 또한 수동등록된 Bean에서 문제가 발생했을 때 해당 위치를 파악하기 쉽다는 장점이 있다.
@Configuration
public class PasswordConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Bean으로 등록하고자 하는 객체를 반환하는 메서드를 선언하고 @Bean을 설정.
이 메서드가 속한 클래스에는 @Configuration을 설정.
@Autowired
PasswordEncoder passwordEncoder;
사용할 곳에서 @Autowired 에너테이션을 통해 컨테이너에서 가져온다.
✔️ 3-2 같은 타입의 Bean이 2개라면?
@Autowired
Food pizza;
@Autowired
Food chicken;
Bean은 클래스 이름의 소문자로 등록이 되기 때문에 이런 식으로 원하는 Bean을 가져올 수 있다.
@Component
@Primary
public class Chicken implements Food {
@Override
public void eat() {
System.out.println("치킨을 먹습니다.");
}
}
@Primary 애너테이션으로 우선순위 높은 Bean을 설정할 수도 있다.
@Component
@Qualifier("pizza")
public class Pizza implements Food {
@Override
public void eat() {
System.out.println("피자를 먹습니다.");
}
}
@Qualifier() 애너테이션으로 id를 매칭 해줄 수도 있는데, 이 경우 Primary보다 더 우선순위가 높아진다.
@Autowired
@Qualifier("pizza")
Food food;
대신 꺼내오는 쪽에서도 id를 지정해줘야 해서 귀찮다.
보통 전역적으로 사용하는 쪽이 @Primary, 지역적으로 사용하는 쪽이 @Qualifier 애너테이션을 사용한다.
✔️ 3-3 인증과 인가란 무엇일까?
인증(Authentication) - 해당 유저가 실제 유저인지 인증 (로그인을 하는 것)
인가(Authorization) - 해당 유저가 특정 리소스에 접근이 가능한지 허가를 확인 (로그인 했는지 확인하는 것)
서버와 클라이언트는 Http 프로토콜을 이용해 비연결성(Connectionless), 무상태(Stateless)로 통신을 하고 있음.
즉 서버와 클라이언트는 실제로 연결 되어있지 않고(요청~응답 시에만 연결 되어있다 끊음) 서버는 클라이언트의 상태를 저장하지 않고 있다.
그럼 어쩧게 유저가 인증 되었다는 정보를 유지시켰을까?
1. 쿠키 세션 방식의 인증 - 인증과 관련된 최소한의 정보를 저장해 로그인을 진행
2. JWT 기반 인증 - 인증에 필요한 정보들을 암호화시킨 토큰으로 서버가 클라이언트를 식별
차이- 쿠키 세션 방식은 세션 저장소에 따로 상태를 저장할 필요가 있으나 JWT는 필요 없다. JWT가 더 효율이 좋다.
✔️ 3-4 쿠키와 세션이란 무엇일까?
쿠키와 세션 모두 HTTP에 상태 정보를 유지하기 위해 사용됨.
쿠키 - 클라이언트 저장
클라이언트에 정보를 저장할 목적으로 사용.
- Name(이름): 고유키
- Value(값): 쿠키의 값
- Domain(도메인): 쿠키가 저장된 도메인
- Path(경로): 쿠키가 사용되는 경로
- Expires(만료기한): 쿠키의 만료기한
세션 - 서버 저장
일정시간 동안 클라이언트 상태를 유지하기 위해 사용. 클라이언트 별로 고유한 '세션 ID'를 부여해 클라이언트 별 필요한 정보를 서버에 저장. 이 '세션 ID'는 클라이언트의 쿠키값(세션 쿠키)으로 저장됨.
public static final String AUTHORIZATION_HEADER = "Authorization";
@GetMapping("/create-cookie")
public String createCookie(HttpServletResponse res) {
addCookie("Robbie Auth", res);
return "createCookie";
}
@GetMapping("/get-cookie")
public String getCookie(@CookieValue(AUTHORIZATION_HEADER) String value) {
System.out.println("value = " + value);
return "getCookie : " + value;
}
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());
}
}
쿠키 사용
@GetMapping("/create-session")
public String createSession(HttpServletRequest req) {
// 세션이 존재할 경우 세션 반환, 없을 경우 새로운 세션을 생성한 후 반환
HttpSession session = req.getSession(true);
// 세션에 저장될 정보 Name - Value 를 추가합니다.
session.setAttribute(AUTHORIZATION_HEADER, "Robbie Auth");
return "createSession";
}
@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;
}
세션 사용
✔️ 3-5 JWT란 무엇일까?
JSON 포멧을 이용해 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token.
서버가 2대 이상인 경우 세션마다 다른 client 로그인 정보를 가지고 있을 수 있음.
- 이 때 Sticky Session 방식으로 Client 마다 요청 Server를 고정하는 방법,
- 세션 저장소를 생성하여 모든 세션을 저장하는 방법이 있다.
- 또 다른 방법으로는 로그인 정보를 Server에 저장하지 않고 JWT를 사용하는 방법이 있다.
장점
- 동시 접속자가 많을 때 서버 측 부하를 낮춤 - Secret Key라는 단 하나의 값으로 검증 가능. 세션 저장소 불필요.
- Client, Server가 다른 도메인을 사용할 때 사용 가능
단점
- 구현의 복잡도 증가
- JWT에 담는 내용이 커질 수록 네트워크 비용 증가
- 기 생성된 JWT를 일부만 만료시킬 방법이 없음
- Secret Key가 유출된 경우 JWT 조작이 가능
1. 로그인 성공 시 서버에서 로그인 정보를 JWT로 암호화 해 쿠키를 생성해서 클라이언트에 전달한다.
2. 서버에서 API 요청 시마다 쿠키에 포함된 JWT를 찾아 사용한다. 이 때 Secret Key를 사용해 위조 여부를 검증하고, JWT 유효기간이 지나지 않았는지 검증한다.
✔️ 3-6 JWT 다루기
JWT Util 클래스 만들기
@Component
public class JwtUtil {
// Header KEY 값
public static final String AUTHORIZATION_HEADER = "Authorization";
// 사용자 권한 값의 KEY
public static final String AUTHORIZATION_KEY = "auth";
// Token 식별자
public static final String BEARER_PREFIX = "Bearer ";
// 토큰 만료시간
private final long TOKEN_TIME = 60 * 60 * 1000L; // 60분
@Value("${jwt.secret.key}") // Base64 Encode 한 SecretKey
private String secretKey;
private Key key;
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 로그 설정
public static final Logger logger = LoggerFactory.getLogger("JWT 관련 로그");
@PostConstruct
public void init() {
byte[] bytes = Base64.getDecoder().decode(secretKey);
key = Keys.hmacShaKeyFor(bytes);
}
// 토큰 생성
public String createToken(String username, UserRoleEnum role) {
Date date = new Date();
return BEARER_PREFIX +
Jwts.builder()
.setSubject(username) // 사용자 식별자값(ID)
.claim(AUTHORIZATION_KEY, role) // 사용자 권한
.setExpiration(new Date(date.getTime() + TOKEN_TIME)) // 만료 시간
.setIssuedAt(date) // 발급일
.signWith(key, signatureAlgorithm) // 암호화 알고리즘
.compact();
}
// JWT Cookie 에 저장
public void addJwtToCookie(String token, HttpServletResponse res) {
try {
token = URLEncoder.encode(token, "utf-8").replaceAll("\\+", "%20"); // Cookie Value 에는 공백이 불가능해서 encoding 진행
Cookie cookie = new Cookie(AUTHORIZATION_HEADER, token); // Name-Value
cookie.setPath("/");
// Response 객체에 Cookie 추가
res.addCookie(cookie);
} catch (UnsupportedEncodingException e) {
logger.error(e.getMessage());
}
}
// JWT 토큰 substring
public String substringToken(String tokenValue) {
if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
return tokenValue.substring(7);
}
logger.error("Not Found Token");
throw new NullPointerException("Not Found Token");
}
// 토큰 검증
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (SecurityException | MalformedJwtException | SignatureException e) {
logger.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
} catch (ExpiredJwtException e) {
logger.error("Expired JWT token, 만료된 JWT token 입니다.");
} catch (UnsupportedJwtException e) {
logger.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
} catch (IllegalArgumentException e) {
logger.error("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
}
return false;
}
// 토큰에서 사용자 정보 가져오기
public Claims getUserInfoFromToken(String token) {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
아직 잘 이해는 못 했다. 음 그렇구나 하고 넘어가는 중.
쿠키에 담아서 사용하는 방식을 채택하였기 때문에 컨트롤러에서는 쿠키를 사용할 때와 비슷하다.
✔️ 3-7 회원가입 구현
패스워드는 개인정보 보호법 때문에 암호화를 꼭 해야하며, 양방향 암호 알고리즘은 사용할 수 없다.
대신 matches 메서드를 이용한다.
회원 가입은 중복 체크가 필요하다.
// 회원 중복 확인
Optional<User> checkUsername = userRepository.findByUsername(username);
if (checkUsername.isPresent()) {
throw new IllegalArgumentException("중복된 사용자가 존재합니다.");
}
Optional 객체가 유효하면 예외 발생시킨다.
✔️ 3-8 로그인 구현 JWT
@PostMapping("/user/login")
public String login(LoginRequestDto requestDto, HttpServletResponse res) {
try {
userService.login(requestDto, res);
} catch (Exception e) {
return "redirect:/api/user/login-page?error";
}
return "redirect:/";
}
로그인을 구현.
try 문을 쉽게 넣으려면 원하는 메서드 뒤에 .try를 입력하고 엔터.
여기서는 클라이언트와 에러 페이지에 대한 약속을 하여 로그인에 실패할 경우 ?error 창으로 이동.
📜 Chapter 4. 수준별 수업 챌린지반
오늘은 메모리 관리에 대한 수업을 진행했다.
가비지 컬렉터, 힙 영역의 동작 원리에 대해서는 처음 들어서 이게 가장 유익했던 것 같다.
특히 가비지 컬렉터가 여러 종류가 있다는 것은 처음 알았다.
G1GC가 가장 성능이 좋다 제일 자주 쓰인다고 한다.
그리고 숙제도 있다... 일주일 안에 하는 거니 천천히 하자.
🌙 오늘을 마치며 🌙
웹에 대한 사전 지식이 없는 편이라 강의가 어렵게 느껴졌다.아예 어려워 못하겠는 건 아니지만 생각보다는 좀 천천히 들어야 겠다.
'공부 기록 > 내배캠Java_5기' 카테고리의 다른 글
[내배캠][TIL] 28일 차 - 금요일, 스프링 시큐리티를 다시 알아보자. (0) | 2024.05.24 |
---|---|
[내배캠][TIL] 27일 차 - 목요일, 강의듣기싫다 (0) | 2024.05.23 |
[내배캠][TIL] 24일 차 - 월요일 (0) | 2024.05.20 |
[내배캠][TIL] 23일 차 - 금요일, 수준별 수업 전부 듣기 (0) | 2024.05.17 |
[내배캠][TIL] 22일 차 - 목요일, Mockito? Jacoco? (0) | 2024.05.16 |