API

Security/OAuth2(구글로그인)/JWT(access, refresh) 발행 및 저장 -개인 복습(API) 1부

letsDoDev 2024. 5. 1. 23:48

해당 게시물 목차

  1. 구글 로그인 (OAuth2 로그인)
  2.  + 토큰 관리
  3. 일반로그인 + 이메일 인증

 


  • java : 21
  • framework : spring boot
  • ide : intelliJ
  • build tool : gradle
  • view : react(로그인까지만 thymeleaf)
  • db : mysql 8
  • etc : jpa

 

(프론트는 리액트로 개발할 것이나 일단 로그인 기능까지만 thymeleaf로 테스트하며 작업할 예정)

 

- pom.xml

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.2.5'
	id 'io.spring.dependency-management' version '1.1.4'
}

group = 'com.login'
version = '0.0.1-SNAPSHOT'

java {
	sourceCompatibility = '21'
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	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-springsecurity6'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'com.mysql:mysql-connector-j'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.security:spring-security-test'
}

tasks.named('test') {
	useJUnitPlatform()
}

 

- 사용할 DB user, database

CREATE USER 'joa'@'%' IDENTIFIED BY 'joa1234';
GRANT ALL PRIVILEGES ON *.* TO 'joa'@'%';
CREATE DATABASE jwtoauthapi;

 

- application.yml

server:
  port: 5000
  servlet:
    context-path: /
    encoding:
      charset: utf-8
      enabled: true
      force: true

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/jwtoauthapi?serverTimezone=Asia/Seoul
    username: joa
    password: joa1234

  mvc:
    view:
      prefix: /templates/
      suffix: .html

  jpa:
    hibernate:
      ddl-auto: update
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
        show-sql: true

 

- 브라우저 localhost:5000 에 접속

- ide 콘솔 창에 뜬 시큐리티 암호 + usename : "user" 로 시큐리티 테스트 수행

 

 

- 로그인 성공 다만 접속 이후 페이지 미지정으로 404 에러 코드 뜸

 

- IndexController.java 

package Global;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * (localhost:5050 접속 시 로그인 페이지 이동을 위한 컨트롤러)
 */
@Controller
@Slf4j
public class IndexController {

    @GetMapping({"", "/index"})
    public String indexPage(){
        return "index";
    }
}

 

- index.html (아직 실제 기능은 미완성 indexController 매핑만 확인할 예정)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>로그인 페이지</title>
</head>
<body>
    <h1>로그인 페이지</h1>
    <button
            type="button"
            onclick="alert('구글 로그인 API 실행될 버튼')"
    >구글 로그인</button>
</body>
</html>

 

- localhost:5000 접속 후 시큐리티 로그인까지 해주면 -> 매핑 성공

 

- 시큐리티 로그아웃 방법

 

 

- 지금은 시큐리티 별도 설정을 하지 않아서  "localhost:5000/" 로 시작하는 url로 접속이 모두

-- 시큐리티 로그인을 거쳐 로그인을 했을 때만 접근이 가능하다.

-- ex) "localhost:5000/hello.html" 이렇게 접속 시 hello.html로 바로 이동하는 게 아니라

-- 시큐리티 로그인 창 -> 로그인 성공하면 -> hello.html로 이동 이렇게 된다.

 

 

- SecurityConfig.java (로그인 page는 시큐리티 통과 안 해도 접근 가능하게 만들어주자)

(

jdk 11, spring boot 2.x.x 로 시큐리티 api를 만든적 있는데

jdk21, spring boot 3.x.x 시큐리티 설정은 처음이라 당황했다 패키지, 인터페이스가 다르다 주의!

jdk 11, spring boot 2.x.x 버전으로 진행한 시큐리티, JWT, OAUTH 구글 로그인은

여기 블로그의

spring security > 카테고리에 있는 게시물을 확인하면 된다.

)

 

package com.login.jwtoauth.Config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity // 스프링 시큐리티 필터가 스프링 필터체인에 등록이 됨.
@EnableMethodSecurity
/**
 *  *  @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) -> 스프링 시큐리티 5.6 이전
 *  *  @EnableMethodSecurity -> 스프링 시큐리티 5.6 이후
 *  └→ 컨트롤러 매핑 메소드에
 *      @Securered,
 *      @PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')"),
 *      @PostAuthorize, 등의 어노테이션을 붙여 SecruityConfig 에서 권한체크를 미리 설정해주지 않아도
 *      컨트롤러 매핑에 바로 권한 체크를 부여하여 사용할 수 있는 방법
 *      위 어노테이션 안에서 쓸수 있는 기능에는
 *      hasRole([role])
 *      hasAnyRole([role1,role2 ...])
 *      principal
 *      authentication
 *      permitAll
 *      denyAll
 *      isAnonymous()
 *      isRememberMe()
 *      isAuthenticated()
 *      isFullyAuthenticated() 등이 있다.
 */
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                // stateless한 rest api를 개발할 것이므로 csrf 공격에 대한 옵션은 꺼둔다.
                .csrf(AbstractHttpConfigurer::disable)

                // 특정 URL에 대한 권한 설정.
                .authorizeHttpRequests((authorizeRequests) -> {

                    // 어플리케이션 맨 첫 페이지는 누구나 접근 가능하게
                    authorizeRequests.requestMatchers("/index").permitAll();
                    authorizeRequests.requestMatchers("/").permitAll();

                    // spring security 6 버전에서는 ROLE_ 생략해야됨. 이전 버전에서는 생략 안 해도 되었음.
                    // .hasRole() 대신에 .hasAuthority()를 사용하게 되면 ROEL_ 를 붙여줘야 한다.
                    //authorizeRequests.requestMatchers("/service/**").authenticated(); // 어떤 권한인지 상관없이 인증된 사용자면 들어올 수 있게
                    
                    // service로 시작하는 url은 USER 권한을 가진 사람만 접근할 수 있도록 
                    authorizeRequests.requestMatchers("/service/**").hasRole("USER");
                    
                    //authorizeRequests.requestMatchers("/manager/**").hasAnyRole("ADMIN", "MANAGER");
                    //authorizeRequests.requestMatchers("/admin/**").hasRole("ADMIN");

                    // 기타 다른 url로 모두 요청 및 접근 가능하게
                    authorizeRequests.anyRequest().permitAll();
                })

                .formLogin((formLogin) -> {
                    /* 권한이 필요한 요청은 해당 url로 리다이렉트 */
                    formLogin.loginPage("/index");
                })

                .build();
    }
}

 

- BeanConfig.java

package com.login.jwtoauth.Config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
public class BeanConfig {
    // 해당 메서드의 리턴되는 오브젝트 IOC를 등록해준다.
    @Bean
    public BCryptPasswordEncoder encodePwd(){
        return new BCryptPasswordEncoder();
    }
}

 

- User.java (엔티티)

package com.login.jwtoauth.User;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jdk.jfr.Timestamp;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;

import java.time.LocalDateTime;

@NoArgsConstructor
@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    private String password;
    private String email;
    private String role;
    private String token; // 임시 컬럼
    @CreationTimestamp
    private LocalDateTime createdDate;
}

 

- servicePage.html (생성)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>서비스 페이지</title>
</head>
<body>
    <h1> 서비스 페이지 - "USER" 권한이 있어야 들어올 수 있는 공간 </h1>
</body>
</html>

 

- IndexController.java (수정)

package com.login.jwtoauth.Global;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * (localhost:5050 접속 시 로그인 페이지 이동을 위한 컨트롤러)
 */
@Controller
@Slf4j
public class IndexController {

    @GetMapping({"", "/index"})
    public String indexPage(){
        return "index";
    }

    @GetMapping("/service")
    public String servicePage() {
        return "servicePage";
    }
}

 

- localhost:5000/service 로 접속했는데 index페이지로 넘거감

-- SecurityConfig.java 에서 .formLogin 설정 적용 잘 된 거 확인됐음

 


- 권한 체크가 끝났으니 본격적으로 소셜 로그인  API를 만들 것이다. ( 우선 구글로그인만 가능하게 만들 예정 )

 

- 접속

https://console.cloud.google.com/

 

Google 클라우드 플랫폼

로그인 Google 클라우드 플랫폼으로 이동

accounts.google.com

 

- 나는 기존에 발급햇던 Oauth 서비스 가 존재하기 때문에 글 작성은 생략하겠다.

- ▶ 그래도 보려면 여기 참조 https://letsdodev.tistory.com/154

(└→ 개인 기록 게시물, 보호 중인 게시글로 공개가 불가능합니다. 미리 죄송합니다ㅠㅠ)

 

spring security - #6 구글 로그인 준비 ★

보호되어 있는 글입니다. 내용을 보시려면 비밀번호를 입력하세요.

letsdodev.tistory.com

- URI 리디렉션만 수정해줬다 기존 프로젝트에서 8090 포트를 사용했었는데, 현재 프로젝트는 5000 포트를 사용하고 있기 떄문

 

 

- 발급 받았으니 이제 프로젝트로 가서 구글 로그인에 필요한 라이브러리를 설치

- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-oauth2-client/2.5.4

 - 해당 프로젝트는 Gradle 로 빌드를 하고 있어 dependencies를 Gradle로 추가할 것이다.

- 만약 다른 빌드툴을 사용하고 있다면 그 빌드툴에 맞는 dependencies 추가 구문을 사용하자

 

- 구글 Oauth 라이브러리 추가 후  build.gradle

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.2.5'
	id 'io.spring.dependency-management' version '1.1.4'
}

group = 'com.login'
version = '0.0.1-SNAPSHOT'

java {
	sourceCompatibility = '21'
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	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-springsecurity6'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'com.mysql:mysql-connector-j'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.security:spring-security-test'

	// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-oauth2-client
	implementation group: 'org.springframework.boot', name: 'spring-boot-starter-oauth2-client', version: '2.5.4'
}

tasks.named('test') {
	useJUnitPlatform()
}

 

 

- application.yml 에 구글 oauth2 login api 설정 추가 : 발급받은 id와 secret을 바로 applicaiton.yml 에 하드코딩 하지 말고

시스템 변수에 등록 후 사용하자 ( -> 보안 강화)

