반응형
아무리 여러 번을 해봐도 로그인은 늘 어렵다.....ㅎㅎㅎ
그리고 아직 친해지지 못한 맥북 + 리액트 네이티브 환경에서 하려니 엄청난 에러를 겪고 겨우 성공한 카카오 로그인...
시작!!
⭐️Frontend
0. 카카오 개발자센터 애플리케이션
설명 생략
1. 리액트네이티브 카카오 로그인 라이브러리 설치
npm install @react-native-seoul/kakao-login
//RN 0.60.X 이상부터는 Auto linking 지원
pod install
https://github.com/crossplatformkorea/react-native-kakao-login
2. info.plist 작성
<key>CFBundleURLTypes</key>
<array>
+ <dict>
+ <key>CFBundleTypeRole</key>
+ <string>Editor</string>
+ <key>CFBundleURLSchemes</key>
+ <array>
+ <string>kakao{카카오 네이티브앱 아이디를 적어주세요}</string>
+ </array>
+ </dict>
</array>
<key>CFBundleVersion</key>
<string>1</string>
+ <key>KAKAO_APP_KEY</key>
+ <string>{카카오 네이티브앱 아이디를 적어주세요}</string>
+ <key>KAKAO_APP_SCHEME</key> // 선택 사항 멀티 플랫폼 앱 구현 시에만 추가하면 됩니다
+ <string>{카카오 앱 스킴을 적어주세요}</string> // 선택 사항
+ <key>LSApplicationQueriesSchemes</key>
+ <array>
+ <string>kakaokompassauth</string>
+ <string>storykompassauth</string>
+ <string>kakaolink</string>
+ </array>
네이티브 앱키는 카카오 개발자 센터에서 발급받은 저 네이티브 앱 키를 적으면 된다!
중요한 건 카카오, 네이버 이렇게 여러 가지의 소셜 로그인을 구현할 때는 아래처럼 <array> 태그 안에 같이 적어줘야 한다는 것!
그리고 네이티브 앱키를 적을 때 앞에 kakao를 꼭 붙여줘야 한다는 것! 예를 들어 1234라면 kakao1234 이렇게!
3. Swift Bridging Header 추가
4. XCode TARGETS > Info > URL Types
마찬가지로 네이티브 앱키 넣어주기.
5. AppDelegate.m 수정
위치는 여기!
아래 코드 추가하기
#import <RNKakaoLogins.h>
- (BOOL)application:(UIApplication *)app
openURL:(NSURL *)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
if([RNKakaoLogins isKakaoTalkLoginUrl:url]) {
return [RNKakaoLogins handleOpenUrl: url];
}
return NO;
}
6. 실행 코드
버튼 만들고
<Button opt={"kakao"} text="카카오톡 아이디 로그인" handlePress={signInWithKakao}/>
라이브러리 import하고 아래와 같이 작성하면 됨!
import { login, logout, unlink, getProfile, getAccessToken} from '@react-native-seoul/kakao-login';
const signInWithKakao = async () => {
const token = await login();
const kakaoProfileResult = await getProfile();
}
💡 실행 화면
⭐️Backend
1. build.gradle 의존성
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly("com.mysql:mysql-connector-j")
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
implementation 'javax.xml.bind:jaxb-api:2.3.0'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.2'
implementation 'com.google.code.gson:gson:2.8.7'
implementation 'com.googlecode.json-simple:json-simple:1.1.1'
implementation 'com.auth0:java-jwt:4.4.0'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
2. application.yml , application-ouath.xml 작성
server:
port: 8085
servlet:
context-path: /
encoding:
charset: UTF-8
enabled: true
force: true
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/스키마명?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul&characterEncoding=UTF-8
username: DB ID
password: DB PW
jpa:
database: mysql
open-in-view: true
hibernate:
ddl-auto: update
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
show-sql: true
properties:
hibernate:
format_sql: true
show_sql: true
h2:
console:
enabled: true
profiles:
include: oauth
logging:
level:
org:
hibernate:
type:
descriptor:
sql: trace
spring:
security:
oauth2:
client:
registration:
naver:
client-id: 네이버 client id
client-secret: 네이버 client secret
client-name: Naver
authorization-grant-type: authorization_code
redirect-uri: http://localhost:8085/ouath/callback/naver
kakao:
client-id: 카카오 client id
client-secret: 카카오 client secret
redirect-uri: http://localhost:8085/ouath/callback/kakao
authorization-grant-type: authorization_code
client-authentication-method: POST
client-name: Kakao
scope:
- profile_nickname
- profile_image
provider:
naver:
authorization-uri: https://nid.naver.com/oauth2.0/authorize
token-uri: https://nid.naver.com/oauth2.0/token
user-info-uri: https://openapi.naver.com/v1/nid/me
user-name-attribute: response
kakao:
authorization-uri: https://kauth.kakao.com/oauth/authorize
token-uri: https://kauth.kakao.com/oauth/token
user-info-uri: https://kapi.kakao.com/v2/user/me
user-name-attribute: id
jwt:
secretKey: 시크릿키
token-validity-in-seconds: 86400
tokenExpiry: 1800000
3. Entity, dto 작성
4. UserRepository
5. JWT 관련 코드 Config 파일 (JwtProvider.java , JwtRequestFilter.java)
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtProvider {
private static final String AUTHORITIES_KEY = "auth";
private static final String BEARER_TYPE = "bearer";
private static final long ACCESS_TOKEN_EXPIRE_TIME = 1000 * 60 * 30;
private UserService userService;
@Value("${jwt.secretKey}")
private String secretKey;
/*public JwtProvider(@Value("${jwt.secret}") String secretKey) {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
this.key = Keys.hmacShaKeyFor(keyBytes);
} */
@PostConstruct
protected void init() {
secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
}
//JW 토큰 생성
public String createToken(String userPk, String roles) {
Claims claims = Jwts.claims().setSubject(userPk);
claims.put("roles", roles);
Date now = new Date();
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + ACCESS_TOKEN_EXPIRE_TIME))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
public Authentication getAuthentication(String accessToken) {
UserEntity userDetails = userService.loadUserByUsername(this.getUserPk(accessToken));
return new UsernamePasswordAuthenticationToken(userDetails.getEmail(),"",userDetails.getAuthorities());
}
public String getUserPk(String token) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
}
// Request의 Header에서 token 값을 가져옵니다. "X-AUTH-TOKEN" : "TOKEN값'
public String resolveToken(HttpServletRequest request) {
return request.getHeader("token");
}
public boolean validateToken(String token) {
try {
//Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
return !claims.getBody().getExpiration().before(new Date());
} catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
log.info("잘못된 JWT 서명입니다.");
} catch (ExpiredJwtException e) {
log.info("만료된 JWT 토큰입니다.");
} catch (UnsupportedJwtException e) {
log.info("지원되지 않는 JWT 토큰입니다.");
} catch (IllegalArgumentException e) {
log.info("JWT 토큰이 잘못되었습니다.");
}
return false;
}
private Claims parseClaims(String accessToken) {
try {
return Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(accessToken).getBody();
} catch (ExpiredJwtException e) {
return e.getClaims();
}
}
}
@RequiredArgsConstructor
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
public static final String AUTHORIZATION_HEADER = "Authorization";
public static final String BEARER_PREFIX = "Bearer ";
private final JwtProvider jwtProvider;
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
return bearerToken.substring(7);
}
return null;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String jwt = resolveToken(request);
if (StringUtils.hasText(jwt) && jwtProvider.validateToken(jwt)) {
Authentication authentication = jwtProvider.getAuthentication(jwt);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
}
6. 스프링 시큐리티 관련 Config 파일 (SecurityConfig.java)
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
public static final String FRONT_URL = "http://localhost:3000";
private final JwtProvider jwtProvider;
private final UserOAuthService userOAuthService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.cors()
.and()
.httpBasic().disable()
.formLogin().disable()
.authorizeHttpRequests(request -> request
.requestMatchers(FRONT_URL+"/**").authenticated().anyRequest().permitAll())
.sessionManagement() //session을 사용하지 않음
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling()
.authenticationEntryPoint(new CustomAuthenticationEntryPoint())
.and()
.addFilterBefore(new JwtRequestFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
7. 카카오 로그인 프론트엔드 리액트네이티브 코드
⭐️카카오 로그인 전체 흐름
(Front) 로그인 -> 인가코드 발급 -> 토큰 발급(access, refresh) -> access Token을 백엔드로 넘김
-> (Backend) 사용자 인증 후 DB 저장 -> JWT 토큰 발급 -> JWT localStorage에 저장
import React, {useEffect, useState} from "react";
import { StyleSheet, View, SafeAreaView, Image, TouchableOpacity, Alert} from "react-native";
import AsyncStorage from '@react-native-async-storage/async-storage';
import axios from "axios";
import {
widthPercentageToDP as wp,
heightPercentageToDP as hp,
widthPercentageToDP,
heightPercentageToDP,
} from 'react-native-responsive-screen';
//import {useDispatch} from 'react-redux';
import { login, logout, unlink, getProfile, getAccessToken} from '@react-native-seoul/kakao-login';
import { useNavigation } from "@react-navigation/native";
import { defaultFontText as Text } from "../../components/Text";
import Button from "../../components/Button";
import { kakaoRedirectURL } from "../../utils/OAuth";
const LoginScreen = () => {
const navigation = useNavigation();
const [loading, setLoading] = React.useState(false);
const [errortext, setErrortext] = useState('');
const [kakaoToken, setKakaoToken] = useState('');
const signInWithKakao = async () => {
try {
const token = await login();
const kakaoProfileResult = await getProfile();
const accessToken = await getAccessToken();
setKakaoToken(JSON.stringify(token));
console.log(kakaoProfileResult);
return fetch(`${kakaoRedirectURL}`, {
method: 'POST',
body: JSON.stringify({kakaoProfileResult, accessToken}),
headers: {
'Content-Type': 'application/json',
},
})
.then(response => response.json())
.then(responseJson => {
setLoading(false);
console.log(JSON.stringify(responseJson));
if(responseJson.status === 200){
AsyncStorage.setItem('user_email', responseJson.email);
AsyncStorage.setItem('user_token', responseJson.token);
AsyncStorage.setItem('user_name', responseJson.username);
navigation.navigate("Main");
}else {
setErrortext(responseJson.message);
console.log('이메일 혹은 패스워드를 확인해주세요.');
}
})
}catch(err) {
console.error('login err', err);
}
};
const signOutWithKakao = async() => {
try {
const message = await logout();
console.log(message);
setKakaoToken('');
}catch (err) {
console.log('signOut error', err);
}
};
const getKakaoProfile = async () => {
try {
const profile = await getKakaoProfile();
setKakaoToken(JSON.stringify(profile));
} catch(err) {
console.error('signOut err', err);
}
};
return (
<SafeAreaView style={styles.container}>
<View style={styles.topArea}>
<View style={styles.textArea}>
<Image source={require("../../assets/logo/muggle_logo.png")} style={styles.logo}/>
<Text style={styles.text}>머글들은 모르는 뮤덕들의 세계</Text>
</View>
</View>
<View style={styles.btnArea}>
<Button opt={"apple"} text="Apple 아이디 로그인" />
<Button opt={"kakao"} text="카카오톡 아이디 로그인" handlePress={signInWithKakao}/>
<Button opt={"naver"} text="네이버 아이디 로그인" handlePress={() => {signInNaver(naverKey)}}/>
</View>
</SafeAreaView>
)
}
const styles = StyleSheet.create({
/* 스타일 생략 */
});
export default LoginScreen;
8. 백엔드 UserController
위의 리액트 코드에서 fetch로 통신을 보낸 kakaoRedirectURL과 일치하는 주소를 컨트롤러단에 PostMapping으로 매핑시켜준다.
JSON으로 받아오기 때문에 JSONObject로 받아서 파싱하여 처리.
STS에서 디버깅을 걸어보면 아래와 같이 토큰이 넘어온다! 사용자 정보도 잘 넘어옴!
9. DB 저장까지 완료!
반응형
'💻 my code archive > ✨React-Native' 카테고리의 다른 글
[RN 프로젝트] #10 리액트네이티브 크롤링 cheerio 사용법 (1) | 2024.02.04 |
---|---|
[RN 프로젝트] #9 리액트네이티브 + 스프링부트(Spring Boot) + JWT 네이버 로그인 구현, 로그아웃 (0) | 2023.12.23 |
[RN 프로젝트] #7 리액트네이티브 fetch + KOPIS 공연 API 가져오기, XML to JSON (React-native-xml2js) (0) | 2023.08.20 |
[RN 프로젝트] #6 리액트 네이티브 커스텀 네비게이션 헤더 NavigationHeader 컴포넌트 생성하기 (0) | 2023.01.29 |
[RN 프로젝트] #5 FlatList 사용법, 마이페이지 화면 만들기 (0) | 2023.01.29 |