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 |