server:
  port: 5000
  servlet:
    context-path: /
    encoding:
      charset: utf-8
      enabled: true
      force: true

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/jwtoauthapi?serverTimezone=Asia/Seoul
    username: joa
    password: joa1234

  mvc:
    view:
      prefix: /templates/
      suffix: .html

  jpa:
    hibernate:
      ddl-auto: update
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
        show-sql: true

  security:
    oauth2:
      client:
        registration:
          google:
            client-id: ${GOOGLE_OAUTH2_ID} # 시스템 변수로 등록한 id
            client-secret: ${GOOGLE_OAUTH2_SECRET} # 시스템 변수로 등록한 secret
            scope:
              - email
              - profile

 

- index.html 에 a 태그 href로 구글 로그인 연결 (저 url 은 내가 함부로 변경할 수 있는 값이 아님)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>로그인 페이지</title>
</head>
<body>
    <h1>로그인 페이지</h1>
    <a href="/oauth2/authorization/google">
        <button
                type="button"
        >구글 로그인</button>
    </a>
</body>
</html>

 

- servicePage.html (로그인 성공하면 보여줄 페이지)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>서비스 페이지</title>
</head>
<body>
    <h1> 서비스 페이지 - "USER" 권한이 있어야 들어올 수 있는 공간 </h1>
</body>
</html>

 

- SecurityConfig.java 수정

package com.login.jwtoauth.Config;

import com.login.jwtoauth.Config.oauth.PrincipalOauth2UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity // 스프링 시큐리티 필터가 스프링 필터체인에 등록이 됨.
@EnableMethodSecurity
@RequiredArgsConstructor
/**
 *  *  @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) -> 스프링 시큐리티 5.6 이전
 *  *  @EnableMethodSecurity -> 스프링 시큐리티 5.6 이후
 *  └→ 컨트롤러 매핑 메소드에
 *      @Securered,
 *      @PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')"),
 *      @PostAuthorize, 등의 어노테이션을 붙여 SecruityConfig 에서 권한체크를 미리 설정해주지 않아도
 *      컨트롤러 매핑에 바로 권한 체크를 부여하여 사용할 수 있는 방법
 *      위 어노테이션 안에서 쓸수 있는 기능에는
 *      hasRole([role])
 *      hasAnyRole([role1,role2 ...])
 *      principal
 *      authentication
 *      permitAll
 *      denyAll
 *      isAnonymous()
 *      isRememberMe()
 *      isAuthenticated()
 *      isFullyAuthenticated() 등이 있다.
 */
public class SecurityConfig {
    
    // oauth2 로그인 시 endPoint에서 사용하기 위해 빈 주입
    private final PrincipalOauth2UserService principalOauth2UserService;

    // 해당 메서드의 리턴되는 오브젝트 IOC를 등록해준다.
    @Bean
    public BCryptPasswordEncoder encodePwd(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                // stateless한 rest api를 개발할 것이므로 csrf 공격에 대한 옵션은 꺼둔다.
                .csrf(AbstractHttpConfigurer::disable)

                // 특정 URL에 대한 권한 설정.
                .authorizeHttpRequests((authorizeRequests) -> {

                    // 어플리케이션 맨 첫 페이지는 누구나 접근 가능하게
                    authorizeRequests.requestMatchers("/index").permitAll();
                    authorizeRequests.requestMatchers("/").permitAll();

                    // spring security 6 버전에서는 ROLE_ 생략해야됨. 이전 버전에서는 생략 안 해도 되었음.
                    // .hasRole() 대신에 .hasAuthority()를 사용하게 되면 ROEL_ 를 붙여줘야 한다.
                    //authorizeRequests.requestMatchers("/service/**").authenticated(); // 어떤 권한인지 상관없이 인증된 사용자면 들어올 수 있게
                    
                    // service로 시작하는 url은 USER 권한을 가진 사람만 접근할 수 있도록 
                    authorizeRequests.requestMatchers("/service/**").hasRole("USER");
                    
                    //authorizeRequests.requestMatchers("/manager/**").hasAnyRole("ADMIN", "MANAGER");
                    //authorizeRequests.requestMatchers("/admin/**").hasRole("ADMIN");

                    // 기타 다른 url로 모두 요청 및 접근 가능하게
                    authorizeRequests.anyRequest().permitAll();
                })

                .formLogin((formLogin) -> {
                    /* 권한이 필요한 요청은 해당 url로 리다이렉트 */
                    formLogin
                            .loginPage("/index")
                            .defaultSuccessUrl("/service");
                })

                .oauth2Login((oauth2) -> oauth2
                        .loginPage("/oauth2/authorization/google") // 권한 접근 실패 시 로그인 페이지로 이동
                        .defaultSuccessUrl("http://localhost:5000/service") // 로그인 성공 시 이동할 페이지
                        .failureUrl("/oauth2/authorization/google") )// 로그인 실패 시 이동 페이지
//                        .userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint   // --> 구글로그인이 완료된 이후의 후처리 과정: 1.코드받기(인증) 2.엑세트토큰, 3.사용자프로필 정보를 가저오고
//                                .userService(principalOauth2UserService)))        //-> 매개변수에 oausth2userservice 타입이 들어가야 함
                        // 후처리 과정
                        // 1. 그 정보를 토대로 회원가입을 자동으로 진행시키기도 함
                        // 2. (이메일, 전화번호, 이름, 아이디) 만 넘어오기 때문에 정보가 모자라다.
                        // ex) 쇼핑몰의 경우 집주소, 등급 등등
                        // 추가적인 정보가 요구되는 경우 추가적인 창을 띄워서 데이터를 받아야 한다.
                        // but 추가적인 정보가 필요없다면 구글이 주는 기본적인 정보로만 회원가입을 진행시켜도 된다.
                        // tip. 구글 로그인이 되면 코드를 돌려받는 게 아니라 엑세스토큰 + 사용자 프로필을 넘겨받는다.

                .build();
    }
}

 

- 여기까지가 버튼 클릭했을 때 구글 로그인 창 나오게 보여주는 부분이다.

 

 

- 아직 계정선택해서 로그인을 하면 403 에러가 뜬다


 

- 후처리를 하지 않아 생기는 문제이다 -> SecurityConfig.java 에서 .useInfoEndpoint 설정을 해주면 됨

- SecurityConfig.java

package com.login.jwtoauth.Config;

import com.login.jwtoauth.Config.oauth.PrincipalOauth2UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity // 스프링 시큐리티 필터가 스프링 필터체인에 등록이 됨.
@EnableMethodSecurity
@RequiredArgsConstructor
/**
 *  *  @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) -> 스프링 시큐리티 5.6 이전
 *  *  @EnableMethodSecurity -> 스프링 시큐리티 5.6 이후
 *  └→ 컨트롤러 매핑 메소드에
 *      @Securered,
 *      @PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')"),
 *      @PostAuthorize, 등의 어노테이션을 붙여 SecruityConfig 에서 권한체크를 미리 설정해주지 않아도
 *      컨트롤러 매핑에 바로 권한 체크를 부여하여 사용할 수 있는 방법
 *      위 어노테이션 안에서 쓸수 있는 기능에는
 *      hasRole([role])
 *      hasAnyRole([role1,role2 ...])
 *      principal
 *      authentication
 *      permitAll
 *      denyAll
 *      isAnonymous()
 *      isRememberMe()
 *      isAuthenticated()
 *      isFullyAuthenticated() 등이 있다.
 */
public class SecurityConfig {
    
    // oauth2 로그인 시 endPoint에서 사용하기 위해 빈 주입
    private final PrincipalOauth2UserService principalOauth2UserService;

    // 해당 메서드의 리턴되는 오브젝트 IOC를 등록해준다.
    @Bean
    public BCryptPasswordEncoder encodePwd(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                // stateless한 rest api를 개발할 것이므로 csrf 공격에 대한 옵션은 꺼둔다.
                .csrf(AbstractHttpConfigurer::disable)

                // 특정 URL에 대한 권한 설정.
                .authorizeHttpRequests((authorizeRequests) -> {

                    // 어플리케이션 맨 첫 페이지는 누구나 접근 가능하게
                    authorizeRequests.requestMatchers("/index").permitAll();
                    authorizeRequests.requestMatchers("/").permitAll();

                    // spring security 6 버전에서는 ROLE_ 생략해야됨. 이전 버전에서는 생략 안 해도 되었음.
                    // .hasRole() 대신에 .hasAuthority()를 사용하게 되면 ROEL_ 를 붙여줘야 한다.
                    //authorizeRequests.requestMatchers("/service/**").authenticated(); // 어떤 권한인지 상관없이 인증된 사용자면 들어올 수 있게
                    
                    // service로 시작하는 url은 USER 권한을 가진 사람만 접근할 수 있도록 
                    authorizeRequests.requestMatchers("/service/**").hasRole("USER");
                    
                    //authorizeRequests.requestMatchers("/manager/**").hasAnyRole("ADMIN", "MANAGER");
                    //authorizeRequests.requestMatchers("/admin/**").hasRole("ADMIN");

                    // 기타 다른 url로 모두 요청 및 접근 가능하게
                    authorizeRequests.anyRequest().permitAll();
                })

                .formLogin((formLogin) -> {
                    /* 권한이 필요한 요청은 해당 url로 리다이렉트 */
                    formLogin
                            .loginPage("/index")
                            .defaultSuccessUrl("/service");
                })

                .oauth2Login((oauth2) -> oauth2
                        .loginPage("/oauth2/authorization/google") // 권한 접근 실패 시 로그인 페이지로 이동
                        .defaultSuccessUrl("http://localhost:5000/service") // 로그인 성공 시 이동할 페이지
                        .failureUrl("/oauth2/authorization/google")// 로그인 실패 시 이동 페이지
                        .userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint   // --> 구글로그인이 완료된 이후의 후처리 과정: 1.코드받기(인증) 2.엑세트토큰, 3.사용자프로필 정보를 가저오고
                                .userService(principalOauth2UserService)))        //-> 매개변수에 oausth2userservice 타입이 들어가야 함
                        // 후처리 과정
                        // 1. 그 정보를 토대로 회원가입을 자동으로 진행시키기도 함
                        // 2. (이메일, 전화번호, 이름, 아이디) 만 넘어오기 때문에 정보가 모자라다.
                        // ex) 쇼핑몰의 경우 집주소, 등급 등등
                        // 추가적인 정보가 요구되는 경우 추가적인 창을 띄워서 데이터를 받아야 한다.
                        // but 추가적인 정보가 필요없다면 구글이 주는 기본적인 정보로만 회원가입을 진행시켜도 된다.
                        // tip. 구글 로그인이 되면 코드를 돌려받는 게 아니라 엑세스토큰 + 사용자 프로필을 넘겨받는다.

