src/main/resources - application-oauth.properties 생성 - 이 파일은 .gitignore에 파일명 추가해서 형상관리에 올리지 않음.
spring.security.oauth2.client.registration.google.client-id=클라이언트id
spring.security.oauth2.client.registration.google.client-secret=클라이언트 보안 비밀
spring.security.oauth2.client.registration.google.scope=profile,email
src/main/resources - application.properties 수정.
# jpa sql show setting
spring.jpa.show_sql=true
# mustache korean encoding
server.servlet.encoding.force-response=true
# h2 : create table - id setting
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL57Dialect
spring.jpa.properties.hibernate.dialect.storage_engine=innodb
spring.datasource.hikari.jdbc-url=jdbc:h2:mem:testdb;MODE=MYSQL
spring.h2.console.enabled=true
spring.profiles.include=oauth
내부적으로 로그인 관련해서 다루기 위한 '사용자' 객체, '권한Role' 준비.
src/main/java/com/jojoldu/springboot/domain/user - User.java - Role 빨강은 그 다음에 만들거라 import 없이 일단 ㄱㄱ.
package com.jojoldu.springboot.domain.user;
import com.jojoldu.springboot.domain.posts.BaseTimeEntity;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Getter
@NoArgsConstructor
@Entity
public class User extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String email;
@Column
private String picture;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role;
@Builder
public User(String name, String email, String picture, Role role) {
this.name = name;
this.email = email;
this.picture = picture;
this.role = role;
}
public User update(String name, String picture) {
this.name = name;
this.picture = picture;
return this;
}
public String getRoleKey() {
return this.role.getKey();
}
}
src/main/java/com/jojoldu/springboot/domain/user - enum Role.java
Enum 값이 String 타입으로 저장하게 함. 기본은 int로 저장되지만 그냥 숫자로 저장되면 무슨 코드를 의미하는지 알 수 없음.
package com.jojoldu.springboot.domain.user;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum Role {
GUEST("ROLE_GURES", "손님"),
USER("ROLE_USER", "일반 사용자");
private final String key;
private final String title;
}
src/main/java/com/jojoldu/springboot/domain/user - interface UserRepository.java
이미 생성된 사용자인지, 처음 가입하는 사용자인지 확인하기 위한 findByEmail 함수
package com.jojoldu.springboot.domain.user;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
build.gradle.kts에 스프링 시큐리티 추가
oauth2-client 추가 > 소셜로그인할 때 꼭 필요.
spring-security-oauth2-client, spring-security-oauth2-jose 기본으로 관리해줌.
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("junit:junit:4.13.1")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("com.h2database:h2")
implementation("org.springframework.boot:spring-boot-starter-mustache")
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
compileOnly("org.projectlombok:lombok")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation(platform("org.junit:junit-bom:5.10.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
}
OAuth 관련해서 총 4개의 파일을 만들거임.
되도록이면, dto 먼저 만들고 Custion...과 Security 생성.
- OAuthAttributes = 소셜 로그인 인증 속성 객체. OAuth2UserService를 통해 가져온 OAuth2User의 attributes 속성을 담음.
naver, kakao, apple도 인증되고 나면 이 객체 사용.
- SessionUser = 세션에 담을 "인증된" 사용자 정보 객체 (그냥 User는 "인증된"게 아니고 @Entity? 라서? 별도로 사용)
- CustomOAuth2UserService = delegate가 loadUser해서 인증 속성을 가져옴.
클라이언트등록•등록ID 등을 saveOrUpdate(속성)함.
현재 세션에("user"에, (속성 담음))
- SecurityConfig = 요청url에 따라서 접근 & 권한 설정.
config.auth
├── dto
│ ├── OAuthAttributes
│ └── SessionUser
│
├── CustomOAuth2UserService
└── SecurityConfig
src/main/java/com/jojoldu/springboot/config/auth - 경로 생성.
src/main/java/com/jojoldu/springboot/config/auth/dto - class OAuthAttributes.java 생성
package com.jojoldu.springboot.config.auth;
import com.jojoldu.springboot.config.auth.dto.OAuthAttributes;
import com.jojoldu.springboot.config.auth.dto.SessionUser;
import com.jojoldu.springboot.domain.user.User;
import com.jojoldu.springboot.domain.user.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpSession;
import java.util.Collections;
@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(); // registrationId=google,apple,naver
String userNameAttributeName = userRequest.getClientRegistration()
.getProviderDetails()
.getUserInfoEndpoint()
.getUserNameAttributeName(); // userNameAttributeName=pk같은 필드값.
// google : sub (naver,kakao 없음)
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);
}
}
src/main/java/com/jojoldu/springboot/config/auth/dto - class SessionUser.java 생성
package com.jojoldu.springboot.config.auth.dto;
import com.jojoldu.springboot.domain.user.User;
import lombok.Getter;
import java.io.Serializable;
@Getter
public class SessionUser implements Serializable {
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();
}
}
src/main/java/com/jojoldu/springboot/config/auth - CustomOAuth2UserService.java
package com.jojoldu.springboot.config.auth.dto;
import com.jojoldu.springboot.domain.user.Role;
import com.jojoldu.springboot.domain.user.User;
import lombok.Builder;
import lombok.Getter;
import java.util.Map;
@Getter
public class OAuthAttributes {
/** OAuth2UserService를 통해 가져온 OAuth2User의 attributes 속성을 담음. naver, kakao, apple도 사용 */
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이라서 하나하나 변환. 다른 로그인이면 ofNaver와 같이 추가 */
public static OAuthAttributes of(String registrationId, String userNameAttributeName, Map<String, Object> attributes) {
return ofGoogle(userNameAttributeName, attributes);
}
private 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()
.name(name)
.email(email)
.picture(picture)
.role(Role.GUEST)
.build();
}
}
src/main/java/com/jojoldu/springboot/config/auth - class SecurityConfig 생성
package com.jojoldu.springboot.config.auth;
import com.jojoldu.springboot.domain.user.Role;
import lombok.RequiredArgsConstructor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@RequiredArgsConstructor
@EnableWebSecurity // build.gradle에서 추가함으로서, Spring Security 라이브러리 활성화
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 접근 못하게 막기
.and().authorizeRequests() // URL별 권한 관리 설정 옵션 시작점. 이게 있어야 antMatchers 메소드 체이닝 가능.
// .antMatchers = 권한 관리 대상 지정 옵션.
// perimitAll = 전체 열람 권한, hasRole = 특정 권한 부여
.antMatchers("/","/css/**","/images/**","/js/**","/h2-console/**").permitAll()
.antMatchers("/api/vi/**").hasRole(Role.USER.name())
.anyRequest().authenticated() // 그 외 나머지 요청URL 이라면 전부 인증된 사용자만 허용=로그인한 사용자.
.and().logout().logoutSuccessUrl("/") // 로그아웃 시 이동할 주소
.and().oauth2Login().userInfoEndpoint().userService(customOAuth2UserService);
}
}
*** SecurityConfig.java의 WebSecurityConfigurerAdapter < deprecated라서 오류인가 싶었음.
>> 나중에 구글 로그인, 개인정보제공 동의 까지 하고 나면 오류남. ㅠ
>>>> 어..? 로그인하고나서 'http://localhost:8080/oauth/authorization/google' 여기로 가는건 문젠데
>>>> 그 상태에서 다시 'localhost:8080'로 가면 로그인 되어있음.. 뭐지? < 화면단 주소가 /oauth2/여야하는데 /oauth/ 오타였음.
아래 소스 말고 위 소스 deprecated 소스 그대로 써도 동작함.
package com.jojoldu.springboot.config.auth;
import com.jojoldu.springboot.domain.user.Role;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final CustomOAuth2UserService customOAuth2UserService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.headers().frameOptions().disable()
.and()
.authorizeHttpRequests(authorize -> authorize
.antMatchers("/", "/css/**", "/images/**", "/js/**", "/h2-console/**", "/profile").permitAll()
.antMatchers("/api/vi/**").hasRole(Role.USER.name())
.anyRequest().authenticated())
.logout(logout -> logout.logoutSuccessUrl("/"))
.oauth2Login(oauth2Login -> oauth2Login.userInfoEndpoint().userService(customOAuth2UserService));
return http.build();
}
}
* 개인 메모
<오류나서 순서 어떻게 돌아가는지 디버깅으로 정리...>
클라 localhost:8080 요청
IndexController ("/") - user = null
그냥 index.mustache 보여줌 'Google Login' 클릭
- "oauth/authorization/google" 주소 호출(Google 쪽)
- 로긴 & 개인정보제공 동의
- return 해줌.
CustomOAuth2UserService.loadUser 발동
delegate.loadUser 가 구글에서 attribute 가져옴.
'스프링부트와 AWS로 혼자 구현하는 웹서비스' 카테고리의 다른 글
17. 구글 API OAuth - 프로젝트 프론트 설정 & 테스트 (0) | 2024.10.20 |
---|---|
15. 구글 API - Oauth2 생성 (0) | 2024.10.20 |
14. '게시글 삭제' 기능 추가 (0) | 2024.10.20 |
13. '게시글 수정' 기능 추가 (0) | 2024.10.20 |
12. '게시글 조회' 기능 추가 (0) | 2024.10.20 |