Goal
- 스프링 시큐리티를 이용한 구글/네이버 로그인 연동
- 세션 저장소로 톰캣 / 데이터베이스 / 메모리 DB가 있으며 이 중 데이터베이스를 사용하는 이유
- ArgumentResolver를 이용하면 어노테이션으로 로그인 세션 정보를 가져올 수 있다
- 스프링 시큐리티 적용 시 기존 테스트 코드에서 문제 해결 방법
스프링 시큐리티란?
막강한 인증(Authentication)과 인가(Authorization) 기능을 가진 프레임워크로 스프링 기반의 애플리케이션에서의 보안 표준
소셜 로그인을 사용하는 이유
- OAuth 로그인 구현시 구글/네이버 등을 통해 아래의 기능을 맡기고, 서비스 개발에 집중할 수 있다
- 로그인 시 보안
- 회원가입 시 이메일 혹은 전화번호 인증
- 비밀번호 찾기
- 비밀번호 변경
- 회원정보 변경
예제1. 구글 로그인 연동
1.1 구글 서비스 등록
1.1.1 https://console.cloud.google.com/welcome?project=modular-analog-380904&pli=1
1.1.2 새 프로젝트 선택 후 생성

이모티콘 위치 참고 ! 셀렉트 박스를 선택하면 위와 같은 팝업이 나오는데, 상단 우측의 NEW PROJECT 선택
1.1.3 프로젝트 이름 설정

이미 실습하면서 freelec-springboot2-webservice는 만들어뒀기 때문에, 참고용으로 my project로 생성
1.1.4 대쉬보드 진입 후 Explore and enable APIs 선택


1.1.5 Credentials의 CREATE CRETENTIALS 클릭 후 OAuth client ID 클릭!!


1.1.6 Application type 선택 후 Name 및 redirect URIs 설정



승인된 리디렉션 URI
- 서비스에서 파라미터로 인증 정보를 주었을 때 인증을 성고하면 구글에서 리다이렉트할 URL
- 스프링 부트 2버전 시큐리티에서는 기본적으로 {도메인}/login/oauth2/code/{소셜서비스코드}로 리다이렉트 URL지원
- 사용자가 별도로 리다이렉트 URL을 지원하는 Controller를 만들 필요가 없다. 시큐리티가 이미 구현
- 현재는 개발 단계이므로 http://localhost:8080/login/oauth2/code/google로만 등록
- aws 서버에 배포하게 되면 localhost 외에 추가로 주소를 등록해야함
1.1.7 목록 생성 됨 확인 후 프로젝트 클릭

1.1.8 클라이언트 ID 및 보안 비밀 코드 정보 확인