                .build();
    }
}

 

- PrinciplaOauth2UserService.java ( -> extends DefaultOAuth2UserService : 필수)

-- 우선 구글 로그인 시 넘어오는 데이터들 확인만

package com.login.jwtoauth.Config.oauth;

import com.login.jwtoauth.Config.auth.PrincipalDetails;
import com.login.jwtoauth.Domain.User.Entity.User;
import com.login.jwtoauth.Domain.User.Repository.userRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * 이런 시큐리티 서비스를 상속받아서 오버라이드 하는 서비스는
 * 내가 별도로 매핑하여 실행시켜줄 필요가 없는 서비스이다.
 *   ▶ 구글 로그인 클릭 시 자동으로 실행되기 때문
 */

@Service
@Slf4j
@RequiredArgsConstructor
@Transactional
public class PrincipalOauth2UserService extends DefaultOAuth2UserService {

    private final BCryptPasswordEncoder bCryptPasswordEncoder;
    // private final userRepository userRepository;

    // 구글 로그인 계정 클릭했을 때 넘어오는 계정 관련 데이터들을 후처리하는 service

    // DefaultOAuth2UserService 에 있는 메소드 overriding
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {

        // 로그인시 넘어오는 데이터들을 찍어보자
        log.info(userRequest.getAccessToken().toString());
        log.info(userRequest.getClientRegistration().toString());
        log.info(userRequest.getAdditionalParameters().toString());
        log.info(super.loadUser(userRequest).getAuthorities().toString());
        log.info(super.loadUser(userRequest).getAttributes().toString());
        /**
         * 2024-04-21T19:08:16.347+09:00  INFO 14076 --- [nio-5000-exec-5] c.l.j.C.o.PrincipalOauth2UserService     : org.springframework.security.oauth2.core.OAuth2AccessToken@[고유번호]
         * 2024-04-21T19:08:16.351+09:00  INFO 14076 --- [nio-5000-exec-5] c.l.j.C.o.PrincipalOauth2UserService     : ClientRegistration{registrationId='google', clientId=[api 에 사용되었었던 클라이언트 id], clientSecret=[api 에 사용되었었던 클라이언트 secret], clientAuthenticationMethod=client_secret_basic, authorizationGrantType=org.springframework.security.oauth2.core.AuthorizationGrantType@[고유번호] redirectUri='{baseUrl}/{action}/oauth2/code/{registrationId}', scopes=[email, profile], providerDetails=org.springframework.security.oauth2.client.registration.ClientRegistration$ProviderDetails@[고유번호], clientName='Google'}
         * 2024-04-21T19:08:16.351+09:00  INFO 14076 --- [nio-5000-exec-5] c.l.j.C.o.PrincipalOauth2UserService     : {id_token= [user 관련 정보를 담고 있는 jwt 토큰]}
         * 2024-04-21T19:08:16.581+09:00  INFO 14076 --- [nio-5000-exec-5] c.l.j.C.o.PrincipalOauth2UserService     : [OAUTH2_USER, SCOPE_https://www.googleapis.com/auth/userinfo.email, SCOPE_https://www.googleapis.com/auth/userinfo.profile, SCOPE_openid]
         * 2024-04-21T19:08:16.740+09:00  INFO 14076 --- [nio-5000-exec-5] c.l.j.C.o.PrincipalOauth2UserService     : {sub=[고유번호], name=[전체이름], given_name=[이름], family_name=[성], picture=[프로필이미지주소], email=[아이디]@gmail.com, email_verified=true, locale=ko}
         */

        return super.loadUser(userRequest);

    }
}

 

- 이제 로그인을 시도해서 log에 찍히는 값을 확인해보자.

2024-04-21T19:08:16.347+09:00  INFO 14076 --- [nio-5000-exec-5] c.l.j.C.o.PrincipalOauth2UserService     : org.springframework.security.oauth2.core.OAuth2AccessToken@[고유번호]
2024-04-21T19:08:16.351+09:00  INFO 14076 --- [nio-5000-exec-5] c.l.j.C.o.PrincipalOauth2UserService     : ClientRegistration{registrationId='google', clientId=[api 에 사용되었었던 클라이언트 id], clientSecret=[api 에 사용되었었던 클라이언트 secret], clientAuthenticationMethod=client_secret_basic, authorizationGrantType=org.springframework.security.oauth2.core.AuthorizationGrantType@[고유번호] redirectUri='{baseUrl}/{action}/oauth2/code/{registrationId}', scopes=[email, profile], providerDetails=org.springframework.security.oauth2.client.registration.ClientRegistration$ProviderDetails@[고유번호], clientName='Google'}
2024-04-21T19:08:16.351+09:00  INFO 14076 --- [nio-5000-exec-5] c.l.j.C.o.PrincipalOauth2UserService     : {id_token= [user 관련 정보를 담고 있는 jwt 토큰]}
2024-04-21T19:08:16.581+09:00  INFO 14076 --- [nio-5000-exec-5] c.l.j.C.o.PrincipalOauth2UserService     : [OAUTH2_USER, SCOPE_https://www.googleapis.com/auth/userinfo.email, SCOPE_https://www.googleapis.com/auth/userinfo.profile, SCOPE_openid]
2024-04-21T19:08:16.740+09:00  INFO 14076 --- [nio-5000-exec-5] c.l.j.C.o.PrincipalOauth2UserService     : {sub=[고유번호], name=[전체이름], given_name=[이름], family_name=[성], picture=[프로필이미지주소], email=[아이디]@gmail.com, email_verified=true, locale=ko}

 

- 이로 유추해보면 로그인 시 넘어오는 데이터를 전달받으려면

super.loadUser(userRequest).getAuthorities())

- 로 접근해야 한다는 것을 유추 가능

 

- UserRepository.java (인터페이스)

package com.login.jwtoauth.Domain.User.repository;

import com.login.jwtoauth.Domain.User.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}

 

- User.java : User 객체를 조금 더 쉽게 사용하기 위해 수정했다.

package com.login.jwtoauth.Domain.User.Entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jdk.jfr.Timestamp;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;

import java.time.LocalDateTime;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    private String password;
    private String email;
    private String role;
    private String token; // 임시 컬럼
    private String provider; // 어떤 클라이언트를 통해 가입한 회원인지
    private String providerId;
    @CreationTimestamp
    private LocalDateTime createdDate;

    @Builder
    private User(String username, String password, String email, String role, String token, String provider, String providerId) {
        this.username = username;
        this.password = password;
        this.email = email;
        this.role = role;
        this.token = token;
        this.provider = provider;
        this.providerId = providerId;
    }
}

 

- test 를 위해 secruityConfig 설정을 잠시 아래 사진 처럼 수정한다.

 

그리고 위에서 defaultSeccessUrl 로 지정한 경로를

아래 별도의 controller를 만들어 매핑을 확인해주자

 

- OAuth2LoginController.java (테스트 컨트롤러)

package com.login.jwtoauth.Global;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.Map;

@Controller
@Slf4j
@RequiredArgsConstructor
public class OAuth2LoginController {

    /**
     * 구를 로그인 성공 후 접근할 service
     * @param oAuth2User
     * @param authentication
     */
    @ResponseBody
    @GetMapping("/oauth/login")
    public Map<String, Object> oAuthInfoCheck(@AuthenticationPrincipal OAuth2User oAuth2User, Authentication authentication) {

        log.info("OAuth2 annotation Object check -> {}", oAuth2User.getAttributes());
        log.info ("OAhth2 casting Object -> {}", (OAuth2User) authentication.getPrincipal());

        /**
         * 두 정보가 같다
         * 즉 @AuthenticationPrincipal OAuth2User oAuth2User 와  (OAuth2User) authentication 이 동일한 타입의 객체라는 것
         */
        // 2024-04-28T13:31:02.233+09:00  INFO 6716 --- [nio-5000-exec-6] c.l.j.C.o.PrincipalOauth2UserService     : userRequest : {sub=[프라이머리 키], name=[이름], given_name=[이름], family_name=[성], picture=[이미지 경로], email=[아이디@gmail.com], email_verified=true, locale=ko}
        // 2024-04-28T13:31:03.184+09:00  INFO 6716 --- [io-5000-exec-10] c.l.j.C.o.PrincipalOauth2UserService     : userRequest : {sub=[프라이머리 키], name=[이름], given_name=[이름], family_name=[성], picture=[이미지 경로], email=[아이디@gmail.com], email_verified=true, locale=ko}

        return oAuth2User.getAttributes();
    }
}

 

- 매핑 결과

OAuth2User 객체가 생성되는 시점 ?

 ▶ 구글 로그인 시

 

우선 로그인 성공 시 세션에 회원 정보가 담기는 객체를 계층으로 나누어 보면 아래와 같다.

시큐리티 세션 > Authentication > UserDetails (일반 로그인 시)

시큐리티 세션 > Authentication > OAuth2User (일반 로그인 시)

이렇게 되면 일반 로그인과 구글 로그인 성공 시 서로 다르게  매핑시켜줘야 하는데,

나는 일반, 구글 로그인 시 모두 사용할 수 있는 객체를 하나 생성해서 하나로 매핑시켜줄 것이다.

아래 처럼

 

이 X라는 클래스는

- PrincipalDetails.java 로 정의한다.

package com.login.jwtoauth.Config.auth;

import com.login.jwtoauth.Domain.User.Entity.User;
import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;

// 시큐리티가 /login을 주소 요청이 오면 낚아채서 로그인을 진행시킨다.
// 이때 로그인 진행이 완료가 되면 session을 만들어준다.. 일반적인 session과 유사한데
// 시큐리티가 가지고 있는 session이 있다. (시큐리티가 자신만의 세션 공간을 가지고 있다.)
// Security ContextHolder 키 값에다가 session 정보를 저장한다.
// 시큐리티 세션에 들어갈 수 있는 object가 정해져있다. => Authentication 타입의 객체
// Authentication 타입의 객체 안에 User 정보가 있어야 된다.
// User오브젝트 타입이 => UserDetails 타입 객체여야한다.


// 정리하자면 ▼
// Security Session => Authentication 객체가 들어간다 => 이 그리고 이 Authentication 객체에
// 유저 정보를 전달할 때 유저정보가 UserDetails 타입이어야 한다.
// 여기서 PrincipalDetails가 UserDetails를 상속받았기 때문에 UserDetails === PrincipalDetails 라고 보면 된다.
// 이제 아래서 만든 PrincipalDetails 객체를 Authentication 객체 안에 넣을 수 있다.

