Developer Note/국비과정 수업내용 정리&저장

24년 12월 3일

DH_PARK 2024. 12. 15. 09:22

UserDetails 인터페이스

스프링 시큐리티에서 사용자의 정보를 담는 인터페이스이다.

사용자의 정보를 불러오기 위해 구현해야 하는 인터페이스로 기본 재정의 메서드는 다음과 같다.

메소드 리턴 타입 설명 기본값

getAuthorities() Collection<? extends GrantedAuthority> 계정의 권한 목록을 리턴  
getPassword() String 계정의 비밀번호를 리턴  
getUsername() String 계정의 고유한 값을 리턴( ex : DB PK값, 중복이 없는 이메일 값 )  
isAccountNonExpired() boolean 계정의 만료 여부 리턴 true ( 만료 안됨 )
isAccountNonLocked() boolean 계정의 잠김 여부 리턴 true ( 잠기지 않음 )
isCredentialsNonExpired() boolean 비밀번호 만료 여부 리턴 true ( 만료 안됨 )
isEnabled() boolean 계정의 활성화 여부 리턴 true ( 활성화 됨 )

이 메서드들 중에서 getUsername() 메서드는 계정의 고유한 값인데 , 프로젝트를 진행할 때

유저의 이메일을 넘겨주게 하는 경우가 있다. 웬만해서는 상관없다고 생각하긴 하는데

SSO 같은 서버를 만들게 되면 중복이 될 수도 있어서 이메일을 전달하기보다는 User 테이블에 있는 PK 값을 넘겨주는 방식을 권장한다.

우선 나는 수업 때 이런식으로 메서드를 재정의했다.

package com.example.demo.config.auth;

import java.security.Principal;
import java.util.ArrayList;
import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import com.example.demo.domain.dto.UserDto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class PrincipalDetails implements UserDetails{

	private UserDto userDto;
	
	@Override //해당 유저의 권한 목록
	public Collection<? extends GrantedAuthority> getAuthorities() {
		Collection<GrantedAuthority> authorities = new ArrayList();
		authorities.add(new SimpleGrantedAuthority(userDto.getRole()));
		return authorities;
	}

	@Override //비밀번호
	public String getPassword() {
		return userDto.getPassword();
	}

	@Override //고유 ID , PK값
	public String getUsername() {
		return userDto.getUsername();
	}

	@Override //계정 만료 여부 , true : 만료 안됨 false : 만료
	public boolean isAccountNonExpired() {
		// TODO Auto-generated method stub
		return true;
	}

	@Override //계정 잠김 여부 , true 잠기지 않음 false : 잠김
	public boolean isAccountNonLocked() {
		// TODO Auto-generated method stub
		return true;
	}

	@Override 비밀번호 만료 여부 , true 만료 안됨 false 만료
	public boolean isCredentialsNonExpired() {
		// TODO Auto-generated method stub
		return true;
	}

	@Override 사용자 활성화 여부 true 활성화 false 비활성화
	public boolean isEnabled() {
		// TODO Auto-generated method stub
	
		return true;
	}

}


UserDetailService 란?

위의 UserDetails 이 사용자의 정보를 담는 인터페이스였다면 이 “UserDetailService” 는

사용자 or 유저의 정보를 가져오는 인터페이스이다.

재정의 할 메서드는 하나밖에 없다.

loadUserByUsername UserDetails 유저의 정보를 불러와서 UserDetails로 리턴

package com.example.demo.config.auth;

import com.example.demo.domain.dto.UserDto;
import com.example.demo.domain.entity.User;
import com.example.demo.domain.repository.UserRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.Optional;

@Service
@Slf4j
public class PrincipalDetailsServiceImpl implements UserDetailsService{

//	@Autowired
//	private UserMapper userMapper;
@Autowired
private UserRepository userRepository;

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

		Optional<User> userOptional=userRepository.findById(username); //username 으로 특정 유저 정보를 불러온다
		if(userOptional.isEmpty())
			throw new UsernameNotFoundException(username); //유저 정보가 없다면 예외발생

		User user=userOptional.get(); //user 정보를 담는다.

		//entity -> dto  
		UserDto userDto = UserDto.builder() //빌더 패턴으로 userDto 변환
				.username(user.getUsername())
				.password(user.getPassword())
				.role(user.getRole())
				.build();

return new PrincipalDetails(userDto);
}
}

 

이 코드에서는 불러온 엔티티를 dto 로 변환하는 작업을 했다.

dto 나 엔티티를 서로 변환시키는 작업은 service 계층에서 하는게 좋다고 한다.


실습

간단하게 로그인 기능을 한번 구현해보도록 하자.

  @PostMapping("/join")
    public void join_post(UserDto dto){
        log.info("join submit"+dto);

        User user = User.builder()
                .username(dto.getUsername())
                .password(passwordEncoder.encode(dto.getPassword()) )
                .role("ROLE_USER")
                .build();

        userRepository.save(user);
    }