1.2 프로젝트 설정
1.2.1 application-oauth 등록
src/main/resources/ 디렉토리에 application-oauth.properties 파일을 생성 하고 아래 코드 작성
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
1.2.2 application.properties에 코드 추가
spring.profiles.include=oauth
1.2.3 git ignore에 코드 추가
application-oauth.properties
scope=profile,email
- 기본값이 openid,profile,email
- 강제로 profile,email을 등록한 이유는 openid라는 scope가 있으면 Open Id Provider로 인식
- 이렇게 되면 OpenProvider인 서비스(구글)와 그렇지 않은 서비스(네이버/카카오)로 나눠 각 OAuth2Service 생성
- 하나의 OAuth2Service로 사용하기 위해 openid scope은 빼고 등록
1.3 프로젝트 구현
1.3.1 사용자정보 담당할 도메인 User 클래스 생성
package com.jojoldu.book.springboot.domain.user;
@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();
}
}
@Enumerated(EnumType.STRING)
- JPA로 데이터베이스를 저장할 때 Enum 값을 어떤 형태로 저장할 지 결정한다
- 기본적으로 int로 된 숫자가 저장
- 숫자로 저장되면 데이터베이스로 확인할 때 그 값이 무슨 코드를 의미하는지 알 수 없다
- 그래서 문자열 EnumType.STRING으로 저장될 수 있도록 선언
1.3.2 각 사용자의 권한을 관리할 Enum 클래스 Role 생성
package com.jojoldu.book.springboot.domain.user;
@Getter
@RequiredArgsConstructor
public enum Role {
GUEST("ROLE_GUEST", "손님"),
USER("ROLE_USER", "일반 사용자");
private final String key;
private final String title;
}
1.3.3 User의 CRUD를 책임질 UserRepository 생성
package com.jojoldu.book.springboot.domain.user;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
findByEmail
- 소셜 로그인으로 반환되는 값 중 email을 통해 이미 생성된 사용자인지 처음 가입하는 사용자인지 판단하는 메소드
1.3.4 스프링 시큐리티 설정 build.gradle에 코드 추가
// ouath2
implementation('org.springframework.boot:spring-boot-starter-oauth2-client')
implementation('org.springframework.boot:spring-boot-starter-security')
1.3.5 config.auth 패키지 생성 후 SecurityConfig 클래스 (OAuth라이브러리를 이요한 소셜로그인 설정 코드) 추가
package com.jojoldu.book.springboot.config.auth;
@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()
.and()
.logout()
.logoutSuccessUrl("/")
.and()
.oauth2Login()
.userInfoEndpoint()
.userService(customOAuth2UserService);
}
}
- @EnableWebSecurity
- Spring Security 설정들을 활성화
- csrf().disable().headers().frameOptions().disable()
- h2-console 화면을 사용하기 위한 해당 옵션 disable
- authorizeRequests
- URL별 권한 관리를 설정하는 옵션의 시작점
- authorizeRequest가 선언되어야만 antMatchers옵션 사용 가능
- antMatchers
- 권한 관리 대상을 지정하는 옵션
- URL, HTTP 메소드별로 관리 가능
- "/"등 지정된 URL들은 permitAll() 옵션을 통해 전체 열람 권한을 주었음
- "/api/v1/**" 주소를 가진 API는 USER권한을 가진 사람만 가능하도록 함
- anyRequest
- 설정된 값들 이외 나머지 URL들을 나타낸다
- 여기서는 authenticated()을 추가하여 나머지 URL들은 모두 인증된 사용자들에게만 허용
- logout().logoutSuccessUrl("/")
- 로그아웃 기능에 대한 여러 설정의 진입점
- 로그아웃 성공 시 /주소로 이동
- oauth2Login
- OAuth2로그인 기능에 대한 여러 설정의 진입점
- userInfoEndPoint
- OAuth2로그인 성공 이후 사용자 정보를 가져올 때의 설정담당
- userService
- 소셜 로그인 성공시 후속 조치를 진행할 UserService 인터페이스의 구현체를 등록
- 리소스 서버(즉, 소셜 서비스들)에서 사용자 정보를 가져온 상태에서 추가로 진행하고자 하는 기능 명시 가능
1.3.6 CustomOAuth2UserService 클래스 추가
package com.jojoldu.book.springboot.config.auth;
@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();
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);
}
}
- registerationId
- 현재 로그인 진행 중인 서비스를 구분하는 코드
- 지금은 구글만 사용하는 불필요한 값이지만 이후 네이버 로그인 연동 시에 네이버 로그인인지 구글 로그인인지 구분하기 위해 사용
- userNameAttributeName
- OAuth2 로그인 진행 시 키가 되는 필드값을 의미하며 Primary Key와 같은 의미
- 구글의 경우 기본적으로 코드를 지원하지만, 네이버 카카오 등은 지원하지 않는다. 구글 기본 코드는 "sub"
- 이후 네이버 로그인과 구글 로그인을 동시 지원할 때 사용된다
- OAuthAttributes
- OAuth2Service를 통해 가져온 OAuth2User의 attribute를 담은 클래스
- 이후 네이버등 다른 소셜 로그인도 이 클래스를 사용
- SessionUser
- 세션에 사용자 정보를 저장하기 위한 Dto 클래스
1.3.7 config.auth.dto 패키지 생성 후 OAuthAttributes 클래스 생성
package com.jojoldu.book.springboot.config.auth.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;
}
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()
- User 엔티티를 생성
- OAuthAttributes에서 엔티티를 생성하는 시점은 처음 가입할 때
- 가입할 때의 기본 권한을 GUEST로 주기 위해서 role 빌더값에는 Role.GUEST를 사용
- OAuthAttributes 클래스 생성이 끝났으면 같은 패키지에 SessionUser 클래스를 생성
1.3.8 SessionUser 클래스 생성
package com.jojoldu.book.springboot.config.auth.dto;
@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();
}
}
- SessionUser에는 인증된 사용자 정보만 필요
1.3 로그인 테스트
1.3.1 로그인 버튼 추가
<div class="row">
<div class="col-md-6">
<a href="/posts/save" role="button" class="btn btn-primary">글 등록</a>
{{#userName}}
Logged in 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>
<a href="/oauth2/authorization/naver" class="btn btn-secondary active" role="button">Naver Login</a>
{{/userName}}
</div>
</div>
1.3.2 IndexController에 userName을 model에 저장하는 코드 추가
@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";
}
}
1.3.3 서비스 띄운 후 구글 로그인


