반응형
https://soohyun6879.tistory.com/211
이전 포스팅에 이어서 카카오 로그인을 구현해보겠습니다!
OAuth
OAuth는 인터넷 사용자들이 비밀번호를 제공하지 않고 다른 웹사이트 상의 자신들의 정보에 대해 웹사이트나 애플리케이션의 접근 권한을 부여할 수 있는 공통적인 수단으로서 접근 위임을 위한 개방형 표준이다. 사용자가 애플리케이션에게 모든 권한을 넘기지 않고 사용자 대신 서비스를 이용할 수 있게 해주는 HTTP 기반의 보안 프로토콜 이다.
1. 카카오 인가코드 받기
https://developers.kakao.com/console/app
위 링크에서 인가코드를 받는다. 인가코드를 받으면 다음과 같은 화면을 볼 수 있다.
2. 카카오 로그인 구현
카카오 로그인을 할 때 카카오의 사용자 정보 중 id를 저장하기 위해 kakaoId 필드를 추가해준다.
@Setter
@Getter // get 함수를 일괄적으로 만들어줍니다.
@NoArgsConstructor // 기본 생성자를 만들어줍니다.
@Entity // DB 테이블 역할을 합니다.
public class User extends Timestamped {
public User(String username, String password, String email, UserRole role) {
this.username = username;
this.password = password;
this.email = email;
this.role = role;
this.kakaoId = null;
}
public User(String username, String password, String email, UserRole role, Long kakaoId) {
this.username = username;
this.password = password;
this.email = email;
this.role = role;
this.kakaoId = kakaoId;
}
// ID가 자동으로 생성 및 증가합니다.
@GeneratedValue(strategy = GenerationType.AUTO)
@Id
private Long id;
// 반드시 값을 가지도록 합니다.
@Column(nullable = false)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String email;
@Column(nullable = false)
@Enumerated(value = EnumType.STRING)
private UserRole role;
@Column(nullable = true)
private Long kakaoId;
}
카카오 로그인을 처리하기 위해 UserService 와 UserRepository 를 수정한다. 마찬가지로 WebSecurityConfig 도 수정한다.
@Service
public class UserService {
private final PasswordEncoder passwordEncoder;
private final UserRepository userRepository;
private final KakaoOAuth2 kakaoOAuth2;
private final AuthenticationManager authenticationManager;
private static final String ADMIN_TOKEN = "AAABnv/xRVklrnYxKZ0aHgTBcXukeZygoC";
@Autowired
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, KakaoOAuth2 kakaoOAuth2, AuthenticationManager authenticationManager) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.kakaoOAuth2 = kakaoOAuth2;
this.authenticationManager = authenticationManager;
}
// 회원가입 처리
public void registerUser(SignupRequestDto requestDto) {
String username = requestDto.getUsername();
// 회원 ID 중복 확인
Optional<User> found = userRepository.findByUsername(username);
if (found.isPresent()) {
throw new IllegalArgumentException("중복된 사용자 ID 가 존재합니다.");
}
// 패스워드 인코딩
String password = passwordEncoder.encode(requestDto.getPassword());
String email = requestDto.getEmail();
// 사용자 ROLE 확인
UserRole role = UserRole.USER;
if (requestDto.isAdmin()) {
if (!requestDto.getAdminToken().equals(ADMIN_TOKEN)) {
throw new IllegalArgumentException("관리자 암호가 틀려 등록이 불가능합니다.");
}
role = UserRole.ADMIN;
}
User user = new User(username, password, email, role);
userRepository.save(user);
}
// 카카오 로그인 처리
public void kakaoLogin(String authorizedCode) {
// 카카오 OAuth2 를 통해 카카오 사용자 정보 조회
KakaoUserInfo userInfo = kakaoOAuth2.getUserInfo(authorizedCode);
Long kakaoId = userInfo.getId();
String nickname = userInfo.getNickname();
String email = userInfo.getEmail();
// 우리 DB 에서 회원 Id 와 패스워드
// 회원 Id = 카카오 nickname
String username = nickname;
// 패스워드 = 카카오 Id + ADMIN TOKEN
String password = kakaoId + ADMIN_TOKEN;
// DB 에 중복된 Kakao Id 가 있는지 확인
User kakaoUser = userRepository.findByKakaoId(kakaoId)
.orElse(null);
// 카카오 정보로 회원가입
if (kakaoUser == null) {
// 패스워드 인코딩
String encodedPassword = passwordEncoder.encode(password);
// ROLE = 사용자
UserRole role = UserRole.USER;
kakaoUser = new User(nickname, encodedPassword, email, role, kakaoId);
userRepository.save(kakaoUser);
}
// 로그인 처리
Authentication kakaoUsernamePassword = new UsernamePasswordAuthenticationToken(username, password);
Authentication authentication = authenticationManager.authenticate(kakaoUsernamePassword);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
Optional<User> findByKakaoId(Long kakaoId);
}
@Configuration
@EnableWebSecurity // 스프링 Security 지원을 가능하게 함
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder encodePassword() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.headers().frameOptions().disable();
http.authorizeRequests()
// image 폴더를 login 없이 허용
.antMatchers("/images/**").permitAll()
// css 폴더를 login 없이 허용
.antMatchers("/css/**").permitAll()
// 회원 관리 URL 전부를 login 없이 허용
.antMatchers("/user/**").permitAll()
// h2-console URL을 login 없이 허용
.antMatchers("/h2-console/**").permitAll()
// 그 외 모든 요청은 인증과정 필요
.anyRequest().authenticated() // 어떤 요청이든 로그인 과정이 없으면 로그인을 하게 한다
.and()
.formLogin() // 로그인 페이지에 대해서는 허용
.loginPage("/user/login") // 로그인 위치
.loginProcessingUrl("/user/login") // 로그인 에러 위치
.defaultSuccessUrl("/") // 로그인한 후 이동할 페이지
.permitAll()
.and()
.logout() // 로그아웃 기능 허용
.logoutUrl("/user/logout")
.permitAll()
.and()
.exceptionHandling()
.accessDeniedPage("/user/forbidden"); // 인가되지 않았을 때 이동할 페이지
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
KakaoUserInfo 는 카카오를 통해 가져온 사용자 정보를 저장할 클래스이다. KakaoOAuth2 를 통해 카카오에서 사용자 정보를 가져온다.
@AllArgsConstructor
@Getter
public class KakaoUserInfo {
Long id;
String email;
String nickname;
}
@Component
public class KakaoOAuth2 {
public KakaoUserInfo getUserInfo(String authorizedCode) {
// 1. 인가코드 -> 액세스 토큰
String accessToken = getAccessToken(authorizedCode);
// 2. 액세스 토큰 -> 카카오 사용자 정보
KakaoUserInfo userInfo = getUserInfoByToken(accessToken);
return userInfo;
}
private String getAccessToken(String authorizedCode) {
// 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", "authorization_code");
params.add("client_id", "*{본인의 REST API키}*");
params.add("redirect_uri", "http://localhost:8080/user/kakao/callback");
params.add("code", authorizedCode);
// HttpHeader와 HttpBody를 하나의 오브젝트에 담기
RestTemplate rt = new RestTemplate();
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
);
// JSON -> 액세스 토큰 파싱
String tokenJson = response.getBody();
JSONObject rjson = new JSONObject(tokenJson);
String accessToken = rjson.getString("access_token");
return accessToken;
}
private KakaoUserInfo getUserInfoByToken(String accessToken) {
// HttpHeader 오브젝트 생성
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + accessToken);
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
// HttpHeader와 HttpBody를 하나의 오브젝트에 담기
RestTemplate rt = new RestTemplate();
HttpEntity<MultiValueMap<String, String>> kakaoProfileRequest = new HttpEntity<>(headers);
// Http 요청하기 - Post방식으로 - 그리고 response 변수의 응답 받음.
ResponseEntity<String> response = rt.exchange(
"https://kapi.kakao.com/v2/user/me",
HttpMethod.POST,
kakaoProfileRequest,
String.class
);
JSONObject body = new JSONObject(response.getBody());
Long id = body.getLong("id");
String email = body.getJSONObject("kakao_account").getString("email");
String nickname = body.getJSONObject("properties").getString("nickname");
return new KakaoUserInfo(id, email, nickname);
}
}
반응형
'Back-end > Spring' 카테고리의 다른 글
[SpringBoot] 9주차 스터디 (페이징, JPA 연관관계) (0) | 2021.10.17 |
---|---|
[SpringBoot] 8주차 스터디 (단위테스트, 통합테스트) (0) | 2021.10.07 |
[SpringBoot] 7주차 스터디 ① (스프링 시큐리티) (0) | 2021.10.02 |
[SpringBoot] 6주차 스터디(객체 지향, DI) (0) | 2021.09.23 |
[SpringBoot] 5주차 스터디 (AWS RDS, EC2) (0) | 2021.09.16 |