// 오버라이드 하자!                        // 일반로그인과 소셜로그인(OAuth) 로그인을 둘 다 사용하기 위해
                               // UserDetails와 OAuth2User 을 모두 상속받아 오버라이드하자
@Slf4j
@ToString
@Getter
@RequiredArgsConstructor
public class PrincipalDetails implements UserDetails, OAuth2User {


    private final User user; // 내가 만들었던 user 객체 => 콤포지션
    private final Map<String, Object> attributes; // OAuth2User 받기 위해 추가한 것

    // 해당 유저의 권한을 리턴하는 곳
    @Override // return 타입이  Collection<? extends GrantedAuthority> 가 필요함.
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // 그냥 user.getRole() 은 string 타입
        Collection<GrantedAuthority> collect = new ArrayList<>(); // 우선 리턴할 타입을 맞추기 위한 객체를 생성해주자
        collect.add(new GrantedAuthority() {

            // new GrantAuthority 타입을 리턴하겠다고 코드를 자동생성하면 생성되는 함수 정의 부분인데
            // 여기에서는 String을 return 받을 수 있다.
            @Override
            public String getAuthority() {
                return user.getRole(); // 이제 드디어 user.getRole() 타입을 넘겨줄 수 있다.
            }

        }); //.add() 안에 들어가야 하는 매개변수의 타입은 granted authority 타입

        return collect;
    }

    // 패스워드를 리턴
    @Override
    public String getPassword() {
        return user.getPassword();
    }

    // 유저네임 리턴
    @Override
    public String getUsername() {
        return user.getUsername();
    }

    // 이 계정 만료 안 됐지?
    @Override
    public boolean isAccountNonExpired() {
        return true; // 응 만료 안 됐어
    }

    // 이 계정 안 잠겼지?
    @Override
    public boolean isAccountNonLocked() {
        return true; // 응 안 잠겼어
    }

    // 이 계정이 뭐 일년이 안 지났지?
    @Override
    public boolean isCredentialsNonExpired() {
        return true; // 응 안 지났어
    }

    // 이 계정이 활성화 되어있니?
    @Override
    public boolean isEnabled() {

        // 여기 응용법 [참고만 실제 코드 작업은 안 했음]
        // 우리 사이트에서 1년 동안 회원이 로그인을 안 하면 휴먼 계정으로 하기로 했다면
        // User 객체에 private Timestamp loginDate; 라는게 있어야 겠죠?
        // 로그인할 때 날짜를 넣어놓고
        // 여기서 user.getLoginDate; 해서 받아와서
        // 현재시간 - 로그인시간 해서 => 1년을 초과하면 return false;로 하면 된다.

        return true; // 응 활성화 되어있어
    }

    //----- impelemnts OAuth2User 하면서 오버라이드 생긴 메소드 ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼

    @Override
    public Map<String, Object> getAttributes() {
        return attributes;
    }

    @Override
    public String getName() {
        return attributes.get("username").toString();
    }
}

 

○ 이제 위에서 생성한 PrincipalDatails.java 를 통해 구글 로그인 버튼 클릭 시 진행될 회원가입 / 로그인 로직을

PrincipalOauth2UserServie.java 에 정의

 

- 먼저 PrinciplaOauth2UserService.java  에서 필요한 repository 메소드를 정의

- UserRepository.java

package com.login.jwtoauth.Domain.User.Repository;

import com.login.jwtoauth.Domain.User.Entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {

    // 사용자 id로 해당 사용자 정보 User 객체로 반환
    // -> 일종의 사용자 id를 통한 사용자 정보 조회로 보면 됨.
    //Optional<User> findByUsername(String userName);
    User findByUsername(String userName);
}

 

 

- PrinciplaOauth2UserService.java 를 수정해서 DB에 회원정보를 등록해 로그인 시 회원가입을 진행시키도록 하자

package com.login.jwtoauth.Config.oauth;

import com.login.jwtoauth.Config.auth.PrincipalDetails;
import com.login.jwtoauth.Domain.User.Entity.User;
import com.login.jwtoauth.Domain.User.Repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * 이런 시큐리티 서비스를 상속받아서 오버라이드 하는 서비스는
 * 내가 별도로 매핑하여 실행시켜줄 필요가 없는 서비스이다.
 *   ▶ 구글 로그인 클릭 시 자동으로 실행되기 때문
 */

@Service
@Slf4j
@RequiredArgsConstructor
@Transactional
public class PrincipalOauth2UserService extends DefaultOAuth2UserService {

    private final BCryptPasswordEncoder bCryptPasswordEncoder;
    private final UserRepository userRepository;

    // 구글 로그인 계정 클릭했을 때 넘어오는 계정 관련 데이터들을 후처리하는 service


    // DefaultOAuth2UserService 에 있는 메소드 overriding
    @Override
    @Transactional
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {

        // 로그인시 넘어오는 데이터들을 찍어보자
        //log.info(userRequest.getAccessToken().toString());
        //log.info(userRequest.getClientRegistration().toString());
        //log.info(userRequest.getAdditionalParameters().toString());
        //log.info(super.loadUser(userRequest).getAuthorities().toString());
        //log.info(super.loadUser(userRequest).getAttributes().toString());
        /**
         * 2024-04-21T19:08:16.347+09:00  INFO 14076 --- [nio-5000-exec-5] c.l.j.C.o.PrincipalOauth2UserService     : org.springframework.security.oauth2.core.OAuth2AccessToken@[고유번호]
         * 2024-04-21T19:08:16.351+09:00  INFO 14076 --- [nio-5000-exec-5] c.l.j.C.o.PrincipalOauth2UserService     : ClientRegistration{registrationId='google', clientId=[api 에 사용되었었던 클라이언트 id], clientSecret=[api 에 사용되었었던 클라이언트 secret], clientAuthenticationMethod=client_secret_basic, authorizationGrantType=org.springframework.security.oauth2.core.AuthorizationGrantType@[고유번호] redirectUri='{baseUrl}/{action}/oauth2/code/{registrationId}', scopes=[email, profile], providerDetails=org.springframework.security.oauth2.client.registration.ClientRegistration$ProviderDetails@[고유번호], clientName='Google'}
         * 2024-04-21T19:08:16.351+09:00  INFO 14076 --- [nio-5000-exec-5] c.l.j.C.o.PrincipalOauth2UserService     : {id_token= [user 관련 정보를 담고 있는 jwt 토큰]}
         * 2024-04-21T19:08:16.581+09:00  INFO 14076 --- [nio-5000-exec-5] c.l.j.C.o.PrincipalOauth2UserService     : [OAUTH2_USER, SCOPE_https://www.googleapis.com/auth/userinfo.email, SCOPE_https://www.googleapis.com/auth/userinfo.profile, SCOPE_openid]
         * 2024-04-21T19:08:16.740+09:00  INFO 14076 --- [nio-5000-exec-5] c.l.j.C.o.PrincipalOauth2UserService     : {sub=[고유번호], name=[전체이름], given_name=[이름], family_name=[성], picture=[프로필이미지주소], email=[아이디]@gmail.com, email_verified=true, locale=ko}
         */

        // 구글로그인 버튼 클릭 -> 구글로그인창 -> 로그인완료 -> code를 리턴(OAuth-Client 라이브러리) -> Access Token을 요청
        // 여기까지가 userRequest 정보 -> 회원프로필 받아야함(loadUser함수로) -> 구글로부터 회원프로필 받아준다.
        OAuth2User oAuth2User = super.loadUser(userRequest);
        log.info("oAuth2User : " + oAuth2User.getAttributes());

        // 구글로부터 넘겨받은 회원정보에서 각 attribute 들을 분리해서 정리하자
        String provider = userRequest.getClientRegistration().getRegistrationId(); // 'google'
        String providerId = oAuth2User.getAttribute("sub"); // google pk
        String username = provider + "_" + providerId; // "google_12345678" 이런 꼴 username 중복 방지

        String email = oAuth2User.getAttribute("email");

        // 토큰 저장 이번에 새로 test 하는 거
        String token = userRequest.getAccessToken().toString();

         // USER, ADMIN, UNDEFINED 이렇게 3종류로 정의
        String role = "UNDEFINED"; // 나중에 관리자 계정 따로 빼서확인할 예정 OR DB로 관리자 승인 내준 사람만 사이트 들어올 수 있게 할 거임.

         // 구글로그인만 진행할 거면 따로 없어도 되는데, 추후 일반 로그인도 개발할 것이기 때문에
         // 구글로그인 회원에게는 임의의 PW를 넣어준다.
        String password = bCryptPasswordEncoder.encode("letsdodev");

        // log.info("userRequest at PrincipalOauth2UserService -> {}", userRequest);
        // log.info("oAuth2User at PrincipalOauth2UserService -> {}", oAuth2User);

        // 구글로부터 넘겨받은 정보를 통해 회원가입을 진행하는 로직 구성
        // todo 1. 먼저 이미 가입되어 있는 회원인지 체크
        User findUser = userRepository.findByUsername(username);
        User user = null;
        if(findUser == null) {
           user = User.builder()
                    .username(username)
                    .email(email)
                    .provider(provider)
                    .providerId(providerId)
                    .token(token)
                    .password(password)
                    .build();
            // log.info("user Entity Obejct -> {}", user);
            userRepository.save(user);
        }

        // userEntity 가 null 이 아니면 --> 즉 이미 회원가입된 사람이라면
        // 이렇게 넣으면 PrincipalDetails에서 정의한 생성자에 의하여
        // oAuth2User의 어트리뷰트가 user타입으로 변함
        //PrincipalDetailsService와 동일하게
        // ※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※
        // PrincipalDetails(userEntity, Map<String,Objevt> attributes) 이걸 반환하는데 이렇게 되면
        // 시큐리티 sesstion에 Authentication(UserDetails)가 자동으로 들어가게 된다.
        // ※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※
        // Session(Authentication(User)) 꼴 완성
        // return super.loadUser(userRequest);


        return new PrincipalDetails(user, oAuth2User.getAttributes());
    }
}

 

- login 결과


 

# 여기서 현재 우리는 User 엔티티에

- token 컬럼만 있고 refresh_token 관련 내용은 존재하지 않는다.

- 사용자 이름(본명)을 받는 컬럼이 존재하지 않는다.

