2025. 3. 17. 14:28ㆍSpringboot/security
전체코드 : https://github.com/gks930620/spring_securty_all
구현 자체는 JWT access token,refresh token에
Oauth2 몇몇 기능을 추가한 거밖에 없다.
그래서 여기는 전체코드를 전부 설명하는것이 아니라 추가된 내용만 작성하는걸로 하고
전체코드는 github를 참고하도록 하자.
Custom-oauth2/login/kakao 컨트롤러
이 떄 이 url은 permitALL 되어야한다.
Oauth2LoginController
@Controller
@RequestMapping("/custom-oauth2/login")
@RequiredArgsConstructor
public class Oauth2LoginController {
private final InMemoryAuthorizationRequestRepository authorizationRequestRepository;
@Value("${spring.security.oauth2.client.registration.kakao.client-id}")
private String kakaoClientId;
@Value("${spring.security.oauth2.client.registration.kakao.redirect-uri}")
private String kakaoRedirectUri;
@RequestMapping("/kakao")
public ResponseEntity<?> kakaoOauth2Login(HttpServletRequest request, HttpServletResponse response){
String state=""+UUID.randomUUID();
// ✅ OAuth2AuthorizationRequest 직접 생성 == 로그인사용자가 로그인페이지 요청한사용자인지 확인하는 용. 클라이언트 구별!!
OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
.authorizationUri("https://kauth.kakao.com/oauth/authorize")
.clientId(kakaoClientId)
.redirectUri(kakaoRedirectUri)
.state(state)
.attributes(attrs -> attrs.put("registration_id", "kakao"))
.build();
authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);
//기본은 session에 저장해서 구별하는건데 여기서는 session없으니까 그냥 map에다.. 서버여러대면 DB 등
// ✅ Authorization URL 생성
String authorizationUrl = "https://kauth.kakao.com/oauth/authorize"
+ "?client_id=" + kakaoClientId
+ "&redirect_uri=" + kakaoRedirectUri
+ "&response_type=code"
+ "&state=" + state;
return ResponseEntity.ok(Map.of("authorizationUrl", authorizationUrl)); //client에는 카카오 restAPI문서에서 요구하는 형태로!
}
}
InMemoryAuthorizationRequestRepository
@Component
public class InMemoryAuthorizationRequestRepository implements
AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
private final Map<String, OAuth2AuthorizationRequest> authorizationRequests = new ConcurrentHashMap<>();
@Override
public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
String state = request.getParameter("state");
if (state == null) {
return null;
}
return authorizationRequests.get(state);
}
@Override
public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) {
if (authorizationRequest == null) {
return;
}
String state = authorizationRequest.getState();
authorizationRequests.put(state, authorizationRequest);
System.out.println("✅ OAuth2AuthorizationRequest 저장: " + state);
// 5분 후 자동 삭제 (보안 상 Authorization Request를 계속 들고 있을 필요 없음)
new Thread(() -> {
try {
TimeUnit.MINUTES.sleep(5);
authorizationRequests.remove(state);
System.out.println("🗑️ OAuth2AuthorizationRequest 만료 삭제: " + state);
} catch (InterruptedException ignored) {}
}).start();
}
@Override
public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request,HttpServletResponse response) {
String state = request.getParameter("state");
if (state == null) {
return null;
}
System.out.println("🚀 OAuth2AuthorizationRequest 조회 후 삭제: " + state);
return authorizationRequests.remove(state);
}
}
로그인과정
우리가 설정할건 카카오로 인가코드 보내기 전에 Oauth2AuthorizationRequest를 검사하는 것이다.
이는 SeucirtyConfig의 authorizationRequestRepository를 위에서 만든
InMemoryAuthorzationRequestRepository로 지정해주면 된다.
http.oauth2Login(oauth2 -> oauth2
.authorizationEndpoint(authEndpoint -> authEndpoint
.authorizationRequestRepository(authorizationRequestRepository)) // ✅ 직접 구현한 저장소 적용
.userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService))
.successHandler(oAuth2LoginSuccessHandler) // ✅ 로그인 성공 시 JWT 발급
.failureHandler((request, response, exception) -> {
exception.printStackTrace();
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}) // ✅ 실패 시 로그 찍기
);
CustomOauth2UserDetailsService와
CustomUserAccount 는 기존 웹 세션방식과 똑같다. 추가해주도록 하자.
https://brilliantdevelop.tistory.com/221 참고.
이후 로그인성공을 했다면 클라이언트에게 JWT 방식에 맞게 access token, refresh token을 보내주면 된다.
successHandler는 위의 config에 successHandler 등록하면 된다.
Oauth2LoginSuccessHandler
@Component
@RequiredArgsConstructor
public class OAuth2LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final JwtUtil jwtUtil;
private final RefreshRepository refreshRepository;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
CustomUserAccount customUserAccount = (CustomUserAccount) authentication.getPrincipal();
String accessToken = jwtUtil.createAccessToken(customUserAccount.getUsername());
String refreshToken=jwtUtil.createRefreshToken(customUserAccount.getUsername());
// ✅ 새 Refresh Token 저장 (기존 Token 삭제 X)
RefreshEntity refreshEntity = new RefreshEntity();
refreshEntity.setUserEntity(customUserAccount.getUserEntity()); // 이전 필터에서 사용자정보저장할 떄 userEntity는 entity상태로 저장됨.
refreshEntity.setToken(refreshToken);
refreshRepository.save(refreshEntity);
// 토큰을 응답에 포함
response.setContentType("application/json");
// response.getWriter().write("{\"access_token\": \"" + accessToken + "\"}");
response.getWriter().write(new ObjectMapper().writeValueAsString(
Map.of("access_token", accessToken, "refresh_token", refreshToken)
));
}
}
이후 로그인과정은 클라이언트가 acccess token, refresh token을 활용하면 된다.
이는 https://brilliantdevelop.tistory.com/226와 똑같다.
실행결과
로그인페이지로 요청 후 authorizationURL을 받는다.
authorizationURL로 요청하면 다음과 같은 화면이 나온다.
(지금 우리 서버는 백엔드서버지만 딱히 로그인페이지에서 id,pw를 입력할 클라이언트를 안 만들어서
브라우저로 진행한다.)
브라우저는 로그인 후 자동으로 redirect-uri로 요청한다.
로그를 보면 /login/oauth2/code/kakao?code= 가 있다. 인가코드가 포함된 요청이다.
(브라우저가 아닐 때는 redirect-uri로 클라이언트가 직접 백엔드서버에 요청하면 된다.)
어쨋든 redirect-uri로 요청했기 때문에 우리 백엔드서버는 access token과 refresh token을 준다.
이후 이 access token으로 /my/info에 요청하면 내 정보를 보게된다.
나머지 토큰만료, refresh 토큰등은 일반회원과 똑같이 처리된다.
(카카오로 로그인을 했다면 일반회원처럼 DB에 저장되어있기떄문에)