소셜 로그인 구현은 다음을 참조.
1. [Spring Boot] OAuth2 소셜 로그인 가이드 (구글, 페이스북, 네이버, 카카오)
2. [Spring] 스프링으로 OAuth2 로그인 구현하기1 - 구글
[Spring] 스프링으로 OAuth2 로그인 구현하기2 - 네이버
3. 소스:
https://github.com/braverokmc79/spring-boot-react-oauth2-social-login-demo
https://github.com/loosie/spring_practice
아래 내용은 스프링부트의 OAuth2 를 이용하지 않고 직접 구현한 카카오 로그인 방법이다.
소스 :
https://github.com/braverokmc79/Springboot-JPA-Blog
카카오 키는 REST API 사용.
https://developers.kakao.com/docs/latest/ko/kakaologin/common
1. loginForm
<a href="https://kauth.kakao.com/oauth/authorize?client_id=3e5c0148058f1d1db8f55cea63a6e820&redirect_uri=http://localhost:8000/auth/kakao/callback&response_type=code" > <img src="${Home}/images/kakao_login_button.png" height="38"> </a>
2. KakaoRequestCode
public class KakaoRequestCode { public final static String GRANT_TYPE ="authorization_code"; public final static String CLIENT_ID ="3e5c01480258f13d1db8f55cea63a6e820"; public final static String REDIRECT_URI ="http://localhost:8000/auth/kakao/callback"; }
3. OAuthToken
import groovy.transform.ToString; import lombok.Data; @Data @ToString public class OAuthToken { private String access_token; private String token_type; private String refresh_token; private String expires_in; private String scope; private String refresh_token_expires_in; }
4.KakaoProfile
json 객체를 담기위한 클래스 자동 생성 사이트
https://www.jsonschema2pojo.org/
import groovy.transform.ToString; import lombok.Data; // // //"id": 2021983330297, //"connected_at": "2021-11-30T09:33:34Z", //"properties": { // "nickname": "홍길동" //}, //"kakao_account": { // "profile_nickname_needs_agreement": false, // "profile": { // "nickname": "홍길동" // }, // "has_email": true, // "email_needs_agreement": false, // "is_email_valid": true, // "is_email_verified": true, // "email": "honggildong@gmail.com" //} @Data @ToString public class KakaoProfile { public Integer id; public String connected_at; public Properties properties; public KakaoAccount kakao_account; @Data public class Properties { public String nickname; public String profile_image; public String thumbnail_image; } @Data public class KakaoAccount { public Boolean profile_nickname_needs_agreement; public Profile profile; public Boolean has_email; public Boolean email_needs_agreement; public Boolean is_email_valid; public Boolean is_email_verified; public String email; @Data public class Profile { public String nickname; public String profile_image; public String thumbnail_image; } } }
5.UserController
import java.security.Principal; import java.util.ArrayList; import java.util.Collection; import java.util.UUID; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.client.RestTemplate; import com.cos.blog.config.auth.PrincipalDetailService; import com.cos.blog.constant.KakaoRequestCode; import com.cos.blog.model.KakaoProfile; import com.cos.blog.model.OAuthToken; import com.cos.blog.model.User; import com.cos.blog.service.UserService; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; //인증이 안된 사용자들이 출입할 수 있는 경로를 /auth/** 허용 //그냥주소가 /이면 index.jsp 허용 //static 이하여 있는 /js/** , /css/** , /image/** @Controller @RequiredArgsConstructor public class UserController { private final UserService userService; private final PrincipalDetailService principalDetailService; @Value("${cos.key}") private String cosKey; @GetMapping("/auth/joinForm") public String joinForm(){ return "user/joinForm"; } @GetMapping("/auth/loginForm") public String loginForm() { return "user/loginForm"; } @GetMapping(value = "/auth/loginForm/error") public String loginError(Model model){ model.addAttribute("loginErrorMsg", "아이디 또는 비밀번호를 확인해 주세요."); return "user/loginForm"; } @GetMapping("/user/updateForm") public String updateUserForm(Principal principal, Model model) { User userInfo =userService.getByUsername(principal.getName()); model.addAttribute("userInfo" ,userInfo); return "user/updateUserForm"; } @GetMapping("/auth/kakao/callback") //@ResponseBody public String kakaoCallback(String code) { /** * https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#refresh-token POST /oauth/token HTTP/1.1 Host: kauth.kakao.com Content-type: application/x-www-form-urlencoded;charset=utf-8 grant_type : authorization_code client_id : 3e5c01480582f1d1db8f55cea63a6e820 redirect_uri : http://localhost:8000/auth/kakao/callback code : */ /** 2. 반환된 code 을 사용하여 헤더와 바디가 포함된 post 방식으로 다시 kako 에 요청 */ //POST 방식으로 key=value 데이터를 요청 (카카오쪽으로) //Retrofit2 //OkHttp //RestTemplate RestTemplate rt =new RestTemplate(); //HttpHeader 오브젝트 생성 HttpHeaders headers=new HttpHeaders(); headers.add("Content-type","application/x-www-form-urlencoded;charset=utf-8"); //HttpBody 오브젝트 생성 MultiValueMap<String, String> params=new LinkedMultiValueMap<>(); params.add("grant_type", KakaoRequestCode.GRANT_TYPE); params.add("client_id", KakaoRequestCode.CLIENT_ID); params.add("redirect_uri",KakaoRequestCode.REDIRECT_URI); params.add("code",code); //HttpHeader 와 HttpBody를 하나의 오브젝트에 담기 HttpEntity<MultiValueMap<String, String>> kakaoTokenRequest=new HttpEntity<>(params, headers); //Http 요청하기 - Post 방식으로 - 그리고 response 변수의 응답 받음. ResponseEntity<String> response=rt.exchange( "https://kauth.kakao.com/oauth/token", HttpMethod.POST, kakaoTokenRequest, String.class ); /* * * 다음과 같이 response.getBody() 로 다음과 같은 JSON 데이터를 받을수 있다. { "access_token": "yq3nNezpw27tM1-SfdsfTshFJSMZj_f5OTfdsfDfqniLrM2gopcNEAAAF-wfAvgw", "token_type": "bearer", "refresh_token": "JmZpH9TnnsBEo10wsdfZV123dssWhT4CN3cjl-MVh0F-xMQopfcNEAAAF-wfAvgQ", "expires_in": 21599, "scope": "account_email profile_image gender profile_nickname", "refresh_token_expires_in": 5183999 } * * * */ /** 3. 위 JSON 데이터를 Gson, Json Simple , ObjectMapper 등로로 파싱후 access_token(카카오 엑세트 토큰)만 추출하여 얻는다. */ //OAuthToken 객체는 위 Gson 데이터와 동일한 변수명으로 만든다. // 다음은 ObjectMapper 를 이용하여 json 파싱후 oauthToken 추출 ObjectMapper objectMapper=new ObjectMapper(); OAuthToken oauthToken =null; try { oauthToken=objectMapper.readValue(response.getBody(), OAuthToken.class); } catch (JsonMappingException e) { e.printStackTrace(); } catch (JsonProcessingException e) { e.printStackTrace(); } /** 4. 카카오엑세트 토큰을 이용하여 다시 카카오에 요청 후 사용자 정보 조회를 한다. (ex 이메일, 닉네임 ..) */ /* GET/POST /v2/user/me HTTP/1.1 Host: kapi.kakao.com Authorization: Bearer {ACCESS_TOKEN} Content-type: application/x-www-form-urlencoded;charset=utf-8 요청주소 : https://kapi.kakao.com/v2/user/me */ RestTemplate rt2 =new RestTemplate(); //HttpHeader 오브젝트 생성 HttpHeaders headers2=new HttpHeaders(); headers2.add("Authorization", "Bearer "+oauthToken.getAccess_token()); headers2.add("Content-type","application/x-www-form-urlencoded;charset=utf-8"); //HttpHeader 와 HttpBody를 하나의 오브젝트에 담기 HttpEntity<MultiValueMap<String, String>> kakaoProfileRequest=new HttpEntity<>(headers2); //Http 요청하기 - Post 방식으로 - 그리고 response 변수의 응답 받음. ResponseEntity<String> response2=rt2.exchange( "https://kapi.kakao.com/v2/user/me", HttpMethod.POST, kakaoProfileRequest, String.class ); /** { "id": 2298323097, "connected_at": "2021-11-30T09:33:34Z", "properties": { "nickname": "홍길동" }, "kakao_account": { "profile_nickname_needs_agreement": false, "profile": { "nickname": "홍길동" }, "has_email": true, "email_needs_agreement": false, "is_email_valid": true, "is_email_verified": true, "email": "honggildong@gmail.com" } } */ /** 5. 위 JSON 데이터를 Gson, Json Simple , ObjectMapper 등로로 파싱후 KakaoProfile 객체에 저장. */ /** * 다음 사이틀 이용해서 KakaoProfile 객체를 생성 * https://www.jsonschema2pojo.org/ * * */ ObjectMapper objectMapper2=new ObjectMapper(); KakaoProfile kakaoProfile =null; try { kakaoProfile=objectMapper2.readValue(response2.getBody(), KakaoProfile.class); } catch (JsonMappingException e) { e.printStackTrace(); } catch (JsonProcessingException e) { e.printStackTrace(); } System.out.println(" cosKey : " + cosKey); System.out.println(" kakaoProfile : " + kakaoProfile.toString()); System.out.println("카카오 아이디(번호):" + kakaoProfile.getId()); System.out.println("카카오 이메일 : "+ kakaoProfile.getKakao_account().getEmail()); System.out.println("블로그서버 유저네임 : " +kakaoProfile.getKakao_account().getEmail() + "_"+kakaoProfile.getId()); UUID garbasePassword=UUID.randomUUID(); System.out.println("블로그서버 이메일 :" + kakaoProfile.getKakao_account().getEmail()); System.out.println("블로그서버 패스워드 : "+garbasePassword); System.out.println(); User kakaoUser=User.builder() .username(kakaoProfile.getKakao_account().getEmail() + "_"+kakaoProfile.getId()) .password(cosKey) .email(kakaoProfile.getKakao_account().getEmail()) .oauth("kakao") .build(); //가입자 혹은 비가입자 체크처리 User user=userService.findByUserName(kakaoProfile.getKakao_account().getEmail() + "_"+kakaoProfile.getId()); if(user==null) { userService.userJoin(kakaoUser); user=kakaoUser; } //UsernamePasswordAuthenticationToken 아이디외 비밀번호가 필요하기때문에 //카카오 oauth 로그인 비밀번호는 cosKey 값으로 모두 동일하다. 보안 주의 // Authentication authentication= new UsernamePasswordAuthenticationToken(user.getUsername(), cosKey); //SecurityContextHolder.getContext().setAuthentication(authentication); sessionReset(user); return "redirect:/"; } /** * 시큐리티 세션 재설정 * 시큐리티 세션 재설정은 서비스에서 트랜잭션이 종료된 후 실행되는 컨트롤에서 설정해야 한다. */ public void sessionReset(User user) { //유저 한명에 권한이 여러개 설정될수 있기 때문에 list 한다. ex)GUEST,USER ,MANAGER,ADMIN Collection<GrantedAuthority> authorities = new ArrayList<>(); authorities.add(() -> user.getRole().toString()); UserDetails userDetails=principalDetailService.loadUserByUsername(user.getUsername()); Authentication newAuthentication = new UsernamePasswordAuthenticationToken(userDetails, null, authorities); SecurityContextHolder.getContext().setAuthentication(newAuthentication); } }
댓글 ( 5)
댓글 남기기