- 그리고 현재  PrinciplaOauth2UserService 에서 토큰 대체 값만 임의로 넣어주었다.

 

- 이제 수행해야 할 작업은 

- 1. User 엔티티 수정

  2 . PrinciplaOauth2UserService 수정

 


 

JWT access_token & refresh_token

 

- User.java (realname, accessToken, refreshToken 추가)

package com.login.jwtoauth.Domain.User.Entity;

import jakarta.persistence.*;
import jdk.jfr.Timestamp;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;

import java.time.LocalDateTime;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@ToString
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String realname; // 유저의 실명
    private String username;
    private String password;
    private String email;
    private String role;
    private String token; // 임시 컬럼
    private String provider; // 어떤 클라이언트를 통해 가입한 회원인지
    private String providerId;

    private String accessToken;
    private String refreshToekn;

    @CreationTimestamp
    private LocalDateTime createdDate;

    @Builder
    private User(String realname, String username, String password, String email, String role, String token, String provider, String providerId, String accessToken, String refreshToekn) {
        this.realname = realname;
        this.username = username;
        this.password = password;
        this.email = email;
        this.role = role;
        this.token = token;
        this.provider = provider;
        this.providerId = providerId;
        this.accessToken = accessToken;
        this.refreshToekn = refreshToekn;
    }
}

 

- 이제 jwt 토큰 발급하는 부분을 사용해야 하는데, 

- jwt 를 발행해주는 라이브러리를 사용할 것이다.

- maven repository 에서 dependency 코드 가져오기

// https://mvnrepository.com/artifact/com.auth0/java-jwt
implementation group: 'com.auth0', name: 'java-jwt', version: '4.4.0'

https://mvnrepository.com/artifact/com.auth0/java-jwt/4.4.0 ▶ 여기서 build tool 에 맞는 dependency 코드

 

더보기

- build.gradle

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.2.5'
	id 'io.spring.dependency-management' version '1.1.4'
}

group = 'com.login'
version = '0.0.1-SNAPSHOT'

java {
	sourceCompatibility = '21'
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	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-springsecurity6'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'com.mysql:mysql-connector-j'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.security:spring-security-test'

	// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-oauth2-client
	implementation group: 'org.springframework.boot', name: 'spring-boot-starter-oauth2-client', version: '2.5.4'

	// https://mvnrepository.com/artifact/com.auth0/java-jwt
	implementation group: 'com.auth0', name: 'java-jwt', version: '4.4.0'
}

tasks.named('test') {
	useJUnitPlatform()
}

# 여기서 jwt 를 사용하기 위해 security 관련 설정을 해줄 것이다.

- CorsConfig.java (생성)

package com.login.jwtoauth.Config;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

// 스프링에서 관리할 수 있도록
@Configuration
public class CorsConfig {

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration();

        // 내 서버가 응답을 할 때 json을 자바스크리브에서 처리할 수 있도록 설정
        corsConfiguration.setAllowCredentials(true);

        // 특정 origin(로컬 호스트의 3000, 5000)에 대한 요청을 허용
        // 3000은 추후 리액트로 개발할 가능성이 있어서 추가
        // 5000은 현재 프로젝트 port 번호라 추가
        corsConfiguration.addAllowedOrigin("http://localhost:3000");
        corsConfiguration.addAllowedOrigin("http://localhost:5000");

        // 모든 헤더에 대한 요청을 허용
        corsConfiguration.addAllowedHeader("*");

        // 특정 HTTP 메소드에 대한 요청을 허용
        corsConfiguration.addAllowedMethod("POST");
        corsConfiguration.addAllowedMethod("GET");
        corsConfiguration.addAllowedMethod("PATCH");
        corsConfiguration.addAllowedMethod("PUT");

        // 특정 경로에 대해서만 CORS 설정을 적용
        source.registerCorsConfiguration("http://localhost:3000/**", corsConfiguration);
        source.registerCorsConfiguration("http://localhost:5000/**", corsConfiguration);

        // CorsFilter를 생성하여 반환
        return new CorsFilter(source);
    }
}

 

 

- SecurityConfig.java (jwt 사용을 위해 설정 수정)

package com.login.jwtoauth.Config;

import com.login.jwtoauth.Config.Filter.TokenFilter;
import com.login.jwtoauth.Config.oauth.PrincipalOauth2UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
import org.springframework.web.filter.CorsFilter;

@Configuration
@EnableWebSecurity // 스프링 시큐리티 필터가 스프링 필터체인에 등록이 됨.
@EnableMethodSecurity
@RequiredArgsConstructor
/**
 *  *  @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) -> 스프링 시큐리티 5.6 이전
 *  *  @EnableMethodSecurity -> 스프링 시큐리티 5.6 이후
 *  └→ 컨트롤러 매핑 메소드에
 *      @Securered,
 *      @PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')"),
 *      @PostAuthorize, 등의 어노테이션을 붙여 SecruityConfig 에서 권한체크를 미리 설정해주지 않아도
 *      컨트롤러 매핑에 바로 권한 체크를 부여하여 사용할 수 있는 방법
 *      위 어노테이션 안에서 쓸수 있는 기능에는
 *      hasRole([role])
 *      hasAnyRole([role1,role2 ...])
 *      principal
 *      authentication
 *      permitAll
 *      denyAll
 *      isAnonymous()
 *      isRememberMe()
 *      isAuthenticated()
 *      isFullyAuthenticated() 등이 있다.
 */
public class SecurityConfig {
    
    // oauth2 로그인 시 endPoint에서 사용하기 위해 빈 주입
    private final PrincipalOauth2UserService principalOauth2UserService;
    // ## JWT 사용을 위해 추가한 코드 1
    private final CorsFilter corsFilter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                // ## JWT 사용을 위해 추가한 코드 4  TokenFilter가 시큐리티 필터보다 먼저 실행되게
                .addFilterBefore(new TokenFilter(), SecurityContextPersistenceFilter.class)

                // stateless한 rest api를 개발할 것이므로 csrf 공격에 대한 옵션은 꺼둔다.
                .csrf(AbstractHttpConfigurer::disable)

                // ## JWT 사용을 위해 추가한 코드 2 : 세션을 사용하지 않겠다는 의미
                .sessionManagement((sessionManagement) -> {
                    sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
                })

                // ## JWT 사용을 위해 추가한 코드 1
                .addFilter(corsFilter)

                // 특정 URL에 대한 권한 설정.
                .authorizeHttpRequests((authorizeRequests) -> {

                    // 어플리케이션 맨 첫 페이지는 누구나 접근 가능하게
                    authorizeRequests.requestMatchers("/index").permitAll();
                    authorizeRequests.requestMatchers("/").permitAll();

                    // spring security 6 버전에서는 ROLE_ 생략해야됨. 이전 버전에서는 생략 안 해도 되었음.
                    // .hasRole() 대신에 .hasAuthority()를 사용하게 되면 ROEL_ 를 붙여줘야 한다.
                    //authorizeRequests.requestMatchers("/service/**").authenticated(); // 어떤 권한인지 상관없이 인증된 사용자면 들어올 수 있게
                    
                    // service로 시작하는 url은 USER 권한을 가진 사람만 접근할 수 있도록 
                    authorizeRequests.requestMatchers("/service/**").hasRole("USER");
                    
                    //authorizeRequests.requestMatchers("/manager/**").hasAnyRole("ADMIN", "MANAGER");
                    //authorizeRequests.requestMatchers("/admin/**").hasRole("ADMIN");

                    // 기타 다른 url로 모두 요청 및 접근 가능하게
                    authorizeRequests.anyRequest().permitAll();
                })

                .formLogin((formLogin) -> {
                    /* 권한이 필요한 요청은 해당 url로 리다이렉트 */
                    formLogin
                            .loginPage("/index")
                            .defaultSuccessUrl("/service");
                })

                // ## JWT 사용을 위해 추가한 코드 3
                .httpBasic(HttpBasicConfigurer::disable)

                .oauth2Login((oauth2) -> oauth2
                        .loginPage("/oauth2/authorization/google") // 권한 접근 실패 시 로그인 페이지로 이동
                        //.defaultSuccessUrl("http://localhost:5000/service") // 로그인 성공 시 이동할 페이지
                        .defaultSuccessUrl("http://localhost:5000/oauth/login") // 로그인 성공 시 이동할 페이지
                        .failureUrl("/oauth2/authorization/google")// 로그인 실패 시 이동 페이지
                        .userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint   // --> 구글로그인이 완료된 이후의 후처리 과정: 1.코드받기(인증) 2.엑세트토큰, 3.사용자프로필 정보를 가저오고
                                .userService(principalOauth2UserService)))        //-> 매개변수에 oausth2userservice 타입이 들어가야 함
                        // 후처리 과정
                        // 1. 그 정보를 토대로 회원가입을 자동으로 진행시키기도 함
                        // 2. (이메일, 전화번호, 이름, 아이디) 만 넘어오기 때문에 정보가 모자라다.
                        // ex) 쇼핑몰의 경우 집주소, 등급 등등
                        // 추가적인 정보가 요구되는 경우 추가적인 창을 띄워서 데이터를 받아야 한다.
                        // but 추가적인 정보가 필요없다면 구글이 주는 기본적인 정보로만 회원가입을 진행시켜도 된다.
                        // tip. 구글 로그인이 되면 코드를 돌려받는 게 아니라 엑세스토큰 + 사용자 프로필을 넘겨받는다.

                .build();
    }
}

 

 

- OAuth2LoginController.java

SecurityConfig 에서 아래처럼

// ## JWT 사용을 위해 추가한 코드 1 : 세션을 사용하지 않겠다는 의미
                .sessionManagement((sessionManagement) -> {
                    sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
                })

세션 미사용 처리를 해주고 구글 회원가입을 시도해봤다.

DB에 적재는 잘 되었다.

그리고 OAuth2LoginController

에서 oAuth2User, authentication 가 null  (-> 500 error) 이어서

@ResponseBody 형태로 구글 회원 정보를 출력할 수 없게 되었다.

 

그래서 임시로 OAuth2LoginController를 수정했다.

package com.login.jwtoauth.Global;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.Map;

@Controller
@Slf4j
@RequiredArgsConstructor
public class OAuth2LoginController extends DefaultOAuth2UserService {

