반응형
🔍스프링 시큐리티와 스프링 시큐리티 Oauth2 클라이언트?
- 많은 서비스에서 구글, 페이스북 등 소셜 로그인 기능을 사용하는 추세.
- 그 이유는 직접 구현하는 것보다 OAuth를 써서 구현하는 것이 더 간단하기 때문이다.
- 로그인 시 보안, 비밀번호 변경&찾기 등등 모든 서비스를 소셜 서비스에 맡기고 그외의 개발에 집중할 수 있다.
🤍구글 서비스 등록
1. 구글 서비스 등록
구글 클라우드 플랫폼 으로 이동해서 프로젝트를 만든다.
API 및 서비스로 이동해서 사용자 인증 정보를 만든다.
먼저 동의화면을 구성해준 다음
OAuth 클라이언트 ID를 만들어준다.
- 스프링 부트2 버전의 시큐리티에서는 기본적으로 {도메인}/login/oauth2/code/{소셜서비스코드} 리다이렉트 URL을 지원하고 있다.
- 사용자가 별도로 리다이렉트 URL을 지원하는 컨트롤러를 만들 필요가 없다.
2. application-oauth.properties 파일 생성
scope=profile,email
- 기본값.
- opinid라는 scope가 있으면 Open id Provider로 인식하기 때문.
- ㄴ구글과 그외의 서비스(네이버, 카카오 등)로 나누어 만들어야 하기 때문에 하나의 OAuth2Service로 사용하기 위해 기본값으로 강제 설정함.
3. application.properties 파일에 설정 코드 추가
spring.profiles.include=oauth
4. .위 개인 정보를 제외하고 커밋되도록 .gitignore에 코드 추가
application-oauth.properties
제외하고 잘 올라갔다.
🤍구글 로그인 구현하기
1. User 엔티티 생성
2. Enum 클래스 Role 생성
3. User CRUD 관련 UserRepository 생성
public interface UserRepository extends JpaRepository<User, Long> {
//소셜 로그인으로 반환되는 값 중 email을 통해 이미 생성된 사용자인지 판단하기 위한 메소드
Optional<User> findByEmail(String email);
}
🤍스프링 시큐리티 설정
4. 의존성 추가
- 소셜 로그인 등 소셜 기능 구현 시 필요한 의존성
//소셜 로그인 등 클라이언트 입장에서 소셜 기능 구현 시 필요한 의존성
implementation('org.springframework.boot:spring-boot-starter-oauth2-client')
5. 시큐리티 관련 클래스가 담긴 config.auth 패키지 만들기
SecurityConfig
- @EnableWebSecurity : 스프링 시큐리티 설정 활성화
- authorizeRequests : URL별 권한 관리 설정. 선언되어야만 anMatchers 옵션 사용 가능
- anMatchers : 권한 관리 대상을 지정하는 옵션임. 여기에서는 /* 등 지정된 URL은 전체 열람 권한을 주고(permitAll()), 특정 주소 API는 USER 권한을 가진 사람만 가능하도록 구현함.
- loglut().logoutSuccessUrl("/") : 로그아웃 설정 진입점, 로그아웃 성공시 / 주소로 이동
- oauth2Login : 로그인 기능에 대한 설정 진입점
- userInfoEndopoint : 로그인 성공 후 사용자 정보를 가져올 때 설정 담당
@RequiredArgsConstructor
@EnableWebSecurity //스프링 시큐리티 설정 활성화
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final CustomOAuth2UserService customOAuth2UserService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.headers().frameOptions().disable() //h2-console 화면 사용 위해 해당 옵션들을 disable하겠다.
.and()
.authorizeRequests() //URL별 권한 관리를 설정하는 옵션의 시작점. 선언되어야만 antMatchers 옵션 사용 가능
//권한 관리 대상을 지정하는 옵션
.antMatchers("/", "/css/**", "/images/**", "/js/**", "/h2-console/**").permitAll()
.antMatchers("/api/v1/**").hasRole(Role.USER.name())
//설정된 값 이외 나머지 URL들을 나타냄.
//나머지 URL들은 모두 인증된 사용자들에게만 허용
.anyRequest().authenticated()
.and()
.logout()
.logoutSuccessUrl("/") //로그아웃 기능에 대한 여러 설정의 진입점
.and()
.oauth2Login()
.userInfoEndpoint()
.userService(customOAuth2UserService);
}
}
CustomOAuth2UserService
- 구글 로그인 후 가져온 사용자 정보를 기반으로 가입, 정보수정, 세션 저장 등의 기능을 지원함.
@RequiredArgsConstructor
@Service
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final UserRepository userRepository;
private final HttpSession httpSession;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
OAuth2User oAuth2User = delegate.loadUser(userRequest);
//현재 로그인 진행 중인 서비스를 구분하는 코드(추후 네이버 로그인 추가 시 네이버인지 구글인지 구분하기 위해 사용)
String registrationId = userRequest.getClientRegistration().getRegistrationId();
//PK와 같은 의미
//구글의 기본 코드는 "sub"
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());
User user = saveOrUpdate(attributes);
httpSession.setAttribute("user", new SessionUser(user));
return new DefaultOAuth2User(
Collections.singleton(new SimpleGrantedAuthority(user.getRoleKey())),
attributes.getAttributes(),
attributes.getNameAttributeKey());
}
private User saveOrUpdate(OAuthAttributes attributes){
User user = userRepository.findByEmail(attributes.getEmail()).map(entity->entity.update(attributes.getName(),attributes.getPicture()))
.orElse(attributes.toEntity());
return userRepository.save(user);
}
}
OAuthAttributes
- DTO
@Getter
public class OAuthAttributes {
private Map<String, Object> attributes;
private String nameAttributeKey;
private String name;
private String email;
private String picture;
@Builder
public OAuthAttributes(Map<String, Object> attributes, String nameAttributeKey, String name, String email, String picture){
this.attributes = attributes;
this.nameAttributeKey = nameAttributeKey;
this.name = name;
this.email = email;
this.picture = picture;
}
//OAuth2User에서 반환하는 사용자 정보가 Map이기 때문에 값 하나하나 변환 필요.
public static OAuthAttributes of(String registrationId, String userNameAttributeName, Map<String, Object> attributes){
return ofGoogle(userNameAttributeName, attributes);
}
public static OAuthAttributes ofGoogle(String userNameAttributeName, Map<String, Object> attributes){
return OAuthAttributes.builder()
.name((String)attributes.get("name"))
.email((String)attributes.get("email"))
.picture((String)attributes.get("picture"))
.attributes(attributes)
.nameAttributeKey(userNameAttributeName)
.build();
}
public User toEntity(){
return User.builder() //가입 시 기본 권한을 GUEST로 주겠다.
.name(name).email(email).picture(picture).role(Role.GUEST)
.build();
}
}
SessionUser
- User 클래스를 그대로 사용하지 않는 이유는 User 클래스에 직렬화를 구현하지 않았기 때문임.
- User 클래스는 엔티티이기 때문에 언제 다른 엔티티와 관계가 형성될지 몰라 직렬화 코드를 넣을 수 없음.
- 그러므로 직렬화 기능을 가진 세션 Dto를 하나 추가로 만드는 것이 이후 유지 보수에 많은 도움이 됨.
@Getter
public class SessionUser implements Serializable {
/*
세션user는 인증된 사용자 정보만 필요로 하므로 name, email, picture만 필드로 선언함.
*/
private String name;
private String email;
private String picture;
public SessionUser(User user){
this.name = user.getName();
this.email = user.getEmail();
this.picture = user.getPicture();
}
}
6-1. IndexController 수정
- 로그인 화면단(index.mustache)에서 userName을 사용할 수 있도록 userName을 model에 저장
- 로그인 성공 시 세션에 SessionUser를 저장하도록 구성.
하지만, 이렇게 되면 반복적인 코드가 생기므로 어노테이션 기반으로 개선해 본다.
먼저 @LoginUser 어노테이션 생성
@Target(ElementType.PARAMETER) //어노테이션이 생성될 수 있는 위치 지정
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser { //어노테이션 클레스로 지정함.
}
LoginUserArgumentResolver 생성
- supportsParameter() : @LoginUser 어노테이션이 붙어있고 파라미터 클래스 타입이 SessionUser.class일 경우 true를 반환함.
- resolveArgument() : 파라미터에 전달할 객체를 생성함. 여기에서는 세션에서 객체를 가져옴.
WebConfig 생성
- LoginUserArgumentResolver가 스프링에서 인식될 수 있도록함.
6-2. IndexController를 어노테이션 기반으로 다시 수정한 코드
7. 로그인 테스트
- 구글 로그인 버튼 추가
- 누르면 우리가 흔히 알고 있는 구글 로그인 화면이 나온다!
- 자신의 구글 아이디로 로그인하면 짠!
- 회원가입이 완료되어 USER 테이블에서도 확인할 수 있다!
🤍세션 저장소로 데이터베이스 사용하기
- 현재 구현한 코드로는 애플리케이션 재실행 시 로그인이 풀려버린다. 세션이 내장 톰캣의 메모리에 저장되기 때문이다.
- 해결 방법으로는 톰캣 세션 사용 / 데이터베이스 세션 저장소로 사용 / 메모리 DB 세션 저장소로 사용 3가지 방법이 있고 그중 2번째 방법을 선택해 보도록 한다.
1. 의존성 추가
//데이터베이스를 세션 저장소로 사용하기 위해 추가
implementation('org.springframework.session:spring-session-jdbc:')
2. application.properties 코드 추가
spring.session.store-type=jdbc
반응형
'💻 my code archive > 🏷️JAVA & Spring(Boot)' 카테고리의 다른 글
스프링부트 공부기록(12) - AWS 배포 :: 회원가입, EC2 인스턴스 생성하기 (0) | 2022.03.02 |
---|---|
스프링부트 공부기록(11) - 게시판 프로젝트 :: 네이버 API 로그인 구현하기 (1) | 2022.03.02 |
스프링부트 공부기록(9) - 게시판 프로젝트 :: 머스테치 사용, 게시판 글 조회, 수정, 삭제 구현하기 (0) | 2022.02.27 |
스프링부트 공부기록(8) - 게시판 프로젝트 :: 머스테치 사용, 게시판 글 등록 구현하기 (0) | 2022.02.27 |
스프링부트 공부기록(7) - 머스테치(Mustache) 화면 구성하기 (0) | 2022.02.27 |