초기에는 오픈아이디 규격이 제각각이었으나
oauth2에서는 표준 규격에 맞게 로그인 API를 제공하고 있습니다.
기존 가입했던 플랫폼에서 인증을 대신하고 사용자의 정보를 가입하려는 플랫폼에 전달하는 방식입니다.
페이코 인증절차
X라는 서비스에서 가입 및 로그인을 간편로그인으로 했을 경우입니다.
X 서비스에서 인증 요청을 페이코에 하고 페이코는 사용자의 정보를 기반으로 인증 코드와 토큰을 발행합니다.
인증이 완료 되면 사용자는 기존에 페이코에 가입 했던 정보를 X 서비에서 그대로 사용할 수 있습니다.
1. 간편 로그인 플랫폼 생성(+google)
console.cloud.google.com/ 사이트에 접속합니다.
1-1. API 플랫폼 사이트에 접속
1-2. 인증 동의 작성
1-3. 사용자 인증 정보 만들기 - OAuth 클라이언트 ID 선택
1-4. 승인 원본과 리디렉션 URI 작성
테스트용이라 localhost를 사용하고 운영에서는 개별의 도메인을 사용합니다.
승인된 자바스크립트 원본 : http://localhost:9090
승인된 리디렉션 : http://localhost:9090/login/oauth2/code/google
1-5. 클라이언트 ID와 클라이언트 보안 비밀번호 확인
API를 사용하기 위한 클라이언트ID와 보안 비밀번호를 발급받고 노출되지 않도록 잘 보관합니다.
2. 의존성 추가
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
implementation ('org.springframework.boot:spring-boot-starter-oauth2-client')
3. 패키지 구성 및 테스트 소스
패키지 구성은 아래 사진과 같습니다.
3-1. application.yml
구글의 경우에는 기본적으로 redirectUri가 login/oauth2/code/google로 잡혀있으니 주석처리했습니다.
카카오, 네이버와 같은 플랫폼에서는 redirectUri를 지정해주어야 합니다.
security:
oauth2:
client:
registration:
google:
client-id: {발급 받은 클라이언트 ID}
client-secret: {발급 받은 보안 비밀번호}
# redirectUri: /oauth2/callback/
scope:
- email
- profile
3-2. WebSecurityConfig.java
@EnableWebSecurity : Spring Security 설정 활성
authorizeRequests : URL별 권한 관리를 설정을 시작
antMatchers : 권한 관리 대상 지정 URL, HTTP 메소드별로 관리 가능
anyRequest : 설정된 값들 이외 나머지 URL의 설정 지정
oauth2Login : OAuth 2 로그인 기능 설정을 시작
userInfoEndpoint : OAuth 2 로그인 성공 이후 사용자 정보를 가져올 시점의 설정
userService : 소셜 로그인 성공 시 후속 처리 시 UserService 인터페이스의 구현체를 등록
package com.otrodevym.spring.base.common.security.config;
import com.otrodevym.spring.base.common.security.oauth.base.model.BaseAuthRole;
import com.otrodevym.spring.base.common.security.oauth.base.service.BaseCustomOAuth2UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(
securedEnabled = true,
jsr250Enabled = true,
prePostEnabled = true
)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// security
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
// security
// 정적 자원에 대해서는 Security 설정을 적용하지 않음.
@Override
public void configure(WebSecurity web) {
web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());
}
// oauth base
private final BaseCustomOAuth2UserService baseCustomOAuth2UserService;
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean(BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
// security
// oauth
@Override
protected void configure(HttpSecurity http) throws Exception {
// --------------- oauth2 base ------------------
http.csrf().disable().headers().frameOptions().disable()
.and()
.authorizeRequests()
.antMatchers("/", "css/**", "/images/**", "/js/**", "/h2/**", "/h2-console/**").permitAll()
.antMatchers("/api/v1/**").hasRole(BaseAuthRole.USER.name())
.anyRequest().authenticated()
.and()
.logout().logoutSuccessUrl("/")
.and()
.oauth2Login().userInfoEndpoint().userService(baseCustomOAuth2UserService);
}
}
3-3. 테이블 생성
create table base_auth_user (
id int not null auto_increment,
name char(50) not null,
email char(50) not null,
picture varchar(3000) not null,
created_date timestamp,
modified_date timestamp,
role char(50) not null,
primary key (id)
);
4. Oauth2 테스트 소스
4-1. BaseAuthRole.java
스프링 시큐리티의 권한 코드에는 항상 ROLE이 앞에 있어야합니다.
package com.otrodevym.spring.base.common.security.oauth.base.model;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum BaseAuthRole {
GUEST("ROLE_GUEST", "손님"),
USER("ROLE_USER", "일반 사용자");
private final String key;
private final String title;
}
4-2. BaseTimeEntity.java
package com.otrodevym.spring.base.common.security.oauth.base.model;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class) // 감시 엔티티 등록
public class BaseTimeEntity {
@CreatedDate
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime modifiedDate;
}
4-3. BaseAuthUser.java
package com.otrodevym.spring.base.common.security.oauth.base.model;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Getter
@NoArgsConstructor
@Entity
public class BaseAuthUser 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 BaseAuthRole role;
@Builder
public BaseAuthUser(String name, String email, String picture, BaseAuthRole role) {
this.name = name;
this.email = email;
this.picture = picture;
this.role = role;
}
public BaseAuthUser update(String name, String picture) {
this.name = name;
this.picture = picture;
return this;
}
public String getRoleKey() {
return this.role.getKey();
}
}
4-4. SessionUser.java
package com.otrodevym.spring.base.common.security.oauth.base.dto;
import com.otrodevym.spring.base.common.security.oauth.base.model.BaseAuthUser;
import lombok.Getter;
import java.io.Serializable;
@Getter
public class SessionUser implements Serializable {
private String name;
private String email;
private String picture;
public SessionUser(BaseAuthUser authUser) {
this.name = authUser.getName();
this.email = authUser.getEmail();
this.picture = authUser.getPicture();
}
}
4-5. OAuthAttributes.java
package com.otrodevym.spring.base.common.security.oauth.base.dto;
import com.otrodevym.spring.base.common.security.oauth.base.model.BaseAuthRole;
import com.otrodevym.spring.base.common.security.oauth.base.model.BaseAuthUser;
import lombok.Builder;
import lombok.Getter;
import java.util.Map;
@Getter
public class OAuthAttributes {
private Map<String, Object> attributes;
private String nameAttributKey;
private String name;
private String email;
private String picture;
@Builder
public OAuthAttributes(Map<String, Object> attributes, String nameAttributKey, String name, String email, String picture) {
this.attributes = attributes;
this.nameAttributKey = nameAttributKey;
this.name = name;
this.email = email;
this.picture = picture;
}
public static OAuthAttributes of(String registratioId, String userNameAttributeName,
Map<String, Object> attribute) {
return ofGoogle(userNameAttributeName, attribute);
}
public static OAuthAttributes ofGoogle(String userNameAttributeName, Map<String, Object> attributes) {
return OAuthAttributes.builder()
.name(String.valueOf(attributes.get("name")))
.email(String.valueOf(attributes.get("email")))
.picture(String.valueOf(attributes.get("picture")))
.attributes(attributes)
.nameAttributKey(userNameAttributeName)
.build();
}
public BaseAuthUser toEntity() {
return BaseAuthUser.builder()
.name(name)
.email(email)
.picture(picture)
.role(BaseAuthRole.GUEST)
.build();
}
}
4-6. BaseAuthUserRepository.java
package com.otrodevym.spring.base.common.security.oauth.base.dao;
import com.otrodevym.spring.base.common.security.oauth.base.model.BaseAuthUser;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface BaseAuthUserRepository extends JpaRepository<BaseAuthUser, Long> {
Optional<BaseAuthUser> findByEmail(String email);
}
4-7. BaseCustomOAuth2UserService.java
registrationId : 간편 로그인 진행하는 플랫폼 코드
userNameAttributeName : OAuth2 로그인 진행 시 키가 되는 필드값으로 Primary Key와 같은 의미
- 구글 : "sub"
- 네이버, 카카오 : 지원안함
OAuthAttributes : OAuth2UserService를 통해 가져온 OAuth2User의 attribute 저장용도의 클래스
SessionUser : 세선 저장용 dto 클래스
package com.otrodevym.spring.base.common.security.oauth.base.service;
import com.otrodevym.spring.base.common.security.oauth.base.dao.BaseAuthUserRepository;
import com.otrodevym.spring.base.common.security.oauth.base.dto.OAuthAttributes;
import com.otrodevym.spring.base.common.security.oauth.base.dto.SessionUser;
import com.otrodevym.spring.base.common.security.oauth.base.model.BaseAuthUser;
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 // 초기화 되지 않은 final 필드나, @NonNull 필드에 생성자를 생성해준다.
@Service
public class BaseCustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final BaseAuthUserRepository baseAuthUserRepository;
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();
// oauth2 로그인 시 키가 되는 필드
String userNameAttributeName =
userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
// OAuthAttributes attributes를 담을 클래스
OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());
BaseAuthUser authUser = saveOrUpdate(attributes);
httpSession.setAttribute("user", new SessionUser(authUser));
return new DefaultOAuth2User(
Collections.singleton(new SimpleGrantedAuthority(authUser.getRoleKey())), attributes.getAttributes(),
attributes.getNameAttributKey()
);
}
private BaseAuthUser saveOrUpdate(OAuthAttributes attributes) {
BaseAuthUser authUser = baseAuthUserRepository.findByEmail(attributes.getEmail())
.map(entity -> entity.update(attributes.getName(), attributes.getPicture())).orElse(attributes.toEntity());
return baseAuthUserRepository.save(authUser);
}
}
4-8. BaseAuthController.java
package com.otrodevym.spring.base.common.security.oauth.base.contoller;
import com.otrodevym.spring.base.common.security.oauth.base.dto.SessionUser;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import javax.servlet.http.HttpSession;
@Controller
@RequiredArgsConstructor
public class BaseAuthController {
private final HttpSession httpSession;
@GetMapping("/oauth/base")
public String index(Model model) {
SessionUser user = (SessionUser) httpSession.getAttribute("user");
if (user != null) {
model.addAttribute("email", user.getEmail());
model.addAttribute("userName", user.getName());
model.addAttribute("userImg", user.getPicture());
}
return "oauth/base/index";
}
}
4-9. home.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>home</title>
</head>
<body>
<a href="/oauth2/authorization/google" class="btn btn-success active" role="button">Google Login</a>
<p th:text="${new java.util.Date().toString()}"></p>
<p th:text="${userName}"></p>
<p th:text="${email}"></p>
</body>
</html>
4-10. index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Index</title>
</head>
<body>
<a href="/oauth2/authorization/google" class="btn btn-success active" role="button">Google Login</a>
<p th:text="${userName}"></p>
<p th:text="${email}"></p>
</body>
</html>
5. 실행 결과
'개발(합니다) > Java&Spring' 카테고리의 다른 글
[spring boot 설정하기-11] actuator 설정 및 테스트 소스 (0) | 2021.04.30 |
---|---|
[spring boot 설정하기-10] dev-tools 설정 및 테스트 소스 (0) | 2021.04.28 |
[spring boot 설정하기-8] security 설정 및 테스트 소스 (1) | 2021.04.14 |
[spring boot 설정하기-7] restdocs 설정 및 테스트 소스 (0) | 2021.04.10 |
[spring boot 설정하기-6] querydsl(+JPA) 설정 및 테스트 소스 (0) | 2021.04.09 |