    /** SecurityConfig에 JWT 관련 설정 전
     * 구를 로그인 성공 후 접근할 service
     * @param oAuth2User
     * @param authentication
     */
//    @ResponseBody
//    @GetMapping("/oauth/login")
//    public Map<String, Object> oAuthInfoCheck(@AuthenticationPrincipal OAuth2User oAuth2User, Authentication authentication) {
//
//        // SecurityConfig 에 JWT 관련 설정 후
//        // SecruityConfig 에서 sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//        // 이후 oAuth2User, authentication 모두  null 값 나오게 됨
//        // 이유 세션을 사용하지 않도록 securityConfig 에 설정해두었기 떄문
//         log.info("OAuth2 annotation Object check -> {}", oAuth2User.getAttributes());
//         log.info ("OAhth2 casting Object -> {}", (OAuth2User) authentication.getPrincipal());
//
//        /**
//         * SecurityConfig에 JWT 관련 설정 전
//         * 두 정보가 같다
//         * 즉 @AuthenticationPrincipal OAuth2User oAuth2User 와  (OAuth2User) authentication 이 동일한 타입의 객체라는 것
//         */
//        // 2024-04-28T13:31:02.233+09:00  INFO 6716 --- [nio-5000-exec-6] c.l.j.C.o.PrincipalOauth2UserService     : userRequest : {sub=[프라이머리 키], name=[이름], given_name=[이름], family_name=[성], picture=[이미지 경로], email=[아이디@gmail.com], email_verified=true, locale=ko}
//        // 2024-04-28T13:31:03.184+09:00  INFO 6716 --- [io-5000-exec-10] c.l.j.C.o.PrincipalOauth2UserService     : userRequest : {sub=[프라이머리 키], name=[이름], given_name=[이름], family_name=[성], picture=[이미지 경로], email=[아이디@gmail.com], email_verified=true, locale=ko}
//
//        return oAuth2User.getAttributes();
//    }

    /** SecurityConfig에 JWT 관련 설정 후
     * 구를 로그인 성공 후 접근할 service
     * service 클래스에 extends  DefaultOAuth2UserService 처리해준 후
     */
    @ResponseBody
    @GetMapping("/oauth/login")
    public String oAuthInfoCheck() {
        return "<h1>구글 로그인 성공</h1>";
    }
}

- FilterConfig.java

package com.login.jwtoauth.Config;

import com.login.jwtoauth.Config.Filter.TokenFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean<TokenFilter> tokenFilterConfig() {
        FilterRegistrationBean<TokenFilter> bean = new FilterRegistrationBean<>(new TokenFilter());
        bean.addUrlPatterns("/check"); // 특정 요청해서 모두 수행
        bean.setOrder(0); // 0 -> 최우선순위 지정

        return bean; // 이렇게 하면 자동으로 필터 등록 완료 ,,
    }
}

 

- TokenFilter.java

package com.login.jwtoauth.Config.Filter;

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;

/**
 * token 있는지 없는지 확인하는 필터!
 */
@Slf4j
public class TokenFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        HttpServletResponse resp = (HttpServletResponse) servletResponse;

        String headerAccess =  req.getHeader("JOA-Access-Token");
        String headerRefresh = req.getHeader("JOA-Refresh-Token");

        if (req.getRequestURI().equals("/check")) {
            if (headerAccess != null && headerAccess.equals("")) {
                log.info("토큰 있음!");
                log.info("로그인 유지");
                resp.sendRedirect("/service");
            } else {
                log.info("토큰 없음!");
                log.info("재로그인 오구");
                resp.sendRedirect("/index");
            }
        } else {
            // http://localhost:5000/check 가 아닌 경우 필터 체인 계속 진행
            filterChain.doFilter(req, resp);
        }
    }
}

 


- 이렇게 하고 필터가 정상적으로 실행되는지 확인해보자!

- 현재 토큰(jwt) 로직을 구성하지 않았기 때문에 "토큰 없음!" 조건문을 타게 될 것이다.

 

2024-05-01T16:26:25.323+09:00  INFO 6476 --- [nio-5000-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
2024-05-01T16:26:25.343+09:00  INFO 6476 --- [nio-5000-exec-1] c.l.jwtoauth.Config.Filter.TokenFilter   : 토큰 없음!
2024-05-01T16:26:25.343+09:00  INFO 6476 --- [nio-5000-exec-1] c.l.jwtoauth.Config.Filter.TokenFilter   : 재로그인 오구
2024-05-01T16:26:25.348+09:00  INFO 6476 --- [nio-5000-exec-1] c.l.jwtoauth.Config.Filter.TokenFilter   : 토큰 없음!
2024-05-01T16:26:25.349+09:00  INFO 6476 --- [nio-5000-exec-1] c.l.jwtoauth.Config.Filter.TokenFilter   : 재로그인 오구
2024-05-01T16:26:26.679+09:00  INFO 6476 --- [nio-5000-exec-2] c.l.jwtoauth.Config.Filter.TokenFilter   : 토큰 없음!
2024-05-01T16:26:26.680+09:00  INFO 6476 --- [nio-5000-exec-2] c.l.jwtoauth.Config.Filter.TokenFilter   : 재로그인 오구
2024-05-01T16:26:27.038+09:00  INFO 6476 --- [nio-5000-exec-3] c.l.jwtoauth.Config.Filter.TokenFilter   : 토큰 없음!
2024-05-01T16:26:27.038+09:00  INFO 6476 --- [nio-5000-exec-3] c.l.jwtoauth.Config.Filter.TokenFilter   : 재로그인 오구
2024-05-01T16:26:27.617+09:00  INFO 6476 --- [nio-5000-exec-3] c.l.j.C.o.PrincipalOauth2UserService     : oAuth2User : {sub=*****************, name=****, given_name=**, family_name=*, picture=**************, email=*******@gmail.com, email_verified=true, locale=ko}

- PrincipalDetails.java (수정한 거 없는데 업데이트 언제 마지막으로 했는지 몰라서 업로드)

package com.login.jwtoauth.Config.auth;

import com.login.jwtoauth.Domain.User.Entity.User;
import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;

// 시큐리티가 /login을 주소 요청이 오면 낚아채서 로그인을 진행시킨다.
// 이때 로그인 진행이 완료가 되면 session을 만들어준다.. 일반적인 session과 유사한데
// 시큐리티가 가지고 있는 session이 있다. (시큐리티가 자신만의 세션 공간을 가지고 있다.)
// Security ContextHolder 키 값에다가 session 정보를 저장한다.
// 시큐리티 세션에 들어갈 수 있는 object가 정해져있다. => Authentication 타입의 객체
// Authentication 타입의 객체 안에 User 정보가 있어야 된다.
// User오브젝트 타입이 => UserDetails 타입 객체여야한다.


// 정리하자면 ▼
// Security Session => Authentication 객체가 들어간다 => 이 그리고 이 Authentication 객체에
// 유저 정보를 전달할 때 유저정보가 UserDetails 타입이어야 한다.
// 여기서 PrincipalDetails가 UserDetails를 상속받았기 때문에 UserDetails === PrincipalDetails 라고 보면 된다.
// 이제 아래서 만든 PrincipalDetails 객체를 Authentication 객체 안에 넣을 수 있다.

// 오버라이드 하자!                        // 일반로그인과 소셜로그인(OAuth) 로그인을 둘 다 사용하기 위해
                               // UserDetails와 OAuth2User 을 모두 상속받아 오버라이드하자
@Slf4j
@ToString
@Getter
@RequiredArgsConstructor
public class PrincipalDetails implements UserDetails, OAuth2User {


    private final User user; // 내가 만들었던 user 객체 => 콤포지션
    private final Map<String, Object> attributes; // OAuth2User 받기 위해 추가한 것

    // 해당 유저의 권한을 리턴하는 곳
    @Override // return 타입이  Collection<? extends GrantedAuthority> 가 필요함.
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // 그냥 user.getRole() 은 string 타입
        Collection<GrantedAuthority> collect = new ArrayList<>(); // 우선 리턴할 타입을 맞추기 위한 객체를 생성해주자
        collect.add(new GrantedAuthority() {

            // new GrantAuthority 타입을 리턴하겠다고 코드를 자동생성하면 생성되는 함수 정의 부분인데
            // 여기에서는 String을 return 받을 수 있다.
            @Override
            public String getAuthority() {
                return user.getRole(); // 이제 드디어 user.getRole() 타입을 넘겨줄 수 있다.
            }

        }); //.add() 안에 들어가야 하는 매개변수의 타입은 granted authority 타입

        return collect;
    }

    // 패스워드를 리턴
    @Override
    public String getPassword() {
        return user.getPassword();
    }

    // 유저네임 리턴
    @Override
    public String getUsername() {
        return user.getUsername();
    }

    // 이 계정 만료 안 됐지?
    @Override
    public boolean isAccountNonExpired() {
        return true; // 응 만료 안 됐어
    }

    // 이 계정 안 잠겼지?
    @Override
    public boolean isAccountNonLocked() {
        return true; // 응 안 잠겼어
    }

    // 이 계정이 뭐 일년이 안 지났지?
    @Override
    public boolean isCredentialsNonExpired() {
        return true; // 응 안 지났어
    }

    // 이 계정이 활성화 되어있니?
    @Override
    public boolean isEnabled() {

        // 여기 응용법 [참고만 실제 코드 작업은 안 했음]
        // 우리 사이트에서 1년 동안 회원이 로그인을 안 하면 휴먼 계정으로 하기로 했다면
        // User 객체에 private Timestamp loginDate; 라는게 있어야 겠죠?
        // 로그인할 때 날짜를 넣어놓고
        // 여기서 user.getLoginDate; 해서 받아와서
        // 현재시간 - 로그인시간 해서 => 1년을 초과하면 return false;로 하면 된다.

        return true; // 응 활성화 되어있어
    }

    //----- impelemnts OAuth2User 하면서 오버라이드 생긴 메소드 ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼

    @Override
    public Map<String, Object> getAttributes() {
        return attributes;
    }

    @Override
    public String getName() {
        return attributes.get("username").toString();
    }

    public User getUserObject() {
        return this.user;
    }
}

 

- User.java 수정 

= 수정전 token, access_token, refresh_token 셋 다 있었음

= 수정후 access_token, refresh_token 만 있음

package com.login.jwtoauth.Domain.User.Entity;

import jakarta.persistence.*;
import jdk.jfr.Timestamp;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;