잠깐 ! 게시글 등록은 권한이 없기 때문에 실패한다.

1.3.4 권한 USER로 변경



1.3.5 게시글 등록

2. 어노테이션 기반으로 개선하기
2.1 config.auth에 LoginUser 인터페이스 생성
package com.jojoldu.book.springboot.config.auth;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser {
}
@Target(Element.PARAMETER)
- 이 어노테이션이 생성될 수 있는 위치를 생성
- PARAMETER로 지정했으니 메소드의 파라미터로 선언된 객체에서만 사용할 수 있음
- 이 외에도 클래스 선언문에 쓸 수 있는 TYPE등이 있음
2.2 LoginUserArgumentResolver 클래스 생성
package com.jojoldu.book.springboot.config.auth;
@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");
}
}
- supportsParameter()
- 컨트롤러 메서드의 특정 파라미터를 지원하는지 판단
- 파라미터에 @LoginUser 어노테이션이 붙어 있고, 파라미터 클래스 타입이 SessionUser.class인 경우 true반환
- resolveArgument()
- 파라미터에 전달할 객체 생성
- 여기서는 세션에서 객체를 가져옴
2.3 LoginUserArgumentResolver가 스프링에서 인식될 수 있도록 WebMvcConfigurer에 추가
@RequiredArgsConstructor
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final LoginUserArgumentResolver loginUserArgumentResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(loginUserArgumentResolver);
}
}
2.4 IndexController 코드에서 반복되는 부분을 @LoginUser 로 개선
@GetMapping("/")
public String index(Model model, @LoginUser SessionUser user){
model.addAttribute("posts", postsService.findAllDesc());
if(user != null){
model.addAttribute("userName", user.getName());
}
return "index";
}
3. 세션 저장소로 데이터베이스 사용하기
3.1 build.gradle에 spring-session-jdbc 의존성 추가
// jdbc
implementation('org.springframework.session:spring-session-jdbc')
3.2 application.properties에 코드 추가
spring.session.store-type=jdbc
예제4. 네이버 로그인 연동
4.1 네이버 서비스 등록
4.1.1 https://developers.naver.com/apps/#/register?api=nvlogin
4.1.2 네이버API 등록


4.1.3 내 어플리케이션의 개요에서 클라이언트 ID, Secret 확인

4.1.4 해당 키값을 application-oauth.properties에 등록
#registration
spring.security.oauth2.client.registration.naver.client-id=클라이언트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
#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
=
4.1.5 스프링 시큐리티 설정 등록- OAuthAttributes에 네이버인지 판단하는 코드와 네이버 생성자 추가
public static OAuthAttributes of(String registrationId, String userNameAttributeName, Map<String, Object> attributes){
if("naver".equals(registrationId)){
return ofNaver("id", attributes);
}
return ofGoogle(userNameAttributeName, 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("name"))
.email((String) response.get("email"))
.picture((String) response.get("profile_image"))
.attributes(response)
.nameAttributeKey(userNameAttributeName)
.build();
}
4.1.6 서비스 띄운 후 네이버 로그인


짜잔! 실패 !ㅋㅋㅋㅋㅋ크롬에서 네이버 다른 아이디로 자동로그인 되있었는데
로그인시도하니까 이렇게 메세지 뜸! 굿! 보안 베리 굿 인증~~~~~~!


