개발자가 Provider에게 Client 등록을 해야 사용자들에 대한 정보를 받을 수 있다
등록하는 장소
GOOGLE : https://console.cloud.google.com/
FACEBOOK : https://developers.facebook.com/
OKTA
추가 가능한 OAuth2Provider
naver : https://developers.naver.com/
kakao : https://developers.kakao.com/
facebook, naver, kakao
OAuth2 Provider의 Resource Server에는 사이트에 가입한 사용자 정보(ex: 아이디, 성별, 생일, 생년월일, 전화번호 등)가 들어있다
개발자가 보통 구현하는 것은 Client Server(인증 역할X)
-> Provider에게 사용자 정보를 달라고 요청(이 정보를 바탕으로 관리할려고 하는 것)
-> 로그인을 Provider에게 위임을 하고 인증이 완료되면 해당 사이트의 Provider의 Authentication Server가 사용자에게 3rd Party Server가 너에 대한 정보를 요청했어? 제공할래?
-> 제공을 하겠다 하면 AuthenticationServer가 서비스 사용자에게 키를 주고 이 키로 3rd Party Server는 Resource Server를 찾아가 정보를 요청 함
-> 우리 사용자가 당신에게 이 정보 가져가도 된데! 가져갈게!
-> 데이터를 주면 3rd Party Server는 DB에 사용자 정보를 저장
-> 이후 서비스를 이용하는 사용자가 똑같이 Provider를 통해서 로그인을 해주면 이 사용자는 기존에 등록되어 있던 사용자와 연결된 자신의 개인 정보를 가지고 서비스를 하게 되는 것
구글은 OAuth2가 아닌 OidcUser 방식의 서비스를 함
-> 이는 OAuth2를 기반으로 발전된 방법
application.yml
server:
port: 9061
## 구글같은 경우에는 provider는 이미 등록이 되어있다
spring:
security:
oauth2:
client:
registration:
google:
client-id: 클라이언트 아이디 넣기
client-secret: 클라이언트 secret 키
## 네이버의 경우에는 프로바이더 등록해야함
# provider:
# naver:
# kakao:
config
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SnsLoginSecurityConfig extends WebSecurityConfigurerAdapter {
private OidcUserService oidcUserService; // 원리 이해할려면 이 코드 분석
private OidcAuthorizationCodeAuthenticationProvider oidcAuthorizationCodeAuthenticationProvider;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.oauth2Login(); // oauth2 로그인 필터 동작
}
}
controller
@RestController
public class HomeController {
@PreAuthorize("isAuthenticated()")
@GetMapping("/greeting")
// OAuth2로 로그인을 하게 되면 userPrincipal에 OAuth2User가 오게 됨
public OAuth2User greeting(@AuthenticationPrincipal OAuth2User user){
return user;
}
}
greeting이라는 서비스를 할려고 들어가면 구글 로그인으로 리다이렉 됨
-> OAuth2로그인을 제공할 수 있는 Provider는 현재 구글만 등록
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
@Table(name = "sp_oauth2_user")
public class SpOAuth2User {
// 사이트를 통해 들어온 사용자의 정보를 이곳에 저장
@Id
private String oauth2UserId; // google-{id}, naver-{id} 이렇게 유니크한 아이디를 키 값으로 사용
private Long userId; // SpUser
private String name;
private String email;
private LocalDateTime created;
private Provider provider;
// 어떤 사용자 인지
public static enum Provider{
google{
public SpOAuth2User convert(OAuth2User user){
return SpOAuth2User.builder()
.oauth2UserId(format("%s_%s", name(), user.getAttribute("sub")))
.provider(google)
.email(user.getAttribute("email"))
.name(user.getAttribute("name"))
.created(LocalDateTime.now())
.build();
}
},
naver{
public SpOAuth2User convert(OAuth2User user){
Map<String, Object> resp = user.getAttribute("response");
return SpOAuth2User.builder()
.oauth2UserId(format("%s_%s", name(), resp.get("id")))
.provider(naver)
.email(""+resp.get("email"))
.name(""+resp.get("name"))
.build();
}
};
public abstract SpOAuth2User convert(OAuth2User userInfo);
}
}
public interface SpOAuth2Repository extends JpaRepository<SpOAuth2User, String> {
}
@Service
@Transactional
public class SpUserService implements UserDetailsService {
@Autowired
private SpUserRepository userRepository;
@Autowired
private SpOAuth2Repository spOAuth2Repository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepository.findUserByEmail(username).orElseThrow(
()->new UsernameNotFoundException(username));
}
public Optional<SpUser> findUser(String email) {
return userRepository.findUserByEmail(email);
}
public SpUser save(SpUser user) {
return userRepository.save(user);
}
public void addAuthority(Long userId, String authority){
userRepository.findById(userId).ifPresent(user->{
SpAuthority newRole = new SpAuthority(user.getUserId(), authority);
if(user.getAuthorities() == null){
HashSet<SpAuthority> authorities = new HashSet<>();
authorities.add(newRole);
user.setAuthorities(authorities);
save(user);
}else if(!user.getAuthorities().contains(newRole)){
HashSet<SpAuthority> authorities = new HashSet<>();
authorities.addAll(user.getAuthorities());
authorities.add(newRole);
user.setAuthorities(authorities);
save(user);
}
});
}
public void removeAuthority(Long userId, String authority){
userRepository.findById(userId).ifPresent(user->{
if(user.getAuthorities()==null) return;
SpAuthority targetRole = new SpAuthority(user.getUserId(), authority);
if(user.getAuthorities().contains(targetRole)){
user.setAuthorities(
user.getAuthorities().stream().filter(auth->!auth.equals(targetRole))
.collect(Collectors.toSet())
);
save(user);
}
});
}
public SpUser load(SpOAuth2User oAuth2User){
SpOAuth2User dbUser = spOAuth2Repository.findById(oAuth2User.getOauth2UserId())
// 사용자가 등록되어있지 않다면 직접 Db에 등록하고 가져와야 함
.orElseGet(() -> {
SpUser user = new SpUser();
// 같은 사용자여도 어떤 플랫폼을 통해 로그인을 했냐에 따라 다른 사용자로 가정
user.setEmail(oAuth2User.getEmail());
user.setName(oAuth2User.getName());
user.setEnabled(true);
userRepository.save(user);
addAuthority(user.getUserId(), "ROLE_USER");
oAuth2User.setUserId(user.getUserId());
return spOAuth2Repository.save(oAuth2User);
});
return userRepository.findById(dbUser.getUserId()).get();
}
}
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SnsLoginSecurityConfig extends WebSecurityConfigurerAdapter {
// @Autowired
// private SpOAuth2UserService oAuth2UserService;
//
// @Autowired
// private SpOidcUserService OidcUserService;
@Autowired
private SpOAuth2SuccessHandler successHandler; // 이를 통해 유저를 맵핑
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2->oauth2
// .userInfoEndpoint(
// userInfo->userInfo.userService(oAuth2UserService)
// .oidcUserService(OidcUserService)
// ).successHandler()
.successHandler(successHandler)
);
}
}
@Component
public class SpOAuth2SuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private SpUserService userService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException
{
Object principal = authentication.getPrincipal();
if(principal instanceof OidcUser){
// OidcUser가 하위 객체라 먼저 살펴봐야 함
// 구글 사용자
SpOAuth2User oauth = SpOAuth2User.Provider.google.convert((OidcUser) principal);
SpUser user = userService.load(oauth);
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(user, "", user.getAuthorities())
);
}else if(principal instanceof OAuth2User){
// naver 사용자
SpOAuth2User oauth = SpOAuth2User.Provider.naver.convert((OAuth2User) principal);
SpUser user = userService.load(oauth);
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(user, "", user.getAuthorities())
);
}
request.getRequestDispatcher("/").forward(request, response); // 루트 페이지로 강제 리다이렉
}
}
@Component
public class SpOAuth2UserService extends DefaultOAuth2UserService {
// 유저 정보가(request) 오면 여기서 OAuth2User를 로딩하는 부분 추가
// 페이스북, 카카오와 같은 벤더들은 OAuth2 User 스펙을 따르기에 이에 대한 정보를 줌
// 구글은 Odic User 스펙을 따름
// loadUser메서드를 추가해주면, OAuth or Oidc 유저를 통해 사용자가 들어왔을 때 이쪽으로 들어옴
// 여기서 사용자를 SpUser 사용자로 변환을 하거나 등록하는 과정 거칠수도 있음
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
return super.loadUser(userRequest);
}
}
@Component
public class SpOidcUserService extends OidcUserService {
@Override
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
return super.loadUser(userRequest);
}
}
@RestController
public class HomeController {
@PreAuthorize("isAuthenticated()")
@GetMapping("/")
// OAuth2로 로그인을 하게 되면 userPrincipal에 OAuth2User가 오게 됨
public Object greeting(@AuthenticationPrincipal Object user){
return user;
}
}