import java.time.LocalDateTime;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@ToString
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String realname; // 유저의 실명
    private String username;
    private String password;
    private String email;
    private String role;
    //private String token; // 임시 컬럼
    private String provider; // 어떤 클라이언트를 통해 가입한 회원인지
    private String providerId;

    @Column(length = 2000)
    private String accessToken;
    @Column(length = 2000)
    private String refreshToken;

    @CreationTimestamp
    private LocalDateTime createdDate;

    @Builder
    private User(String realname, String username, String password, String email, String role, String provider, String providerId, String accessToken, String refreshToken) {
        this.realname = realname;
        this.username = username;
        this.password = password;
        this.email = email;
        this.role = role;
        //this.token = token;
        this.provider = provider;
        this.providerId = providerId;
        this.accessToken = accessToken;
        this.refreshToken = refreshToken;
    }
}

 


- PrinciplaOauth2UserService  (토큰 생성해서 db 저장까지)

package com.login.jwtoauth.Config.oauth;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.login.jwtoauth.Config.auth.PrincipalDetails;
import com.login.jwtoauth.Domain.User.Entity.User;
import com.login.jwtoauth.Domain.User.Repository.UserRepository;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.Date;

/**
 * 이런 시큐리티 서비스를 상속받아서 오버라이드 하는 서비스는
 * 내가 별도로 매핑하여 실행시켜줄 필요가 없는 서비스이다.
 *   ▶ 구글 로그인 클릭 시 자동으로 실행되기 때문
 */

@Service
@Slf4j
@RequiredArgsConstructor
@Transactional
public class PrincipalOauth2UserService extends DefaultOAuth2UserService {

    private final BCryptPasswordEncoder bCryptPasswordEncoder;
    private final UserRepository userRepository;

    // 구글 로그인 계정 클릭했을 때 넘어오는 계정 관련 데이터들을 후처리하는 service

    // JWT 사용 후
    private final HttpServletResponse response;
    private final HttpServletRequest request;


    // DefaultOAuth2UserService 에 있는 메소드 overriding
    @Override
    @Transactional
     //public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { // jwt 사용 전
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {

        // 로그인시 넘어오는 데이터들을 찍어보자
        //log.info(userRequest.getAccessToken().toString());
        //log.info(userRequest.getClientRegistration().toString());
        //log.info(userRequest.getAdditionalParameters().toString());
        //log.info(super.loadUser(userRequest).getAuthorities().toString());
        //log.info(super.loadUser(userRequest).getAttributes().toString());
        /**
         * 2024-04-21T19:08:16.347+09:00  INFO 14076 --- [nio-5000-exec-5] c.l.j.C.o.PrincipalOauth2UserService     : org.springframework.security.oauth2.core.OAuth2AccessToken@[고유번호]
         * 2024-04-21T19:08:16.351+09:00  INFO 14076 --- [nio-5000-exec-5] c.l.j.C.o.PrincipalOauth2UserService     : ClientRegistration{registrationId='google', clientId=[api 에 사용되었었던 클라이언트 id], clientSecret=[api 에 사용되었었던 클라이언트 secret], clientAuthenticationMethod=client_secret_basic, authorizationGrantType=org.springframework.security.oauth2.core.AuthorizationGrantType@[고유번호] redirectUri='{baseUrl}/{action}/oauth2/code/{registrationId}', scopes=[email, profile], providerDetails=org.springframework.security.oauth2.client.registration.ClientRegistration$ProviderDetails@[고유번호], clientName='Google'}
         * 2024-04-21T19:08:16.351+09:00  INFO 14076 --- [nio-5000-exec-5] c.l.j.C.o.PrincipalOauth2UserService     : {id_token= [user 관련 정보를 담고 있는 jwt 토큰]}
         * 2024-04-21T19:08:16.581+09:00  INFO 14076 --- [nio-5000-exec-5] c.l.j.C.o.PrincipalOauth2UserService     : [OAUTH2_USER, SCOPE_https://www.googleapis.com/auth/userinfo.email, SCOPE_https://www.googleapis.com/auth/userinfo.profile, SCOPE_openid]
         * 2024-04-21T19:08:16.740+09:00  INFO 14076 --- [nio-5000-exec-5] c.l.j.C.o.PrincipalOauth2UserService     : {sub=[고유번호], name=[전체이름], given_name=[이름], family_name=[성], picture=[프로필이미지주소], email=[아이디]@gmail.com, email_verified=true, locale=ko}
         */

        // 구글로그인 버튼 클릭 -> 구글로그인창 -> 로그인완료 -> code를 리턴(OAuth-Client 라이브러리) -> Access Token을 요청
        // 여기까지가 userRequest 정보 -> 회원프로필 받아야함(loadUser함수로) -> 구글로부터 회원프로필 받아준다.
        OAuth2User oAuth2User = super.loadUser(userRequest);
        log.info("oAuth2User : " + oAuth2User.getAttributes());

        // 구글로부터 넘겨받은 회원정보에서 각 attribute 들을 분리해서 정리하자
        String provider = userRequest.getClientRegistration().getRegistrationId(); // 'google'
        String providerId = oAuth2User.getAttribute("sub"); // google pk
        String username = provider + "_" + providerId; // "google_12345678" 이런 꼴 username 중복 방지
        String realname = oAuth2User.getAttribute("name");

        String email = oAuth2User.getAttribute("email");

        // 토큰 저장 이번에 새로 test 하는 거
        // 임시 토큰임 대체로 넣어 둔 거임
        //String token = userRequest.getAccessToken().toString();

         // USER, ADMIN, UNDEFINED 이렇게 3종류로 정의
        String role = "UNDEFINED"; // 나중에 관리자 계정 따로 빼서확인할 예정 OR DB로 관리자 승인 내준 사람만 사이트 들어올 수 있게 할 거임.

         // 구글로그인만 진행할 거면 따로 없어도 되는데, 추후 일반 로그인도 개발할 것이기 때문에
         // 구글로그인 회원에게는 임의의 PW를 넣어준다.
        String password = bCryptPasswordEncoder.encode("letsdodev");

        // log.info("userRequest at PrincipalOauth2UserService -> {}", userRequest);
        // log.info("oAuth2User at PrincipalOauth2UserService -> {}", oAuth2User);

        // 이제 jwt 사용하기 시작하면서 세션 대신 사용자에게 토큰 정보 넘겨줘야함.
        /**
         * jwt 토큰 만들기
         */
        // 엑세스 토큰 만들기
        String accessToken = JWT.create()
                .withSubject("joaAccessToken") // 토큰명 지정
                .withExpiresAt(new Date(System.currentTimeMillis() + 30000)) // 테스트 위해서 토큰 만료기한 30초로
                .withClaim("id", email)
                .withClaim("username", username)
                .withClaim("role", role)
                .sign(Algorithm.HMAC256("joasign")); // 내 서버만 아는 고유의 값
        // 리프레쉬 토큰 만들기
        String refreshToken = JWT.create()
                .withSubject("joaRefreshToken") // 토큰명 지정
                .withExpiresAt(new Date(System.currentTimeMillis() + 30000)) // 테스트 위해서 토큰 만료기한 30초로
                .withClaim("id", email)
                .withClaim("username", username)
                .withClaim("role", role)
                .sign(Algorithm.HMAC256("joasign")); // 내 서버만 아는 고유의 값

        log.info(accessToken);
        log.info(refreshToken);

        // 구글로부터 넘겨받은 정보를 통해 회원가입을 진행하는 로직 구성
        // todo 1. 먼저 이미 가입되어 있는 회원인지 체크
        User findUser = userRepository.findByUsername(username);
        User user = null;
        if(findUser == null) {
           user = User.builder()
                    .realname(realname)
                    .username(username)
                    .role(role)
                    .email(email)
                    .provider(provider)
                    .providerId(providerId)
                    // .token(token)
                    .accessToken("Bearer " + accessToken)
                    .refreshToken("Bearer " + refreshToken)
                    .password(password)
                    .build();
            // log.info("user Entity Obejct -> {}", user);
            userRepository.save(user);
        }

        // userEntity 가 null 이 아니면 --> 즉 이미 회원가입된 사람이라면
        // 이렇게 넣으면 PrincipalDetails에서 정의한 생성자에 의하여
        // oAuth2User의 어트리뷰트가 user타입으로 변함
        //PrincipalDetailsService와 동일하게
        // ※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※
        // PrincipalDetails(userEntity, Map<String,Objevt> attributes) 이걸 반환하는데 이렇게 되면
        // 시큐리티 sesstion에 Authentication(UserDetails)가 자동으로 들어가게 된다.
        // ※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※
        // Session(Authentication(User)) 꼴 완성
        // return super.loadUser(userRequest);

        /**
         * 여기서 반환하려고 했는데 메소드 반환타입을 마음대로 변경할 수가 없어서
         * defaultSuccessUrl 에 매핑되어 있는 컨트롤러 메소드에서 수행할 예정
         */

        // jwt 사용 전 시큐리티 세션에 사용자 정보 넘기는 거였음
        return new PrincipalDetails(user, oAuth2User.getAttributes());
    }
}

 

-  SecurityConfig.java (defaultSuccessUrl 주석처리하고 successHandler 추가)

package com.login.jwtoauth.Config;

import com.login.jwtoauth.Config.Filter.TokenFilter;
import com.login.jwtoauth.Config.oauth.PrincipalOauth2UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
import org.springframework.web.filter.CorsFilter;

@Configuration
@EnableWebSecurity // 스프링 시큐리티 필터가 스프링 필터체인에 등록이 됨.
@EnableMethodSecurity
@RequiredArgsConstructor
/**
 *  *  @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) -> 스프링 시큐리티 5.6 이전
 *  *  @EnableMethodSecurity -> 스프링 시큐리티 5.6 이후
 *  └→ 컨트롤러 매핑 메소드에
 *      @Securered,
 *      @PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')"),
 *      @PostAuthorize, 등의 어노테이션을 붙여 SecruityConfig 에서 권한체크를 미리 설정해주지 않아도
 *      컨트롤러 매핑에 바로 권한 체크를 부여하여 사용할 수 있는 방법
 *      위 어노테이션 안에서 쓸수 있는 기능에는
 *      hasRole([role])
 *      hasAnyRole([role1,role2 ...])
 *      principal
 *      authentication
 *      permitAll
 *      denyAll
 *      isAnonymous()
 *      isRememberMe()
 *      isAuthenticated()
 *      isFullyAuthenticated() 등이 있다.
 */
public class SecurityConfig {
    
    // oauth2 로그인 시 endPoint에서 사용하기 위해 빈 주입
    private final PrincipalOauth2UserService principalOauth2UserService;