결과 로그인 성공 그 잡채
'책 > 스프링부트와 AWS로 혼자 구현하는 웹서비스' 카테고리의 다른 글
[스프링 부트와 AWS로 혼자 구현하는 웹 서비스 4장] - 머스테치로 화면 구성하기 (0) | 2023.03.16 |
---|---|
[스프링 부트와 AWS로 혼자 구현하는 웹 서비스 3장] - 스프링 부트에서 JPA로 데이터베이스 다뤄보자 (0) | 2023.03.15 |
[스프링 부트와 AWS로 혼자 구현하는 웹 서비스 2장] - 스프링 부트에서 테스트 코드를 작성하자 (0) | 2023.03.13 |
Goal
- 스프링 시큐리티를 이용한 구글/네이버 로그인 연동
- 세션 저장소로 톰캣 / 데이터베이스 / 메모리 DB가 있으며 이 중 데이터베이스를 사용하는 이유
- ArgumentResolver를 이용하면 어노테이션으로 로그인 세션 정보를 가져올 수 있다
- 스프링 시큐리티 적용 시 기존 테스트 코드에서 문제 해결 방법
스프링 시큐리티란?
막강한 인증(Authentication)과 인가(Authorization) 기능을 가진 프레임워크로 스프링 기반의 애플리케이션에서의 보안 표준
소셜 로그인을 사용하는 이유
- OAuth 로그인 구현시 구글/네이버 등을 통해 아래의 기능을 맡기고, 서비스 개발에 집중할 수 있다
- 로그인 시 보안
- 회원가입 시 이메일 혹은 전화번호 인증
- 비밀번호 찾기
- 비밀번호 변경
- 회원정보 변경
예제1. 구글 로그인 연동
1.1 구글 서비스 등록
1.1.1 https://console.cloud.google.com/welcome?project=modular-analog-380904&pli=1
1.1.2 새 프로젝트 선택 후 생성

이모티콘 위치 참고 ! 셀렉트 박스를 선택하면 위와 같은 팝업이 나오는데, 상단 우측의 NEW PROJECT 선택
1.1.3 프로젝트 이름 설정

이미 실습하면서 freelec-springboot2-webservice는 만들어뒀기 때문에, 참고용으로 my project로 생성
1.1.4 대쉬보드 진입 후 Explore and enable APIs 선택


1.1.5 Credentials의 CREATE CRETENTIALS 클릭 후 OAuth client ID 클릭!!


1.1.6 Application type 선택 후 Name 및 redirect URIs 설정



승인된 리디렉션 URI
- 서비스에서 파라미터로 인증 정보를 주었을 때 인증을 성고하면 구글에서 리다이렉트할 URL
- 스프링 부트 2버전 시큐리티에서는 기본적으로 {도메인}/login/oauth2/code/{소셜서비스코드}로 리다이렉트 URL지원
- 사용자가 별도로 리다이렉트 URL을 지원하는 Controller를 만들 필요가 없다. 시큐리티가 이미 구현
- 현재는 개발 단계이므로 http://localhost:8080/login/oauth2/code/google로만 등록
- aws 서버에 배포하게 되면 localhost 외에 추가로 주소를 등록해야함
1.1.7 목록 생성 됨 확인 후 프로젝트 클릭

1.1.8 클라이언트 ID 및 보안 비밀 코드 정보 확인

