반응형
OAuth2 로그인
- 소셜 로그인 사용을 통하여 서비스 개발 집중
- 기존 1.5 버전에서 2.0 버전으로 바뀌면서 enum 대체
- CommonOAuth2Provider enum 기능
- 구글, 깃허브, 페이스북 옥타등 기본 설정값 제공 - 다른 소셜 로그인은 추가 필요
구글 로그인
프로젝트 생성
- 프로젝트 생성 - API 및 서비스 - 사용자 인증 정보 - 사용자 인증 정보 만들기 - OAuth 클라이언트 ID - 동의 화면 구성
OAuth 클라이언트 ID 만들기
- 클라이언트 ID와 보안 비밀번호가 발급된다.
- 리다리렉트 URI : {도메인}/login/oauth2/code/google 로 등록하자 위에 사진 오타
properties 등록
spring.security.oauth2.client.registration.google.client-id=
spring.security.oauth2.client.registration.google.client-secret=
spring.security.oauth2.client.registration.google.scope = profile, email
- 발급 받은 id와 비밀번호를 등록한다.
- 새로운 properties에 저장하고 싶다면 기존 프로퍼티에 spring.profiles.include=oauth를 추가해준다. (application-oauth.properties) 추가
git rm -r --cached .
git add .
git commit -m "fixed untracked files"
- 노출시 위험한 정보는 gitignore에서 관리하자. 만약 커밋목록에 나올경우 다음과 같은 명령어로 캐시를 삭제하자.
사용자 엔티티 생성
package com.crawler.domain.user;
import com.crawler.domain.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();
}
}
package com.crawler.domain.user;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum Role {
GUEST("ROLE_GUEST", "손님"),
USER("ROLE_USER", "일반 사용자");
private final String key;
private final String title;
}
package com.crawler.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);
}
- User 엔티티 관련 코드 작성
시큐리티 설정
implementation('org.springframework.boot:spring-boot-starter-oauth2-client')
- gradle에 스프링 시큐리티 의존성 추가
package com.crawler.config.auth;
import com.crawler.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
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final CustomOAuth2UserService customOauth2UserService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().headers().frameOptions().disable()
.and()
.authorizeRequests()
.antMatchers("/", "/css/**", "/images/**",
"/js/**", "/h2-console/**").permitAll()
.antMatchers("/api/v1/**").hasRole(Role.USER.name())
.anyRequest().authenticated() // 나머지 모든 URL 로그인한 사용자만 사용
.and()
.logout()
.logoutSuccessUrl("/") // 로그아웃시 시작 주소
.and()
.oauth2Login()
.userInfoEndpoint()
.userService(customOauth2UserService);
}
}
- @EnableWebSecurity : 스프링 시큐리티 설정을 활성화
- csrf() : h2-console 사용을 위해 disable
- .authorizeRequest : URI 별 권한 관리 설정 - 이후 andMatcher 사용 가능 - /api/v1 은 USER 권한만 사용 가능
- userService : 소셜 로그인 성공 시 후속 조치를 진행할 인터페이스 구현체 등록
package com.crawler.config.auth;
import com.crawler.config.auth.dto.OAuthAttributes;
import com.crawler.config.auth.dto.SessionUser;
import com.crawler.domain.user.User;
import com.crawler.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
class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final UserRepository userRepository;
private final HttpSession httpSession;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2UserService delegate = new DefaultOAuth2UserService();
OAuth2User oAuth2User = delegate.loadUser(userRequest);
String registrationId = userRequest.getClientRegistration().getRegistrationId(); // 현재 로그인 진행중인 서비스 구분 (여러 소셜 로그인시 구별)
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails()
.getUserInfoEndpoint().getUserNameAttributeName(); // 구글의 기본코드인 sub
//OAuthAttributes를 통해 가져온 OAuth2User의 attribute를 담은 클래스
OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());
User user = saveOrUpdate(attributes);
httpSession.setAttribute("user", new SessionUser(user)); // 세션에 사용자 정보를 저장하기위한 DTO 클래스
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);
}
}
- 로그인 이후 가져온 사용자의 정보를 기반으로 가입, 세션 저장등 기능 수행
- SessionUser 라는 새로운 DTO를 만드는 이유는 직렬화를 엔티티에 구현하면 추후 유지보수가 어려워지기 때문
package com.crawler.config.auth.dto;
import com.crawler.domain.user.Role;
import com.crawler.domain.user.User;
import lombok.Builder;
import lombok.Getter;
import java.util.Map;
@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;
}
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();
}
}
- of : OAuth2User에 반환하는 사용자 정보는 Map 형태이기 때문에 값을 하나씩 반환
- toEntity() : 가입할 때 기본권한을 GUEST로 주기 위해서 사용 (처음 가입 시점)
<div class="col-md-6">
<a href="/posts/save" role="button" class="btn btn-primary">글등록</a>
{{#userName}}
Login as: <span id="user">{{userName}}</span>
<a href="/logout" class="btn btn-info active "role="button">Logout</a>
{{/userName}}
{{^userName}}
<a href="/oauth2/authorization/google" class="btn btn-success active"
role="button">Google Login</a>
{{/userName}}
</div>
- 로그인 기능 추가 - mustache 템플릿 엔진 사용
package com.crawler.web;
import com.crawler.config.auth.LoginUser;
import com.crawler.config.auth.dto.SessionUser;
import com.crawler.service.posts.PostsService;
import com.crawler.web.dto.PostsResponseDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import javax.servlet.http.HttpSession;
@RequiredArgsConstructor
@Controller
public class IndexController {
private final PostsService postsService;
private final HttpSession httpSession;
@GetMapping("/")
public String index(Model model) {
model.addAttribute("posts", postsService.findAllDesc());
SessionUser user = (SessionUser) httpSession.getAttribute("user");
if (user != null) {
model.addAttribute("userName", user.getName());
}
return "index";
}
...
}
- Controller 생성 - 추후 SessionUser는 어노테이션으로 개선
- 로그인 성공 화면
참고 - 어노테이션 기반 개선
- SessionUser는 세션 값이 필요할 때마다 직접 세션에서 값을 가져와야 한다.
- 이 부분을 메서드 인자로 세션 값을 바로 받아올 수 있도록 변경
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser {
}
- 커스텀 어노테이션 생성
package com.crawler.config.auth;
import com.crawler.config.auth.dto.SessionUser;
import lombok.RequiredArgsConstructor;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import javax.servlet.http.HttpSession;
@RequiredArgsConstructor
@Component
public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver {
private final HttpSession httpSession;
@Override
public boolean supportsParameter(MethodParameter parameter) {
boolean isLoginUserAnnotation = parameter
.getParameterAnnotation(LoginUser.class) != null;
boolean isUserClass = SessionUser.class.equals(parameter.getParameterType());
return isLoginUserAnnotation && isUserClass;
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
return httpSession.getAttribute("user");
}
}
- @LoginUser를 사용하기 위한 환경 구성
package com.crawler.config;
import com.crawler.config.auth.LoginUserArgumentResolver;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@RequiredArgsConstructor
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final LoginUserArgumentResolver loginUserArgumentResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(loginUserArgumentResolver);
}
}
- LoginUserArgumentResolver가 스프링에서 인식될 수 있도록 WebMvcConfigurer에 추가
@GetMapping("/")
public String index(Model model, @LoginUser SessionUser user) {
model.addAttribute("posts", postsService.findAllDesc());
if (user != null) {
model.addAttribute("userName", user.getName());
}
return "index";
}
- Controller를 수정하여 이제 어느 컨트롤러던지 어노테이션을 통해 세션 정보를 가져올 수 있게 됬다.
세션 저장소로 DB 사용
- 어플리케이션을 재실행 시 로그인이 풀리는 것은 세션이 내장 톰캣의 메모리에 저장되기 때문이다.
- 즉, 배포할 때마다 톰캣이 재시작 되므로 로그아웃 되는 것이다.
- WAS간 공용 세션을 사용할 수 있는 가장 쉬운방법은 DB이다.
- Redis, Memcached와 같은 메모리 DB를 세션저장소로 사용하는 방법도 있다. (주로 B2C 서비스에서 사용 - 외부 메모리, 유료)
implementation('org.springframework.session:spring-session-jdbc')
- gradle 의존성 추가
spring.session.store-type=jdbc
- application.properties에 세션 저장소를 jdbc를 사용하도록 설정
h2-console에 들어가보면 spring_session 테이블이 생성된 것을 볼 수 있다.
WAS 배포전에는 재시작하면 세션이 풀린다. 이후, AWS를 이용한 RDS 서비스로 배포시 세션이 풀리지 않는다.
네이버 로그인
- https://developers.naver.com/apps 로 이동하여 application 등록
- 어플리케이션 등록 후 ID, 비밀번호를 복사하여 properties에 수동 등록 한다. (네이버는 스프링에 공식 지원 X)
spring.security.oauth2.client.registration.naver.client-id=
spring.security.oauth2.client.registration.naver.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=name, email, profile_image
spring.security.oauth2.client.registration.naver.client-name=Naver
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
- application-auth.properties에 네이버 수동 등록 (OAuth2Provider 미제공)
- 네이버 회원 조회 시 이름을 반드시 response로 해야 한다. (JSON 반환)
시큐리티 설정 등록
- 왼쪽과 같이 OAuthAttributes 클래스에 네이버 판단해주는 코드와 생성자를 추가해준다.
- 결과 화면
본 포스팅은 프리렉-이동욱님의 '스프링 부트와 AWS로 혼자 구현하는 웹 서비스' 책을 참고하였습니다.
반응형