    // ## JWT 사용을 위해 추가한 코드 5
    private final OAuth2SuccessHandler oAuth2SuccessHandler;
    // ## JWT 사용을 위해 추가한 코드 1
    private final CorsFilter corsFilter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                // stateless한 rest api를 개발할 것이므로 csrf 공격에 대한 옵션은 꺼둔다.
                .csrf(AbstractHttpConfigurer::disable)

                // ## JWT 사용을 위해 추가한 코드 2 : 세션을 사용하지 않겠다는 의미
                .sessionManagement((sessionManagement) -> {
                    sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
                })

                // ## JWT 사용을 위해 추가한 코드 1
                .addFilter(corsFilter)

                // ## JWT 사용을 위해 추가한 코드 4  TokenFilter가 시큐리티 필터보다 먼저 실행되게
                .addFilterBefore(new TokenFilter(), SecurityContextPersistenceFilter.class)

                // 특정 URL에 대한 권한 설정.
                .authorizeHttpRequests((authorizeRequests) -> {

                    // 어플리케이션 맨 첫 페이지는 누구나 접근 가능하게
                    authorizeRequests.requestMatchers("/index").permitAll();
                    authorizeRequests.requestMatchers("/").permitAll();

                    // spring security 6 버전에서는 ROLE_ 생략해야됨. 이전 버전에서는 생략 안 해도 되었음.
                    // .hasRole() 대신에 .hasAuthority()를 사용하게 되면 ROEL_ 를 붙여줘야 한다.
                    //authorizeRequests.requestMatchers("/service/**").authenticated(); // 어떤 권한인지 상관없이 인증된 사용자면 들어올 수 있게
                    
                    // service로 시작하는 url은 USER 권한을 가진 사람만 접근할 수 있도록 
                    authorizeRequests.requestMatchers("/service/**").hasRole("USER");
                    
                    //authorizeRequests.requestMatchers("/manager/**").hasAnyRole("ADMIN", "MANAGER");
                    //authorizeRequests.requestMatchers("/admin/**").hasRole("ADMIN");

                    // 기타 다른 url로 모두 요청 및 접근 가능하게
                    authorizeRequests.anyRequest().permitAll();
                })

                .formLogin((formLogin) -> {
                    /* 권한이 필요한 요청은 해당 url로 리다이렉트 */
                    formLogin
                            .loginPage("/index")
                            .defaultSuccessUrl("/service");
                })

                // ## JWT 사용을 위해 추가한 코드 3
                .httpBasic(HttpBasicConfigurer::disable)

                .oauth2Login((oauth2) -> oauth2
                        .loginPage("/oauth2/authorization/google") // 권한 접근 실패 시 로그인 페이지로 이동
                        //.defaultSuccessUrl("http://localhost:5000/service") // 로그인 성공 시 이동할 페이지
                        //.defaultSuccessUrl("http://localhost:5000/oauth/login") // 로그인 성공 시 이동할 페이지
                        .failureUrl("/oauth2/authorization/google")// 로그인 실패 시 이동 페이지
                        .userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint   // --> 구글로그인이 완료된 이후의 후처리 과정: 1.코드받기(인증) 2.엑세트토큰, 3.사용자프로필 정보를 가저오고
                                .userService(principalOauth2UserService))
                                .successHandler(oAuth2SuccessHandler)
                )        //-> 매개변수에 oausth2userservice 타입이 들어가야 함
                        // 후처리 과정
                        // 1. 그 정보를 토대로 회원가입을 자동으로 진행시키기도 함
                        // 2. (이메일, 전화번호, 이름, 아이디) 만 넘어오기 때문에 정보가 모자라다.
                        // ex) 쇼핑몰의 경우 집주소, 등급 등등
                        // 추가적인 정보가 요구되는 경우 추가적인 창을 띄워서 데이터를 받아야 한다.
                        // but 추가적인 정보가 필요없다면 구글이 주는 기본적인 정보로만 회원가입을 진행시켜도 된다.
                        // tip. 구글 로그인이 되면 코드를 돌려받는 게 아니라 엑세스토큰 + 사용자 프로필을 넘겨받는다.

                .build();
    }
}

 

- OAuth2SuccessHandler.java ( 토큰 받아와서 localStorage에 저장할 자바스크립트 메소드 포함한 html 생성)

package com.login.jwtoauth.Config;

import com.login.jwtoauth.Config.auth.PrincipalDetails;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.Map;


@Configuration
@Slf4j
public class OAuth2SuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        // OAuth2User oAuth2User = (OAuth2User)authentication.getPrincipal();
        // log.info("OAuth2SuccessHandler - Principal - OAuth2User = {}", oAuth2User);

        PrincipalDetails principalDetails = (PrincipalDetails)((OAuth2User)authentication.getPrincipal());

        log.info(principalDetails.getUser().toString());

        // 안정성 문제 때문에 반려
        // String accessToken =  principalDetails.getUser().getAccessToken();
        // String refreshToken = principalDetails.getUser().getRefreshToken();
        // response.sendRedirect("/tknsave?at="+accessToken+"&rt="+refreshToken);

        String username = principalDetails.getUser().getUsername();
        response.sendRedirect("/tknsave?username="+username);
    }
}

 

- tknsave.html : 위 컨트롤러에서 파라미터로 넘겨받은 username으로 엑세스 토큰 , 리프레쉬 토큰 가져와서 로컬 스토리지에 저장하는 역할 (▶ 아직 로컬 스토리지 저장 후 window.location.href로 다른 페이지 넘기는 거는 안했음)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>권한 대기 중인 사람들 들어오는 페이지</title>
</head>
<script>
    const getTokens = async (username) => {

        const url = "http://localhost:5000/get/authorization"
        const body = {
            username : username
        }

        const options = {
            method: 'POST', // 요청 메서드를 POST로 설정
            headers: {
                'Content-Type': 'application/json' // 요청 본문의 데이터 타입을 JSON으로 설정
            },
            body: JSON.stringify(body) // 데이터를 JSON 형식으로 변환하여 본문에 설정
        };

        try {
            const response = await fetch(url, options);
            if (response.ok) {
                console.log("validation OK");
                const data = await response.json(); // 응답 데이터를 JSON으로 파싱
                //console.log(data);
                return data; // 데이터 반환
            } else {
                throw new Error('Network response was not ok.');
            }
        } catch (error) {
            console.error("validation failed");
            console.error(error);
            throw error;
        }
    }
    function tknsave() {
        //let url = window.location.href;
        //console.log(url);

        // 현재 URL에서 URLSearchParams 객체 생성
        const urlParams = new URLSearchParams(window.location.search);

        // "username" 매개변수의 값 가져오기
        const username = urlParams.get('username');

       const tokensJson = getTokens(username).then(response => {
           //console.log(response);
           localStorage.setItem("joaAccessToken", response.joaAccessToken);
           localStorage.setItem("joaRefreshToken", response.joaRefreshToken);
       })
    }

</script>
<body onload="tknsave()">
</body>
</html>

 

[tknsave.html 에서 요청했던 토큰들(access,refresh) 가져오는 요청 매핑되는 컨트롤러, 서비스, RequestDto]

- UserRepository.java (수정 : 토큰 가져오는 메소드 생성)

package com.login.jwtoauth.Domain.User.Repository;

import com.login.jwtoauth.Domain.User.Entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {

    // 사용자 id로 해당 사용자 정보 User 객체로 반환
    // -> 일종의 사용자 id를 통한 사용자 정보 조회로 보면 됨.
    //Optional<User> findByUsername(String userName);
    User findByUsername(String userName);

    // username 으로  회원정보 가지고 오기 -> 엑세스토큰, 리프레쉬 토큰 가지고 오는 것
    User findAccessTokenByUsername(String username);
    User findRefreshTokenByUsername(String username);
}

 

- GetTokenRequestDto.java

package com.login.jwtoauth.Domain.Token.Dto;

import lombok.Getter;
import lombok.NoArgsConstructor;

@NoArgsConstructor
@Getter
public class GetTokenRequestDto {

    private String username;

    public GetTokenRequestDto(String username) {
        this.username = username;
    }
}

- TokenController.java 

package com.login.jwtoauth.Domain.Token.Controller;

import com.login.jwtoauth.Domain.Token.Dto.GetTokenRequestDto;
import com.login.jwtoauth.Domain.Token.Service.TokenService;
import com.login.jwtoauth.Domain.User.Repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.minidev.json.JSONObject;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequiredArgsConstructor
public class tokenController {

    private final TokenService tokenService;

    @PostMapping("/get/authorization")
    @ResponseBody
    public JSONObject getTokens(@RequestBody GetTokenRequestDto requestDto) {
        log.info("------------------------------------------------");
        log.info(tokenService.getTokens(requestDto).toString());
        return tokenService.getTokens(requestDto);
    }
}

- TokenService.java

package com.login.jwtoauth.Domain.Token.Service;

import com.login.jwtoauth.Domain.Token.Dto.GetTokenRequestDto;
import com.login.jwtoauth.Domain.User.Entity.User;
import com.login.jwtoauth.Domain.User.Repository.UserRepository;
import lombok.RequiredArgsConstructor;
import net.minidev.json.JSONObject;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestBody;

import java.util.HashMap;
import java.util.Map;

@Service
@RequiredArgsConstructor
public class TokenService {

    private final UserRepository userRepository;

    public JSONObject getTokens(@RequestBody GetTokenRequestDto getTokenRequestDto) {


        User findAccessToken =  userRepository.findAccessTokenByUsername(getTokenRequestDto.getUsername());
        User findReFreshToken = userRepository.findRefreshTokenByUsername(getTokenRequestDto.getUsername());

//        Map<String, String> tokenMap = new HashMap<>();
//        tokenMap.put("joaAccessToken", findAccessToken.getAccessToken());
//        tokenMap.put("joaAccessToken", findAccessToken.getRefreshToken());

        JSONObject jsonObject = new JSONObject();

        jsonObject.put("joaAccessToken", findAccessToken.getAccessToken());
        jsonObject.put("joaRefreshToken", findReFreshToken.getRefreshToken());
        return jsonObject;
    }
}

 

 

= tknsave.html 랜더까지 과정이 실행되면

실행결과

localStorage에 토큰이 저장된다.


게시물이 너무 길어져 1부는 여기서 마무리

access-token, refresh-token 으로 접속자 필터링 하는 거는 
다음 게시물에서 진행될 예정