1.2 프로젝트 설정
1.2.1 application-oauth 등록
src/main/resources/ 디렉토리에 application-oauth.properties 파일을 생성 하고 아래 코드 작성
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
1.2.2 application.properties에 코드 추가
spring.profiles.include=oauth
1.2.3 git ignore에 코드 추가
application-oauth.properties
scope=profile,email
- 기본값이 openid,profile,email
- 강제로 profile,email을 등록한 이유는 openid라는 scope가 있으면 Open Id Provider로 인식
- 이렇게 되면 OpenProvider인 서비스(구글)와 그렇지 않은 서비스(네이버/카카오)로 나눠 각 OAuth2Service 생성
- 하나의 OAuth2Service로 사용하기 위해 openid scope은 빼고 등록
1.3 프로젝트 구현
1.3.1 사용자정보 담당할 도메인 User 클래스 생성
package com.jojoldu.book.springboot.domain.user;
@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();
}
}
@Enumerated(EnumType.STRING)
- JPA로 데이터베이스를 저장할 때 Enum 값을 어떤 형태로 저장할 지 결정한다
- 기본적으로 int로 된 숫자가 저장
- 숫자로 저장되면 데이터베이스로 확인할 때 그 값이 무슨 코드를 의미하는지 알 수 없다
- 그래서 문자열 EnumType.STRING으로 저장될 수 있도록 선언
1.3.2 각 사용자의 권한을 관리할 Enum 클래스 Role 생성
package com.jojoldu.book.springboot.domain.user;
@Getter
@RequiredArgsConstructor
public enum Role {
GUEST("ROLE_GUEST", "손님"),
USER("ROLE_USER", "일반 사용자");
private final String key;
private final String title;
}
1.3.3 User의 CRUD를 책임질 UserRepository 생성
package com.jojoldu.book.springboot.domain.user;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
findByEmail
- 소셜 로그인으로 반환되는 값 중 email을 통해 이미 생성된 사용자인지 처음 가입하는 사용자인지 판단하는 메소드
1.3.4 스프링 시큐리티 설정 build.gradle에 코드 추가
// ouath2
implementation('org.springframework.boot:spring-boot-starter-oauth2-client')
implementation('org.springframework.boot:spring-boot-starter-security')
1.3.5 config.auth 패키지 생성 후 SecurityConfig 클래스 (OAuth라이브러리를 이요한 소셜로그인 설정 코드) 추가
package com.jojoldu.book.springboot.config.auth;
@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()
.and()
.logout()
.logoutSuccessUrl("/")
.and()
.oauth2Login()
.userInfoEndpoint()
.userService(customOAuth2UserService);
}
}
- @EnableWebSecurity
- Spring Security 설정들을 활성화
- csrf().disable().headers().frameOptions().disable()
- h2-console 화면을 사용하기 위한 해당 옵션 disable
- authorizeRequests
- URL별 권한 관리를 설정하는 옵션의 시작점
- authorizeRequest가 선언되어야만 antMatchers옵션 사용 가능
- antMatchers
- 권한 관리 대상을 지정하는 옵션
- URL, HTTP 메소드별로 관리 가능
- "/"등 지정된 URL들은 permitAll() 옵션을 통해 전체 열람 권한을 주었음
- "/api/v1/**" 주소를 가진 API는 USER권한을 가진 사람만 가능하도록 함
- anyRequest
- 설정된 값들 이외 나머지 URL들을 나타낸다
- 여기서는 authenticated()을 추가하여 나머지 URL들은 모두 인증된 사용자들에게만 허용
- logout().logoutSuccessUrl("/")
- 로그아웃 기능에 대한 여러 설정의 진입점
- 로그아웃 성공 시 /주소로 이동
- oauth2Login
- OAuth2로그인 기능에 대한 여러 설정의 진입점
- userInfoEndPoint
- OAuth2로그인 성공 이후 사용자 정보를 가져올 때의 설정담당
- userService
- 소셜 로그인 성공시 후속 조치를 진행할 UserService 인터페이스의 구현체를 등록
- 리소스 서버(즉, 소셜 서비스들)에서 사용자 정보를 가져온 상태에서 추가로 진행하고자 하는 기능 명시 가능
1.3.6 CustomOAuth2UserService 클래스 추가
package com.jojoldu.book.springboot.config.auth;
@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();
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);
}
}
- registerationId
- 현재 로그인 진행 중인 서비스를 구분하는 코드
- 지금은 구글만 사용하는 불필요한 값이지만 이후 네이버 로그인 연동 시에 네이버 로그인인지 구글 로그인인지 구분하기 위해 사용
- userNameAttributeName
- OAuth2 로그인 진행 시 키가 되는 필드값을 의미하며 Primary Key와 같은 의미
- 구글의 경우 기본적으로 코드를 지원하지만, 네이버 카카오 등은 지원하지 않는다. 구글 기본 코드는 "sub"
- 이후 네이버 로그인과 구글 로그인을 동시 지원할 때 사용된다
- OAuthAttributes
- OAuth2Service를 통해 가져온 OAuth2User의 attribute를 담은 클래스
- 이후 네이버등 다른 소셜 로그인도 이 클래스를 사용
- SessionUser
- 세션에 사용자 정보를 저장하기 위한 Dto 클래스
1.3.7 config.auth.dto 패키지 생성 후 OAuthAttributes 클래스 생성
package com.jojoldu.book.springboot.config.auth.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;
}
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()
- User 엔티티를 생성
- OAuthAttributes에서 엔티티를 생성하는 시점은 처음 가입할 때
- 가입할 때의 기본 권한을 GUEST로 주기 위해서 role 빌더값에는 Role.GUEST를 사용
- OAuthAttributes 클래스 생성이 끝났으면 같은 패키지에 SessionUser 클래스를 생성
1.3.8 SessionUser 클래스 생성
package com.jojoldu.book.springboot.config.auth.dto;
@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();
}
}
- SessionUser에는 인증된 사용자 정보만 필요
1.3 로그인 테스트
1.3.1 로그인 버튼 추가
<div class="row">
<div class="col-md-6">
<a href="/posts/save" role="button" class="btn btn-primary">글 등록</a>
{{#userName}}
Logged in 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>
<a href="/oauth2/authorization/naver" class="btn btn-secondary active" role="button">Naver Login</a>
{{/userName}}
</div>
</div>
1.3.2 IndexController에 userName을 model에 저장하는 코드 추가
@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";
}
}
1.3.3 서비스 띄운 후 구글 로그인


