프로젝트 생성 및 의존성 추가
필수 의존성
- Lombok
- Spring Web
- Mustache
- Spring Security
- OAuth2 Client
- Spring Data JPA
- MySQL Driver
기본 컨트롤러 및 View 생성
@Controller
public class MainController {
@GetMapping("/")
public String mainP() {
return "main";
}
}
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Main Page</title>
</head>
<body>
main page
</body>
</html>
@Controller
public class MyController {
@GetMapping("/my")
public String myP() {
return "my";
}
}
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>My Page</title>
</head>
<body>
my page
</body>
</html>
OAuth2UserService 구현
OAuth2UserService
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
System.out.println(oAuth2User.getAttributes());
String registrationId = userRequest.getClientRegistration().getRegistrationId();
OAuth2Response oAuth2Response = null;
if (registrationId.equals("naver")) {
oAuth2Response = new NaverResponse(oAuth2User.getAttributes());
} else if (registrationId.equals("google")) {
oAuth2Response = new GoogleResponse(oAuth2User.getAttributes());
} else {
return null;
}
}
}
OAuth2Response
{
resultcode=00, message=success, response={id=123123123, name=홍길동}
}
{
resultcode=00, message=success, id=123123123, name=홍길동
}
OAuth2Response
public interface OAuth2Response {
String getProvider();
String getProviderId();
String getEmail();
String getName();
}
NaverResponse
- OAuth2Response 구현
- json데이터가 response로 감싸져 있어서 생성자에서 attribute.get("response")로 초기화
public class NaverResponse implements OAuth2Response {
private final Map<String, Object> attribute;
public NaverResponse(Map<String, Object> attribute) {
this.attribute = (Map<String, Object>) attribute.get("response");
}
@Override
public String getProvider() {
return "naver";
}
@Override
public String getProviderId() {
return attribute.get("id").toString();
}
@Override
public String getEmail() {
return attribute.get("email").toString();
}
@Override
public String getName() {
return attribute.get("name").toString();
}
}
GoogleResponse
public class GoogleResponse implements OAuth2Response{
private final Map<String, Object> attribute;
public GoogleResponse(Map<String, Object> attribute) {
this.attribute = attribute;
}
@Override
public String getProvider() {
return "google";
}
@Override
public String getProviderId() {
return attribute.get("sub").toString();
}
@Override
public String getEmail() {
return attribute.get("email").toString();
}
@Override
public String getName() {
return attribute.get("name").toString();
}
}
OAuth2Response를 SecurityConfig에 등록
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final CustomOAuth2UserService customOAuth2UserService;
public SecurityConfig(CustomOAuth2UserService customOAuth2UserService) {
this.customOAuth2UserService = customOAuth2UserService;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.csrf((csrf) -> csrf.disable());
http
.formLogin((login) -> login.disable());
http
.httpBasic((basic) -> basic.disable());
http
.oauth2Login((oauth2) -> oauth2
.userInfoEndpoint((userInfoEndpointConfig ->
userInfoEndpointConfig.userService(customOAuth2UserService))));
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/", "/oauth2/**", "/login/**").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
}
로그인 완료
CustomOAuth2UserService 최종 구현
- 유저에게 role 부여 및 CustomOAuth2User를 만들어서 리턴
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
System.out.println(oAuth2User.getAttributes());
String registrationId = userRequest.getClientRegistration().getRegistrationId();
OAuth2Response oAuth2Response = null;
if (registrationId.equals("naver")) {
oAuth2Response = new NaverResponse(oAuth2User.getAttributes());
} else if (registrationId.equals("google")) {
oAuth2Response = new GoogleResponse(oAuth2User.getAttributes());
} else {
return null;
}
String role = "ROLE_USER";
return new CustomOAuth2User(oAuth2Response, role);
}
}
CustomOAuth2user 생성
- OAuth2User를 구현
-Collection<GrantedAuthority> collection = new ArrayList<>(); 컬렉션 생성
- collection.add()를 통해 role값 리턴
- getUsername()은 임의로 사용자 식별 값으로 사용하기 위한 예시
public class CustomOAuth2User implements OAuth2User {
private final OAuth2Response oAuth2Response;
private final String role;
public CustomOAuth2User(OAuth2Response oAuth2Response, String role) {
this.oAuth2Response = oAuth2Response;
this.role = role;
}
@Override
public Map<String, Object> getAttributes() {
return null;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collection = new ArrayList<>();
collection.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return role;
}
});
return collection;
}
@Override
public String getName() {
return oAuth2Response.getName();
}
public String getUsername() {
return oAuth2Response.getProvider() + " " + oAuth2Response.getProviderId();
}
}
유저 정보 DB 저장
User 엔티티 작성
@Entity
@Getter
@Setter
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String email;
private String role;
}
UserRepository 작성
public interface UserRepository extends JpaRepository<User, Long> {
User findByUsername(String username);
}
CustomOAuth2UserService 수정
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private final UserRepository userRepository;
public CustomOAuth2UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
System.out.println(oAuth2User.getAttributes());
String registrationId = userRequest.getClientRegistration().getRegistrationId();
OAuth2Response oAuth2Response = null;
if (registrationId.equals("naver")) {
oAuth2Response = new NaverResponse(oAuth2User.getAttributes());
} else if (registrationId.equals("google")) {
oAuth2Response = new GoogleResponse(oAuth2User.getAttributes());
} else {
return null;
}
String username = oAuth2Response.getProvider() + " " + oAuth2Response.getProviderId();
User findUser = userRepository.findByUsername(username);
String role = null;
if (findUser == null) {
User user = new User();
user.setUsername(username);
user.setEmail(oAuth2Response.getEmail());
user.setRole("ROLE_USER");
userRepository.save(user);
} else {
role = findUser.getRole();
findUser.setEmail(oAuth2Response.getEmail());
}
return new CustomOAuth2User(oAuth2Response, role);
}
}
커스텀 로그인 페이지
커스텀 로그인 페이지 설정
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Login Page</title>
</head>
<body>
login page
<hr>
<a href="/oauth2/authorization/naver">naver login</a><br>
<a href="/oauth2/authorization/google">google login</a><br>
</body>
</html>
@Controller
public class LoginController {
@GetMapping("/login")
public String loginP() {
return "login";
}
}
http
.oauth2Login((oauth2) -> oauth2
.loginPage("/login")
.userInfoEndpoint((userInfoEndpointConfig ->
userInfoEndpointConfig.userService(customOAuth2UserService))));
ClientRegistration
OAuth2 서비스 변수 등록 방법
- ClientRegistration
- 서비스별 OAuth2 클라이언트 정보를 가지고 있는 클래스
- ClientRegistrationRepository
- ClientRegistration의 저장소로 서비스별 ClientRegistration들을 가짐
ClientRegistration 생성
@Component
public class SocialClientRegistration {
public ClientRegistration naverClientRegistration() {
return ClientRegistration.withRegistrationId("naver")
.clientId("아이디")
.clientSecret("비밀번호")
.redirectUri("http://localhost:8080/login/oauth2/code/naver")
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.scope("name", "email")
.authorizationUri("https://nid.naver.com/oauth2.0/authorize")
.tokenUri("https://nid.naver.com/oauth2.0/token")
.userInfoUri("https://openapi.naver.com/v1/nid/me")
.userNameAttributeName("response")
.build();
}
public ClientRegistration googleClientRegistration() {
return ClientRegistration.withRegistrationId("google")
.clientId("아이디")
.clientSecret("비밀번호")
.redirectUri("http://localhost:8080/login/oauth2/code/google")
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.scope("profile", "email")
.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth")
.tokenUri("https://www.googleapis.com/oauth2/v4/token")
.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs")
.issuerUri("https://accounts.google.com")
.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")
.userNameAttributeName(IdTokenClaimNames.SUB)
.build();
}
ClientRegistrationRepository
- ClientRegistration 종류가 몇가지 되지 않아 InMemory 방식 사용
@Configuration
public class CustomClientRegistrationRepo {
private final SocialClientRegistration socialClientRegistration;
public CustomClientRegistrationRepo(SocialClientRegistration socialClientRegistration) {
this.socialClientRegistration = socialClientRegistration;
}
public ClientRegistrationRepository clientRegistrationRepository() {
return new InMemoryClientRegistrationRepository(
socialClientRegistration.naverClientRegistration(),
socialClientRegistration.googleClientRegistration());
}
}
SecurityConfig에 등록
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final CustomOAuth2UserService customOAuth2UserService;
private final CustomClientRegistrationRepo customClientRegistrationRepo;
public SecurityConfig(CustomOAuth2UserService customOAuth2UserService, CustomClientRegistrationRepo customClientRegistrationRepo) {
this.customOAuth2UserService = customOAuth2UserService;
this.customClientRegistrationRepo = customClientRegistrationRepo;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.csrf((csrf) -> csrf.disable());
http
.formLogin((login) -> login.disable());
http
.httpBasic((basic) -> basic.disable());
http
.oauth2Login((oauth2) -> oauth2
.loginPage("/login")
.clientRegistrationRepository(customClientRegistrationRepo.clientRegistrationRepository())
.userInfoEndpoint((userInfoEndpointConfig ->
userInfoEndpointConfig.userService(customOAuth2UserService))));
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/", "/oauth2/**", "/login/**").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
}
OAuth2AuthorizationRequestRedirectFilter
로그인 페이지 : /oauth2/authorization/서비스명
- 로그인 페이지에서 GET : /oauth2/authorization/서비스명 경로로 요청을 할 경우 OAuth2 의존성에 의해 OAuth2AuthorizationRequestRedirectFilter에서 해당 요청을 받고 내부 프로세서를 진행한다.
OAuth2LoginAuthenticationFilter
로그인 redirect_uri 필터 : /login/oauth2/code/서비스명
- OAuth2LoginAuthenticationFilter는 인증 서버에서 로그인을 성공한 뒤 우리 서버측으로 발급되는 CODE를 획득하고, CODE를 통해 Access 토큰과 User 정보를 획득하는 OAuth2LoginAuthenticationProvider를 호출하는 일련의 과정을 시작하는 필터임
OAuth2AuthorizedClientService
OAuth2AuthorizedClientService 설명
- OAuth2 소셜 로그인을 진행한 사용자에 대해 서버는 사용자의 access 토큰과 같은 정보를 담을 공간이 필요함
- 기본적으로 InMemory 형식이 사용되나 사용자가 증가하면 스케일 아웃 문제로 인해 InMemory 방식은 실무에서 사용되지 않음
- 따라서 DB에 정보를 저장하기 위해서는 OAuth2AuthorizedClientService를 직접 작성해야함
JDBC 의존성 추가
- JDBC 방식을 지원하며 JPA 사용을 원할 경우 직접 커스텀 해야함
- build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
}
CustomOAuth2AuthorizedClientService 생성
- return new를 통해 InMemory가 아닌 Jdbc방식으로 설정
@Configuration
public class CustomOAuth2AuthorizedClientService {
@Bean
public OAuth2AuthorizedClientService oAuth2AuthorizedClientService(JdbcTemplate jdbcTemplate,
ClientRegistrationRepository clientRegistrationRepository) {
return new JdbcOAuth2AuthorizedClientService(jdbcTemplate, clientRegistrationRepository);
}
}
SecurityConfig 등록
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final CustomOAuth2UserService customOAuth2UserService;
private final CustomClientRegistrationRepo customClientRegistrationRepo;
private final CustomOAuth2AuthorizedClientService customOAuth2AuthorizedClientService;
private final JdbcTemplate jdbcTemplate;
public SecurityConfig(CustomOAuth2UserService customOAuth2UserService, CustomClientRegistrationRepo customClientRegistrationRepo, CustomOAuth2AuthorizedClientService customOAuth2AuthorizedClientService, JdbcTemplate jdbcTemplate) {
this.customOAuth2UserService = customOAuth2UserService;
this.customClientRegistrationRepo = customClientRegistrationRepo;
this.customOAuth2AuthorizedClientService = customOAuth2AuthorizedClientService;
this.jdbcTemplate = jdbcTemplate;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.csrf((csrf) -> csrf.disable());
http
.formLogin((login) -> login.disable());
http
.httpBasic((basic) -> basic.disable());
http
.oauth2Login((oauth2) -> oauth2
.loginPage("/login")
.clientRegistrationRepository(customClientRegistrationRepo.clientRegistrationRepository())
.authorizedClientService(customOAuth2AuthorizedClientService.oAuth2AuthorizedClientService(
jdbcTemplate, customClientRegistrationRepo.clientRegistrationRepository()))
.userInfoEndpoint((userInfoEndpointConfig ->
userInfoEndpointConfig.userService(customOAuth2UserService))));
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/", "/oauth2/**", "/login/**").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
}
DB에 해당 테이블 생성
CREATE TABLE oauth2_authorized_client (
client_registration_id varchar(100) NOT NULL,
principal_name varchar(200) NOT NULL,
access_token_type varchar(100) NOT NULL,
access_token_value blob NOT NULL,
access_token_issued_at timestamp NOT NULL,
access_token_expires_at timestamp NOT NULL,
access_token_scopes varchar(1000) DEFAULT NULL,
refresh_token_value blob DEFAULT NULL,
refresh_token_issued_at timestamp DEFAULT NULL,
created_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
PRIMARY KEY (client_registration_id, principal_name)
);
참조 링크
개발자 유미 - 스프링 OAuth2 클라이언트 세션