웹서비스는 우선, OAuth2의 client로 kakao를 등록해줘야 하는 작업이 필요함 -> 왜냐면 기존 library에는 google, facebook, twitter등의 글로벌 기업만 표준으로 잡혀 있기에.. 표준을 따른 Oauth2 client를 등록해줘야 한다.
Library설치(build.gradle) 추후 RESTAPI호출 및 JWT생성
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
//katalk access token을 가져오기 위한 Oauth 서버와의 통신을 위한 webclient
implementation 'org.springframework.boot:spring-boot-starter-webflux'
//jwt
implementation 'io.jsonwebtoken:jjwt:0.9.1'
#REST API KEY
spring.security.oauth2.client.registration.kakao.client-id=<REST API KEY>
#authorization code ?? ?? uri
spring.security.oauth2.client.registration.kakao.redirect-uri=<REDIRECT URI>
spring.security.oauth2.client.registration.kakao.client-authentication-method=POST
#spring.security.oauth2.client.registration.kakao.client-secret=secret-key //카카오는 존재하지 않음
spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code
#spring.security.oauth2.client.registration.kakao.scope=profile_nickname, profile_image, account_email, gender, birthday
spring.security.oauth2.client.registration.kakao.scope=profile_nickname
spring.security.oauth2.client.registration.kakao.client_name=kakao
spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize
spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token
spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me
spring.security.oauth2.client.provider.kakao.user-name-attribute=id
@Configuration
@EnableConfigurationProperties(OAuth2ClientProperties.class)
@Conditional(ClientsConfiguredCondition.class)
@RequiredArgsConstructor
public class OAuth2ClientRegistrationRepositoryConfiguration {
private final OAuth2ClientProperties properties;
@Bean
@ConditionalOnMissingBean(ClientsConfiguredCondition.class)
public InMemoryClientRegistrationRepository clientRegistrationRepository(){
List<ClientRegistration> registrations = new ArrayList<>(
OAuth2ClientPropertiesRegistrationAdapter.getClientRegistrations(this.properties).values());
return new InMemoryClientRegistrationRepository(registrations);
}
}
@Component
@Slf4j
public class JwtTokenProvider {
@Value("${jwt.access-token.expire-length}")
private long accessTokenValidityInMilliseconds;
@Value("${jwt.refresh-token.expire-length}")
private long refreshTokenValidityInMilliseconds;
@Value("${jwt.token.secret-key}")
private String secretKey;
public String createAccessToken(String payload){
return createToken(payload, accessTokenValidityInMilliseconds);
}
public String createRefreshToken(){
byte[] array = new byte[7];
new Random().nextBytes(array);
String generatedString = new String(array, StandardCharsets.UTF_8);
return createToken(generatedString, refreshTokenValidityInMilliseconds);
}
public String createToken(String payload, long expireLength){
Claims claims = Jwts.claims().setSubject(payload);
Date now = new Date();
Date validity = new Date(now.getTime() + expireLength);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(validity)
.signWith(SignatureAlgorithm.HS512,secretKey)
.compact();
}
public String getPayload(String token){
try{
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody()
.getSubject();
}catch (ExpiredJwtException e){
return e.getClaims().getSubject();
}catch (JwtException e){
throw new RuntimeException("유효하지 않은 토큰입니다.");
}
}
public boolean validateToken(String token){
try{
Jws<Claims> claimsJws = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token);
return !claimsJws.getBody().getExpiration().before(new Date());
}catch (JwtException | IllegalArgumentException exception){
return false;
}
}
}
public abstract class Oauth2UserInfo {
protected Map<String, Object> attributes;
public Oauth2UserInfo(Map<String, Object> attributes){
this.attributes = attributes;
}
public Map<String, Object> getAttributes(){
return attributes;
}
public abstract Long getId();
public abstract String getName();
public abstract String getNickName();
}
public class KakaoUserInfo extends Oauth2UserInfo {
public KakaoUserInfo(Map<String, Object> attributes) {
super(attributes);
}
@Override
public Long getId() {
return Long.parseLong(String.valueOf(attributes.get("id")));
}
@Override
public String getName() {
return (String) getKakaoAccount().get("name");
}
@Override
public String getNickName() {
return (String) getProfile().get("nickname");
}
public Map<String, Object> getKakaoAccount(){
return(Map<String, Object>) attributes.get("kakao_account");
}
public Map<String, Object> getProfile(){
return (Map<String, Object>) getKakaoAccount().get("profile");
}
public String getProvider(){
return "kakao";
}
}
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "RTUSER")
@Getter
public class User extends BaseEntity{
@Id
@Column(name = "user_id")
private Long id; //katalk PK 값으로 매핑하기
private String name;
private String nickName;
private String kakaoAccessToken;
private String kakaoRefreshToken;
private String cloudEmail;
private String calenderEmail;
@Enumerated(EnumType.STRING)
private Role role;
@Column(name = "kakao_update")
private LocalDateTime kakaoUpdate;
@OneToMany(mappedBy = "user")
private List<Calender> calenderList = new ArrayList<>();
public User(Long id, String name, String nickName, String kakaoAccessToken, String kakaoRefreshToken, Role role){
this.id = id;
this.name = name;
this.nickName = nickName;
this.kakaoAccessToken = kakaoAccessToken;
this.kakaoRefreshToken = kakaoRefreshToken;
this.role = role;
}
public void setKakaoUpdate(){
this.kakaoUpdate = LocalDateTime.now();
}
public void updateNickName(String nickName){
this.nickName = nickName;
}
public void setKakaoToken(String kakaoAccessToken, String kakaoRefreshToken){
this.kakaoAccessToken = kakaoAccessToken;
this.kakaoRefreshToken = kakaoRefreshToken;
}
}
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class LoginResponse {
private Long id;
private String name;
private String nickName;
private Role role;
private String accessToken;
private String refreshToken;
public LoginResponse(User user, String accessToken, String refreshToken){
this.id = user.getId();
this.name = user.getName();
this.nickName = user.getNickName();
this.role = user.getRole();
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
}
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional(readOnly = true)
public class OauthService {
private static final String BEARER_TYPE = "Bearer";
private final InMemoryClientRegistrationRepository inMemoryRepository;
private final UserRepository userRepository;
private final JwtTokenProvider jwtTokenProvider;
/**
* InMemoryRepo : oauth properties를 담고 있음.
*
* @getToken() 넘겨받은 code로 access토큰 요청
* @getUserProfile 첫 로그인 시 회원가입 진행
*/
@Transactional
public LoginResponse login(String providerName, String code) {
ClientRegistration provider = inMemoryRepository.findByRegistrationId(providerName);
OAuth2AccessTokenResponse tokenResponse = getToken(code, provider);
tokenResponse.getAccessToken(); // accesstoken
tokenResponse.getRefreshToken(); // refreshtoken
User user = getUserProfile(providerName, tokenResponse, provider);
String accessToken = jwtTokenProvider.createAccessToken(String.valueOf(user.getId()));
String refreshToken = jwtTokenProvider.createRefreshToken();
return new LoginResponse(user, accessToken, refreshToken);
}
private OAuth2AccessTokenResponse getToken(String code, ClientRegistration provider) {
return WebClient.create()
.post()
.uri(provider.getProviderDetails().getTokenUri())
.headers(httpHeaders -> {
httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
httpHeaders.setAcceptCharset(Collections.singletonList(StandardCharsets.UTF_8));
})
.bodyValue(tokenRequest(code, provider))
.retrieve()
.bodyToMono(OAuth2AccessTokenResponse.class)
.block();
}
private MultiValueMap<String, String> tokenRequest(String code, ClientRegistration provider) {
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("code", code);
formData.add("grant_type", "authorization_code");
formData.add("redirect_uri", provider.getRedirectUri());
formData.add("client_id", provider.getClientId());
return formData;
}
private User getUserProfile(String providerName, OAuth2AccessTokenResponse tokenResponse, ClientRegistration provider) {
Map<String, Object> userAttributes = getUserAttributes(provider, tokenResponse);
//뭘가져올 수 있는지 알아야함.
// id -> get('id')
// nickname -> get('kakao_account) -> get("profile") -> get("nickname")
// name -> get("kakao_account) -> get("name")
if (providerName.equals("kakao")) {
throw new IllegalArgumentException("잘못된 접근입니다.");
}
KakaoUserInfo kakaoUserInfo = new KakaoUserInfo(userAttributes);
Long kakao_id = kakaoUserInfo.getId();
String provide = kakaoUserInfo.getProvider();
String name = kakaoUserInfo.getName();
String nickName = kakaoUserInfo.getNickName();
if (userRepository.findById(kakao_id).isPresent()) {
throw new DuplicateSignInException();
}
User user = new User(kakao_id, name, nickName, tokenResponse.getAccessToken().getTokenValue(), tokenResponse.getRefreshToken().getTokenValue(), Role.ROLE_USER);
User save = userRepository.save(user);
return save;
}
private Map<String, Object> getUserAttributes(ClientRegistration provider, OAuth2AccessTokenResponse tokenResponse) {
return WebClient.create()
.get()
.uri(provider.getProviderDetails().getUserInfoEndpoint().getUri())
.headers(httpHeaders -> httpHeaders.setBearerAuth(tokenResponse.getAccessToken().getTokenValue()))
.retrieve()
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {
})
.block();
}
}
@RequiredArgsConstructor
@Slf4j
@RestController
public class TestController {
private final OauthService oauthService;
@GetMapping("/login/oauth/{provider}")
public ResponseEntity<LoginResponse> login(@PathVariable String provider, @RequestParam String code) {
LoginResponse loginResponse = oauthService.login(provider, code);
return ResponseEntity.ok().body(loginResponse);
}
}
libary필요없음.
서비스 단만 바꾸면 된다(Oauth2와 의존성 있는 모든 것을 지우고)
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional(readOnly = true)
public class OauthService {
private static final String BEARER_TYPE = "Bearer";
private final UserRepository userRepository;
private final JwtTokenProvider jwtTokenProvider;
@Transactional
public LoginResponse loginWithToken(String providerName, UserToken userToken) {
User user = getUserProfileByToken(providerName, userToken);
String accessToken = jwtTokenProvider.createAccessToken(String.valueOf(user.getId()));
String refreshToken = jwtTokenProvider.createRefreshToken();
return new LoginResponse(user, accessToken, refreshToken);
}
private Map<String, Object> getUserAttributesByToken(UserToken userToken){
return WebClient.create()
.get()
.uri("https://kapi.kakao.com/v2/user/me")
.headers(httpHeaders -> httpHeaders.setBearerAuth(userToken.getAccessToken()))
.retrieve()
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {})
.block();
}
private User getUserProfileByToken(String providerName, UserToken userToken){
if(!providerName.equals("kakao")){
throw new IllegalArgumentException("잘못된 접근입니다.");
}
Map<String, Object> userAttributesByToken = getUserAttributesByToken(userToken);
KakaoUserInfo kakaoUserInfo = new KakaoUserInfo(userAttributesByToken);
Long kakao_id = kakaoUserInfo.getId();
String name = kakaoUserInfo.getName();
String nickName = kakaoUserInfo.getNickName();
if(userRepository.findById(kakao_id).isPresent()){
throw new DuplicateSignInException();
}
User user = new User(kakao_id, name, nickName, userToken.getAccessToken(), userToken.getRefreshToken(), Role.ROLE_USER);
User save = userRepository.save(user);
return save;
}
}