로그인이 성공하면 JWT를 발급해야 한다.
프론트 단에서 로그인 경로에 대한 하이퍼링크를 실행하면 소셜 로그인창이 등장하고 로그인 로직이 수행된다.
로그인이 성공하면 JWT 가 발급되는데, JWT를 받을 로직이 없다.(리다이렉트를 쓰면 안됨)
-프론트가 모든 책임을 맡음

소셜 로그인 버튼을 누르면, 외부 소셜 로그인 페이지가 뜬다. 코드-요청-API 클라이언트를 통해서 유저 정보 발급
편하지만, 프론트측에서 보낸 유저 정보의 진위 여부를 따져야 한다. 추가적인 보안 로직, 터널링을 해야 한다.
-프론트와 백이 책임을 나눔

로그인 요청->코드 발급->API CLIENT로 백엔드로 코드를 전달, 이걸 통해서 그 뒤의 작업을 한다.
(코드와 액세스토큰을 프론트에서 발급받고, 토큰을 백으로 보내서 그 뒤의 작업)
카카오 개발 포럼을 보면 코드나 액세스토큰을 전송하는 방법을 지양한다
-모든 책임을 백이 구현함
하이퍼링크로 백엔드 API GET요청을 보냄. 로그인 페이지에서 로그인후의 모든 작업을 백에서 함
다만, JWT를 획득하기가 까다롭다.


JWT
헤더: JWT 임을 명시, 사용된 암호 알고리즘
페이로드: 필요한 정보
시그니쳐:누가 발급했는지, 언제 발급했는지 등을 발급자를 확실하게 보여줌
(시그니쳐만 암호화가 진행됨)
(지폐와 같이 외부에서 그 금액을 확인하고 금방 외형을 따라서 만들 수 있지만 발급처에 대한 보장 및 검증은 확실하게 해야하는 경우에 사용한다. 따라서 토큰 내부에 비밀번호와 같은 값 입력 금지)
암호화 종류
양방향
대칭키 - 이 프로젝트는 양방향 대칭키 방식 사용 : HS256
비대칭키
단방향
이번에는 대칭키를 사용한다.
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class UserDto {
private String role;
private String name;
private String userName;
}
@RequiredArgsConstructor
@Component
public class CustomSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final 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, String.valueOf(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);
//cookie.setSecure(true); https 아니니까
cookie.setPath("/");//모든 전역에 설정
cookie.setHttpOnly(true);
return cookie;
}
}
@RequiredArgsConstructor
@Service
public class CustomerOAuth2UserService extends DefaultOAuth2UserService {
private final MemberRepository memberRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oauth2User = super.loadUser(userRequest);
String registrationId = userRequest.getClientRegistration().getRegistrationId();
OAuth2Response oAuth2Response;
if(registrationId.equals("naver")){
oAuth2Response = new NaverResponse(oauth2User.getAttributes());
} else if(registrationId.equals("google")){
oAuth2Response = new GoogleResponse(oauth2User.getAttributes());
} else {
return null;
}
String userName = oAuth2Response.getProvider()+" "+oAuth2Response.getProviderId();
UserDto userDto = new UserDto("ROLE_USER", oAuth2Response.getName(), userName);
return new CustomOAuth2User(userDto);
}
}
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authorization = null;
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
if (cookie.getName().equals("Authorization")) {
authorization = cookie.getValue();
}
}
if (authorization == 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.setRole(role);
userDto.setUserName(username);
//UserDetails에 회원 정보 객체 담기
CustomOAuth2User customOAuth2User = new CustomOAuth2User(userDto);
//스프링 시큐리티 인증 토큰 생성
Authentication authToken = new UsernamePasswordAuthenticationToken(customOAuth2User, null, customOAuth2User.getAuthorities());
//세션에 사용자 등록
SecurityContextHolder.getContext().setAuthentication(authToken);
filterChain.doFilter(request, response);
}
}
@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, String expiredMs){
return Jwts.builder()
.claim("username", username)
.claim("role", role)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expiredMs))
.signWith(secretKey)
.compact();
}
}
import java.util.Collections;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final CustomerOAuth2UserService customerOauth2UserService;
private final CustomSuccessHandler customSuccessHandler;
private final JwtUtil jwtUtil;
public SecurityConfig(CustomerOAuth2UserService customOAuth2UserService, CustomSuccessHandler customSuccessHandler, JwtUtil jwtUtil) {
this.customerOauth2UserService = customOAuth2UserService;
this.customSuccessHandler = customSuccessHandler;
this.jwtUtil = jwtUtil;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(corsCustomizer -> corsCustomizer.configurationSource(new CorsConfigurationSource() {
@Override
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Collections.singletonList("http://43.203.233.134:3000"));
configuration.setAllowedMethods(Collections.singletonList("*"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(Collections.singletonList("*"));
configuration.setMaxAge(3600L);
configuration.setExposedHeaders(Collections.singletonList("Set-Cookie"));
configuration.setExposedHeaders(Collections.singletonList("Authorization"));
return configuration;
}
}));
//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(customerOauth2UserService))
.successHandler(customSuccessHandler)
);
//경로별 인가 작업
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/").permitAll()
.anyRequest().authenticated());
//세션 설정 : STATELESS
http
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
}
@Configuration
public class CorsMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry corsRegistry) {
corsRegistry.addMapping("/**")
.exposedHeaders("Set-Cookie")
.allowedOrigins("http://43.203.233.134:3000");
}
}