외부 로그인은 기존 프로젝트에 적용해본적이 없어서 OAuth2에 대해 알아보고 정리해보았다.
이제 네이버 로그인 서비스를 적용할 Spring boot 애플리케이션을 작성해보자.
Dependency
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'mysql:mysql-connector-java'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
생성 완료했다면, 메인 애플리케이션 클래스에 @EnableJpaRepositories
추가
# registration
spring.security.oauth2.client.registration.naver.client-id=<client id>
spring.security.oauth2.client.registration.naver.client-secret=<client secret>
spring.security.oauth2.client.registration.naver.redirect-uri={baseUrl}/{action}/oauth2/code/{registrationId}
spring.security.oauth2.client.registration.naver.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.naver.scope=nickname,email,gender,age,profile_image
spring.security.oauth2.client.registration.naver.client-name=Naver
# provider
spring.security.oauth2.client.provider.naver.authorization-uri=https://nid.naver.com/oauth2.0/authorize
spring.security.oauth2.client.provider.naver.token-uri=https://nid.naver.com/oauth2.0/token
spring.security.oauth2.client.provider.naver.user-info-uri=https://openapi.naver.com/v1/nid/me
spring.security.oauth2.client.provider.naver.user-name-attribute=response
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
spring.jpa.database=mysql
spring.jpa.hibernate.ddl-auto=update
spring.jpa.generate-ddl=true
#spring.jpa.show-sql=true
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect
spring.jpa.hibernate.use-new-id-generator-mappings=true
spring.jackson.serialization.fail-on-empty-beans=false
# database
spring.profiles.include=db
spring.datasource.url=jdbc:mysql://localhost:3306/oauthTest?characterEncoding=UTF-8
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=1234
spring.security.oauth2.client.registration.naver.scope에는 네이버에서 가져올 정보를 추가하면 되는것 같다.
기존에 사용했던 코드를 재사용하려 했으나 WebSecurityConfigurerAdapter
는 deprecated되었다고 한다. 따라서 새롭게 권장되는 방식인 filterchain을 사용하여 작성해보았다.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SpringSecurityConfig {
private final UserService userService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/", "/css/**", "/images/**", "/js/**", "/profile").permitAll()
.antMatchers("/api/**").hasRole(Role.USER.name())
.anyRequest().authenticated().and()
.logout().logoutSuccessUrl("/").and()
.oauth2Login()
.userInfoEndpoint()
.userService(userService);
return http.build();
}
}
antMatchers("/", "/css/**", "/images/**", "/js/**", "/profile").permitAll(): 전체에서 열람 가능
antMatchers("/api/**").hasRole(Role.USER.name()): USER 권한을 가진 사람만 접근 가능
User Entity
@Builder
@AllArgsConstructor
@Getter @Entity
public class User {
public User() {}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String nickname;
@Column(nullable = false)
private String email;
@Column(name = "profile_picture")
private String picture;
@Column(nullable = false)
private String gender;
@Column(nullable = false)
private String age; // 연령대
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role;
public User update(String name, String picture) {
this.name = name;
this.picture = picture;
return this;
}
public String getRoleKey() {
return this.role.getKey();
}
}
다른 정보도 추가하고 싶다면 아래 참조 -> 개발 가이드
나는 헷갈리지 않기 위해 response에 담겨있는 이름과 똑같이 작성해서 사용했다.
@Getter
@RequiredArgsConstructor
public enum Role {
GUEST("ROLE_GUEST", "손님"),
USER("ROLE_USER", "사용자");
private final String key;
private final String value;
}
‼️ 주의할 점
@Getter
public class SessionUser implements Serializable {
SessionUser() {}
public SessionUser(User user) {
this.name = user.getName();
this.email = user.getEmail();
this.picture = user.getPicture();
}
private String nickname;
private String email;
private String picture;
private String gender;
private String age;
}
@Service
@RequiredArgsConstructor
public class UserService 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();
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails()
.getUserInfoEndpoint().getUserNameAttributeName();
// 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.getNickname(), attributes.getPicture()))
.orElse(attributes.toEntity());
return userRepository.save(user);
}
}
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
JPA에서 기본적으로 제공해주는 CRUD 메서드 말고 이메일로 사용자 객체를 찾는 메서드를 사용하기 때문에 작성해 주었다.
@RequiredArgsConstructor
@Controller
public class UserController {
private final HttpSession httpSession;
@GetMapping("/")
public String index(Model model) {
SessionUser user = (SessionUser) httpSession.getAttribute("user");
if (user != null) {
model.addAttribute("userName", user.getName());
}
return "index";
}
}
@Getter
@Builder
@RequiredArgsConstructor
public class OAuthAttributes {
private final Map<String, Object> attributes;
private final String nameAttributeKey;
private final String name;
private final String email;
private final String picture;
private final String gender;
private final String age;
public static OAuthAttributes of(String registrationId, Map<String, Object> attributes) {
if("naver".equals(registrationId)) {
return ofNaver("id", attributes);
}
else {
return ofKakao("id", attributes);
}
}
private static OAuthAttributes ofNaver(String userNameAttributeName, Map<String, Object> attributes) {
Map<String, Object> response = (Map<String, Object>) attributes.get("response");
return OAuthAttributes.builder()
.name((String) response.get("nickname"))
.email((String) response.get("email"))
.picture((String) response.get("profile_image"))
.gender((String) response.get("gender"))
.age((String) response.get("age"))
.attributes(response)
.nameAttributeKey(userNameAttributeName)
.build();
}
public User toEntity() {
return User.builder()
.name(name)
.email(email)
.picture(picture)
.gender(gender)
.age(age)
.role(Role.GUEST)
.build();
}
}
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Main Page</title>
</head>
<body>
<div sec:authorize="isAuthenticated()">
Logged in as: <span id="user" th:text="${username}">None</span>
<a href="/logout" class="btn btn-info active" role="button">Logout</a>
</div>
<a sec:authorize="isAnonymous()" href="/oauth2/authorization/naver" class="btn btn-secondary active" role="button">Naver Login</a>
</body>
그다음 기본 정보 제공 동의 후
스프링 부트와 AWS로 혼자 구현하는 웹 서비스 - 이동욱
이런 글 적어주셔서 감사합니다. 도움이 됐어요