대칭키를 사용한대 ..!!
알바 --(20분)-- 학교 --(20분)-- 집 --(20분)-- 체육관
알바하는 곳부터 체육관까지 일직선으로 지하철역 5정거장 차이난다.
신기한데예
혹시 cors안했으면 BE목록 가서 해야함
Header
.Payload
.Signature
Header
Payload
.Signature
spring:
jwt:
secret: 원하는 키
.parseSignedClaims(token)
이 검증을 해준다는 걸 다뤘다.package com.jsh.oauth_jwt_study.jwt;
import io.jsonwebtoken.Jwts;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Date;
@Component
public class JWTUtil {
private SecretKey secretKey;
public JWTUtil(@Value("${spring.jwt.secret}") String secret) {
this.secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
//시크릿키 만들기 *두근두근*
}
public String getUsername(String token) {
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload()
.get("username", String.class);
}
public String getRole(String token) {
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload()
.get("role", String.class);
}
public Boolean isExpired(String token) {
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload()
.getExpiration()
.before(new Date());
}
public String createJwt(String username, String role, Long expiredMs) {
return Jwts.builder()
.claim("username", username)
.claim("role", role)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expiredMs))
.signWith(secretKey)
.compact();
}
}
바로 쿠키를 가지고 만드는게 좋다.
자세한 내용은 아래서 설명하겠다 😅
@Component
public class CustomSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final JWTUtil jwtUtil;
public CustomSuccessHandler(JWTUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//OAuth2User
CustomOAuth2User customUserDetails = (CustomOAuth2User) authentication.getPrincipal();
//토큰을 만드는 코드
String username = customUserDetails.getUsername();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
GrantedAuthority auth = iterator.next();
String role = auth.getAuthority();
String token = jwtUtil.createJwt(username, role, 60*60*60L);
//쿠키를 구워요~! 🍪🍪
response.addCookie(createCookie("Authorization", token));
response.sendRedirect("http://localhost:3000/");
}
private Cookie createCookie(String authorization, String token) {
//클래스 이름이 어떻게 쿠키? 너무 귀여워... 🥲
Cookie cooKie = new Cookie(authorization,token);
cooKie.setMaxAge(60*60*60); //쿠키가 적용될 유효 시간을 설정한다. (여기서는 60시간..ㄷㄷ)
cooKie.setPath("/"); //쿠키가 유효한 경로를 설정한다. "/"는 루트 경로로 설정해 모든 경로에서 쿠키가 유효하도록 설정한다.
cooKie.setHttpOnly(true); // 쿠키를 HTTP(S)에서만 접근 가능하도록 한다.
//cooKie.setSecure(true); //쿠키를 HTTPS으로만 전송되도록 설정한다. (그런데 나는 돈없어서 HTTP임 그래서 주석할거임ㅋ)
return cooKie;
}
}
만약 요청에 토큰이 있는 경우 토큰을 검증하고, 강제로 SecurityContextHolder에 세션을 생성하자.
물론 이 애플리케이션은 STATLESS 상태이기 때문에 소멸된다.
따라서 이렇게 검증을 하고 세션을 생성할 필터를 만들어야한다.
유저가 보낸 쿠키가 제대로 된 토큰이라면 (임시)세션에 권한을 넣어줘서 다양한 리소스에 접근할 수 있도록 하는 것이다.
java
는 null을 다루면 바로 예외를 터트리기 때문에^^사실 요청에 어떤 값을 추가하거나 어떤 메서드를 호출하는 것은 아니다.
흐름 자체는 잘못된 토큰이거나 올바른 토큰이어도 똑같다!
다만, 강제로 세션에 유저를 넣어줘서 이후 권한이 필요한 리소스에 정상적으로 접근할 수 있도록 하는 것이다.
로직이 복잡해서... 좋은 코드는 아니라고 생각한다.
일단 이 코드가 담당하고 있는 역할이 아주 많다 ^🥕^
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String requestUri = request.getRequestURI();
if (requestUri.matches("^\\/login(?:\\/.*)?$")) {
filterChain.doFilter(request, response);
return;
}
if (requestUri.matches("^\\/oauth2(?:\\/.*)?$")) {
filterChain.doFilter(request, response);
return;
}
String authorization = null;
Cookie[] cookies = request.getCookies();
if (cookies == null) {
System.out.println("cookie null");
filterChain.doFilter(request, response);
//조건이 해당되면 메소드 종료 (필수)
return;
}
for (Cookie cookie : cookies) {
System.out.println(cookie.getName());
if (cookie.getName().equals("Authorization")) {
authorization = cookie.getValue();
}
}
//Authorization 헤더 검증
if (authorization == null) {
System.out.println("token null");
filterChain.doFilter(request, response);
//조건이 해당되면 메소드 종료 (필수)
return;
}
//토큰
String token = authorization;
//토큰 소멸 시간 검증
if (jwtUtil.isExpired(token)) {
System.out.println("token expired");
filterChain.doFilter(request, response);
//조건이 해당되면 메소드 종료 (필수)
return;
}
세션에 들어가는 건? Authentication
타입
Authentication
타입에 들어가는 건 ~~~ OAuth2User
String username = jwtUtil.getUsername(token);
String role = jwtUtil.getRole(token);
//userDTO를 생성하여 값 set
UserDTO userDTO = new UserDTO();
userDTO.setUsername(username);
userDTO.setRole(role);
//UserDetails에 회원 정보 객체 담기
CustomOAuth2User customOAuth2User = new CustomOAuth2User(userDTO);
//스프링 시큐리티 인증 토큰 생성
Authentication authToken = new UsernamePasswordAuthenticationToken(customOAuth2User, null, customOAuth2User.getAuthorities());
//세션에 사용자 등록
SecurityContextHolder.getContext().
setAuthentication(authToken);
package com.jsh.oauth_jwt_study.jwt;
import com.jsh.oauth_jwt_study.dto.CustomOAuth2User;
import com.jsh.oauth_jwt_study.dto.UserDTO;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
public class JWTFilter extends OncePerRequestFilter {
private final JWTUtil jwtUtil;
public JWTFilter(JWTUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String requestUri = request.getRequestURI();
if (requestUri.matches("^\\/login(?:\\/.*)?$")) {
filterChain.doFilter(request, response);
return;
}
if (requestUri.matches("^\\/oauth2(?:\\/.*)?$")) {
filterChain.doFilter(request, response);
return;
}
//cookie들을 불러온 뒤 Authorization Key에 담긴 쿠키를 찾음
String authorization = null;
Cookie[] cookies = request.getCookies();
if (cookies == null) {
System.out.println("cookie null");
filterChain.doFilter(request, response);
//조건이 해당되면 메소드 종료 (필수)
return;
}
for (Cookie cookie : cookies) {
System.out.println(cookie.getName());
if (cookie.getName().equals("Authorization")) {
authorization = cookie.getValue();
}
}
//Authorization 헤더 검증
if (authorization == null) {
System.out.println("token null");
filterChain.doFilter(request, response);
//조건이 해당되면 메소드 종료 (필수)
return;
}
//토큰
String token = authorization;
//토큰 소멸 시간 검증
if (jwtUtil.isExpired(token)) {
System.out.println("token expired");
filterChain.doFilter(request, response);
//조건이 해당되면 메소드 종료 (필수)
return;
}
//토큰에서 username과 role 획득
String username = jwtUtil.getUsername(token);
String role = jwtUtil.getRole(token);
//userDTO를 생성하여 값 set
UserDTO userDTO = new UserDTO();
userDTO.setUsername(username);
userDTO.setRole(role);
//UserDetails에 회원 정보 객체 담기
CustomOAuth2User customOAuth2User = new CustomOAuth2User(userDTO);
//스프링 시큐리티 인증 토큰 생성
Authentication authToken = new UsernamePasswordAuthenticationToken(customOAuth2User, null, customOAuth2User.getAuthorities());
//세션에 사용자 등록
SecurityContextHolder.getContext().
setAuthentication(authToken);
filterChain.doFilter(request, response);
}
}
package com.jsh.oauth_jwt_study.config;
import com.jsh.oauth_jwt_study.jwt.JWTFilter;
import com.jsh.oauth_jwt_study.jwt.JWTUtil;
import com.jsh.oauth_jwt_study.oauth2.CustomSuccessHandler;
import com.jsh.oauth_jwt_study.service.CustomOAuth2UserService;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import java.util.Collections;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final CustomOAuth2UserService customOAuth2UserService;
private final CustomSuccessHandler customSuccessHandler;
private final JWTUtil jwtUtil;
public SecurityConfig(CustomOAuth2UserService customOAuth2UserService, CustomSuccessHandler customSuccessHandler, JWTUtil jwtUtil) {
this.customOAuth2UserService = customOAuth2UserService;
this.customSuccessHandler = customSuccessHandler;
this.jwtUtil = jwtUtil;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
//csrf disable
http
.csrf((auth) -> auth.disable());
//From 로그인 방식 disable
http
.formLogin((auth) -> auth.disable());
//HTTP Basic 인증 방식 disable
http
.httpBasic((auth) -> auth.disable());
//JWTFilter 추가
http
.addFilterBefore(new JWTFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
//oauth2
http
.oauth2Login((oauth2) -> oauth2
.userInfoEndpoint((userInfoEndpointConfig) -> userInfoEndpointConfig
.userService(customOAuth2UserService))
.successHandler(customSuccessHandler)
);
//경로별 인가 작업
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/").permitAll()
.anyRequest().authenticated());
//세션 설정 : STATELESS
http
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
}
CustomSuccessHandler
는 무엇일까?public class CustomSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
SimpleUrlAuthenticationSuccessHandler
를 상속받았다.SimpleUrlAuthenticationSuccessHandler
는 : 인증이 성공적으로 이루어졌을 때 실행되는 클래스다. onAuthenticationSuccess
메서드는 무엇일까?onAuthenticationSuccess
메서드가 하는 일1. 리다이렉션 처리
2. 세션관리
3. 쿠키 설정
4. 로그 기록
5. 추가적인 비즈니스 로직 수행
onAuthenticationSuccess
의 파라미터 정보HttpServletRequest request
HttpServletResponse response
Authentication authentication
이 외에도 자주 사용하는 쿠키 클래스 관련 코드를 알아보자.
<쿠키를 조회하는 코드>
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("name".equals(cookie.getName())) {
String value = cookie.getValue();
// 쿠키 값 사용
}
}
}
<쿠키를 삭제하는 방법>
cookie.setMaxAge(0); // 유효 기간을 0으로 설정하여 쿠키 삭제
- 세션리스지만 일시적으로 세션을 넣어서 권한 검사를 통과할 수 있도록 도와준다.
- 자바에서 NULL은 재앙이다.
오우 힘들어요~!