로컬호스트에서 join 페이지로 가서 회원가입을 진행한다.

정보를 입력하고 회원가입을 하면 DB 에 내 정보가 저장된다.

그리고 위 principalDetailService 클래스에서 간단하게 진행한 유효성 검사를 통해 DB 에 값이 있는지확인하고 인증을 한다.

@GetMapping("/user")
	public void user(Authentication authentication, @AuthenticationPrincipal PrincipalDetails principalDetails, Model model) {
		log.info("GET /user..." + authentication);
		log.info("name..." + authentication.getName());
		log.info("principal..." + authentication.getPrincipal());
		log.info("authorities..." + authentication.getAuthorities());
		log.info("details..." + authentication.getDetails());
		log.info("credential..." + authentication.getCredentials());

        model.addAttribute("authentication", authentication);
        model.addAttribute("principal", principalDetails);

	}

그리고 @AuthenticationPrincipal 어노테이션을 사용해서 유저 인증정보를 가져온다

이 어노테이션을 사용하면 인증정보를 확인할 때 매번 DB를 확인하지 않고 세션에 있는 정보를 사용해서 인증정보를 확인할 수 있다.

 

여기까지가 일반 로그인 작업을 구현하는 과정이었다. 이제 Oauth2 로그인에 대해서 알아보자.


스프링시큐리티 oauth2 로그인 구현

자바에서는 oauth2 로그인을 하면 이 정보가 OAuth2User 타입 객체로 세션에 저장이 된다.

그리고 이후에 내정보 조회 API 를 만든다면

소셜로그인 사용자 정보 조회 기능은 세션에서 OAuth2User 타입 객체를 꺼내서 써야하고

일반로그인 사용자 정보 조회 기능에서는 세션에서 UserDetails 타입 객체를 꺼내서 쓰면 된다.

사용법

우선 application.properties 에 구글 로그인을 가능하게 해주는 설정을 해준다.

#Google
spring.security.oauth2.client.registration.google.client-id=-발급받은 ID
spring.security.oauth2.client.registration.google.client-secret=-발급받은 secret Key
spring.security.oauth2.client.registration.google.scope=email,profile

그리고

<a *href*="/oauth2/authorization/google">구글 로그인</a><br>

HTML 코드에서 이런 주소로 로그인 요청을 보내면 로그인화면이 나온다

 

 

 

여기서 로그인을 하면 각종 요청들을 받게 되는데 , 이 값들을 dto 에 받아서 DB에 조회 한 뒤 ,

값이 존재한다면 그대로 access_token 을 발급받기 위한 과정으로 넘어가고 ,

DB에 받아온 UserDto 와 같은 데이터가 존재하지 않는다면 새로운 entity를 생성해 db에 추가한다.

 //DB 조회
        String username = oAuth2UserInfo.getProvider()+"_"+oAuth2UserInfo.getProviderId();
        String password = passwordEncoder.encode("1234");
        Optional<User> userOptional =  userRepository.findById(username);

        UserDto userDto = null;
        if(userOptional.isPresent()){
            //기존계정이 존재 Entity->Dto
            User user = userOptional.get();
            userDto = new UserDto();
            userDto.setUsername(user.getUsername());
            userDto.setPassword(user.getPassword());
            userDto.setRole(user.getRole());
            userDto.setProvider(user.getProvider());
            userDto.setProviderId(user.getProviderId());

        }else{
            //새로운계정 DB저장
            //Entity 생성 값
            User user = new User();
            user.setUsername(username);
            user.setPassword(password);
            user.setRole("ROLE_USER");
            user.setProvider(oAuth2UserInfo.getProvider());
            user.setProviderId(oAuth2UserInfo.getProviderId());
            userRepository.save(user);

            userDto = new UserDto();
            userDto.setUsername(username);
            userDto.setPassword(password);
            userDto.setRole("ROLE_USER");
            userDto.setProvider(oAuth2UserInfo.getProvider());
            userDto.setProviderId(oAuth2UserInfo.getProviderId());
        }

oauth2로그인에 대한 소스 코드.


optional 이란 ?

NullPointerException 문제를 줄이기 위한 수단 같은거라고 한다.

Optional<User> userOptional =  userRepository.findById(username);

User 안에 객체가 존재한다면 그 값을 포함하고 존재하지 않으면 비어있는 Optional 을 반환한다.

'Developer Note > 국비과정 수업내용 정리&저장' 카테고리의 다른 글

24년 12월 5일  (1) 2024.12.15
24년 12월 4일  (0) 2024.12.15
24년 12월 2일  (2) 2024.12.10
24년 11월 28일  (1) 2024.12.08
24년 11월 27일  (2) 2024.12.05