잠깐 ! 게시글 등록은 권한이 없기 때문에 실패한다.

1.3.4 권한 USER로 변경



1.3.5 게시글 등록

2. 어노테이션 기반으로 개선하기
2.1 config.auth에 LoginUser 인터페이스 생성
package com.jojoldu.book.springboot.config.auth;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser {
}
@Target(Element.PARAMETER)
- 이 어노테이션이 생성될 수 있는 위치를 생성
- PARAMETER로 지정했으니 메소드의 파라미터로 선언된 객체에서만 사용할 수 있음
- 이 외에도 클래스 선언문에 쓸 수 있는 TYPE등이 있음
2.2 LoginUserArgumentResolver 클래스 생성
package com.jojoldu.book.springboot.config.auth;
@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");
}
}
- supportsParameter()
- 컨트롤러 메서드의 특정 파라미터를 지원하는지 판단
- 파라미터에 @LoginUser 어노테이션이 붙어 있고, 파라미터 클래스 타입이 SessionUser.class인 경우 true반환
- resolveArgument()
- 파라미터에 전달할 객체 생성
- 여기서는 세션에서 객체를 가져옴
2.3 LoginUserArgumentResolver가 스프링에서 인식될 수 있도록 WebMvcConfigurer에 추가
@RequiredArgsConstructor
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final LoginUserArgumentResolver loginUserArgumentResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(loginUserArgumentResolver);
}
}
2.4 IndexController 코드에서 반복되는 부분을 @LoginUser 로 개선
@GetMapping("/")
public String index(Model model, @LoginUser SessionUser user){
model.addAttribute("posts", postsService.findAllDesc());
if(user != null){
model.addAttribute("userName", user.getName());
}
return "index";
}
3. 세션 저장소로 데이터베이스 사용하기
3.1 build.gradle에 spring-session-jdbc 의존성 추가
// jdbc
implementation('org.springframework.session:spring-session-jdbc')
3.2 application.properties에 코드 추가
spring.session.store-type=jdbc
예제4. 네이버 로그인 연동
4.1 네이버 서비스 등록
4.1.1 https://developers.naver.com/apps/#/register?api=nvlogin
4.1.2 네이버API 등록


4.1.3 내 어플리케이션의 개요에서 클라이언트 ID, Secret 확인

4.1.4 해당 키값을 application-oauth.properties에 등록
#registration
spring.security.oauth2.client.registration.naver.client-id=클라이언트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
#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
=
4.1.5 스프링 시큐리티 설정 등록- OAuthAttributes에 네이버인지 판단하는 코드와 네이버 생성자 추가
public static OAuthAttributes of(String registrationId, String userNameAttributeName, Map<String, Object> attributes){
if("naver".equals(registrationId)){
return ofNaver("id", attributes);
}
return ofGoogle(userNameAttributeName, 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("name"))
.email((String) response.get("email"))
.picture((String) response.get("profile_image"))
.attributes(response)
.nameAttributeKey(userNameAttributeName)
.build();
}
4.1.6 서비스 띄운 후 네이버 로그인


짜잔! 실패 !ㅋㅋㅋㅋㅋ크롬에서 네이버 다른 아이디로 자동로그인 되있었는데
로그인시도하니까 이렇게 메세지 뜸! 굿! 보안 베리 굿 인증~~~~~~!


결과 로그인 성공 그 잡채
'책 > 스프링부트와 AWS로 혼자 구현하는 웹서비스' 카테고리의 다른 글
[스프링 부트와 AWS로 혼자 구현하는 웹 서비스 4장] - 머스테치로 화면 구성하기 (0) | 2023.03.16 |
---|---|
[스프링 부트와 AWS로 혼자 구현하는 웹 서비스 3장] - 스프링 부트에서 JPA로 데이터베이스 다뤄보자 (0) | 2023.03.15 |
[스프링 부트와 AWS로 혼자 구현하는 웹 서비스 2장] - 스프링 부트에서 테스트 코드를 작성하자 (0) | 2023.03.13 |