Spring Boot Admin Server, Client config 설정하기

이미지
 Spring Boot로 많은 프로젝트를 진행하게 됩니다. 많은 모니터링 도구가 있지만, Spring Boot 어플리케이션을 쉽게 모니터링 할 수 있는 방법을 소개하려고 합니다.   코드 중심으로 살펴보겠습니다. 1. 어드민 서버 구축 1-1. 디펜던시 추가 dependencies { // https://mvnrepository.com/artifact/de.codecentric/spring-boot-admin-starter-server implementation 'de.codecentric:spring-boot-admin-starter-server:2.5.4' } 1-2. 어드민 서버 설정 활성화 @SpringBootApplication @EnableAdminServer public class ServerApplication { public static void main (String[] args) { SpringApplication. run (ServerApplication. class, args) ; } } EnableAdminServer를 하면 됩니다. 2. 클라이언트 서버 설정  예제는 book-client, member-client 2가지 클라이언트, member-client가 2개의 인스턴스 실행으로 작성했습니다.  2-1 디펜던시 추가 dependencies { // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web implementation 'org.springframework.boot:spring-boot-starter-web:2.5.4' // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-actuator implementation 'org.spring...

Oauth jwt example

Oauth jwt API WEB 예제 샘플 코드


https://github.com/withccm/oauth-jwt-example

API 서버와 WEB 클라이언트 oauth 로그인 인증 예제로 구성

프로젝트 구성

API

  • Oauth 토근으로 인증할 수 있는 프로젝트 구성
  • JWT 사용하여 Access Token, Refresh Token 인증 로직 구현
    • JWT(Json Web Token)는 사용자 인증을 위한 암호화된 토큰이다.
    • Session서버를 구성이 필요없어, 확장성이 있다.
    • JWT를 이용하여 Access Token, Refresh Token 각각 토큰을 생성한다.
    • Access Token은 만료 시간이 짧은 토큰, 반면 Refresh Token은 만료 시간이 길다.
    • Access Token이 탈취될 경우 보안 취약점이 발생할 수 있다. 만료시간을 짧게함으로 피해를 줄일 수 있다.(추가로 구현하면 서버에서 기존 토큰을 강제 로그아웃 시키는 기능도 가능하다.)
      Access Token이 만료되는 경우 매번 로그인을 해야하는 번거로움이 생길 수 있는데, 이를 보완하는 것이 Refresh Token이다. Refresh Token은 Access Token이 만료되 시점에 신규 토큰으로 갱신하게 해준다.
  • 다양한 Client(AOS, IOS등) 상관없이 확장 가능
  • 구글 샘플
  • 역할 샘플


Web Client

  • 구글 로그인 SDK 구현
  • Access Token 만료시 Refresh Token 호출 로직
  • 샘플 API 호출

토큰 인증 과정 UML

토큰 인증 과정 UML 이미지


access 토큰과 refresh 토큰

access 토큰은 API 요청시에 헤더에 포함하여 인증값으로 사용한다. access 토큰이 탈퇴당하는 경우 보안에 취약해진다. 이를 방지하기 위해 access 토큰의 유효시간은 짧게 제공한다.

이때 문제점이 발생한다. access 토큰이 만료 될때마다, 사용자에게 로그인을 요구한다면 사용자는 서비스 이용에 번거로움이 생긴다. 이때 사용하는 것이 refresh 토큰이다. (refresh 토큰의 만료시간은 길게 설정하는 편이다.)

서버에서 access 토큰이 만료되었다는 응답을 받았을때, refresh 토큰을 사용하여 access 토큰을 새로 갱신할 수 있다.


API 상세설명

개발 환경

  • Java 11
  • Gradle 7.1
  • Mysql

주요 자바 라이브러리

  • Spring Boot 2.5.2
  • Spring security
  • JPA
  • jjwt-api
  • Lombok

데이터베이스 접속정보

  • host : localhost
  • port : 3306
  • database : mytest
  • username : github
  • password : test1234

수정해서 사용하시면 됩니다.


서버 접속 정보

http://localhost:8080/swagger-ui.html


API 목록

  1. POST /api/v1/auth/login/google
    구글 로그인
  2. POST /api/v1/logout
    로그아웃
  3. POST /api/v1/auth/refresh
    토근 리프레시
  4. GET /api/v1/myProfile
    나의 프로필

테이블 스키마

CREATE TABLE `user` (
`userNo` bigint(20) NOT NULL AUTO_INCREMENT,
`oauthType` varchar(50) DEFAULT NULL COMMENT 'ProviderType\n구글, 페이스북 등등',
`oauthId` varchar(2555) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
`email` varchar(255) DEFAULT NULL,
`imageUrl` varchar(255) DEFAULT NULL,
`role` varchar(255) DEFAULT NULL,
PRIMARY KEY (`userNo`)
);

CREATE TABLE `userRefreshToken` (
`refreshTokenSeq` bigint(20) NOT NULL AUTO_INCREMENT,
`userNo` bigint(20) NOT NULL,
`refreshToken` varchar(300) DEFAULT NULL,
`refreshTokenExpires` datetime DEFAULT NULL,
`accessToken` varchar(300) DEFAULT NULL,
`accessTokenExpires` datetime DEFAULT NULL,
`createdDate` datetime DEFAULT NULL,
`updatedDate` datetime DEFAULT NULL,
PRIMARY KEY (`refreshTokenSeq`),
KEY `idx1` (`userNo`,`accessToken`),
KEY `idx2` (`userNo`,`refreshToken`)
);

CREATE TABLE `book` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`bookname` varchar(200) DEFAULT NULL,
PRIMARY KEY (`id`)
);

사용하기 전에 확인할 부분

기본적으로 사용가능하지만

  1. application-oauth.yml
    1. jwt.secret
      1. jwt secret key 설정
    2. app.auth 하위설정
      1. 토큰 관련 설정
      2. 토큰 시간 지정
  2. application-datasource.yml
    1. 데이터베이스 접속설정
    2. spring.datasource 하위설정
    3. url, username, password, hikari.maximum-pool-size

주요 코드

AuthToken 클래스

JWT 라이브러리를 사용해 토큰을 관리하는 AuthToken 클래스를 알아보자.

AuthToken 클래스는 JWT 라이브러리에 생성된 토큰을 쉽게 사용하도록 도와준다.

JWT API로 토큰을 생성할때 인증에 필요한 정보를 함께 암호시킨다.

전송되는데이터(Payload)에 앞에서 언급한 값이 세팅하게 되는데, 이때 key-value형식의 데이터 조각을 Claim라 부른다. 샘플에서는 사용자식별자(userNo)와 역할이 포함되어 있다.

{
"sub" : "321",
"role" : "ROLE_USER"
}

AuthTokenTest.java 파일에서 다양한 예제가 구현되어 있다.


API 공통 응답

API는 다음과 같은 형식으로 응답이 이루어진다.

public class ApiResponse<T> {

private final int code;
private final String message;
private final T data;
    ...
}

오류 코드 정의

public enum ApiResponseCode {
...
INVALID_ACCESS_TOKEN(30001, "Invalid access token."),
INVALID_REFRESH_TOKEN(30002, "Invalid refresh token."),
NOT_EXPIRED_TOKEN_YET(30003, "Not expired token yet."),
   ...
}

API 오류 처리

GlobalAPIExceptionAdvice 클래스가 예외처리를 담당한다.

Spring Security 설정

  • RestAuthenticationEntryPoint.java
    • 인증 실패(토큰 만료) 응답을 API 에러로 변경
  • TokenAccessDeniedHandler.java
    • 인가 실패(역할/권한없음) 응답 처리
  • AuthTokenProvider.java
    • 토큰 생성 역할
  • AuthenticationFactory.java
    • 토큰에서 사용자 정보 추출
  • TokenAuthenticationFilter.java
    • 토큰 유효성 확인

CORS 설정

교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)는 추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제이다. - MDN


브라우저에서는 기본적으로 같은 출처(Origin)의 자원만 요청을 보안의 이유로 허용하고 있다. 출처은 scheme, host, port 으로 구성되어 있다. 같은 출처는 3가지 요소가 같은 경우를 말한다.

http:// -> scheme
localhost -> host
:8080 -> port

예제는 편의상 모든 출처 허용으로 설정되어 있고 본인의 서비스에 맞게 설정하길 바란다.

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") .allowedOriginPatterns("*").allowedMethods("*").allowCredentials(true);
}
}


WEB 상세 설명

실행 방법

npm start or yarn start

http://localhost:3000에서 확인 가능합니다.


환경설정

.env 파일

  • REACT_APP_API_URL : API 서버 도메인 정의

Oauth 로그인 설정

각각 프로바이더에 맞게 설정하면된다.


구글

사용자 인증 정보를 사용하기 위해 등록을 해야한다.

  1. 구글 클라우드 접속
  2. 프로젝트 만들기
    프로젝트 만들기 이미지
  3. 왼쪽 사용자 인증 정보 선택
  4. 오른쪽 CREATE CREDENTIALS 선택
    사용자 인증 정보 이미지
  5. OAuth 클라이언트 ID 선택
    OAuth 클라이언트 ID 선택 이미지
  6. OAuth 동의화면 생성
    (OAuth 동의화면이 이미 만들어졌다면 이 부분은 스킵해도 됩니다.)
        6-1. 동의 화면 구성 선택
    동의화면 만들기 이미지
        6-2. 외부 선택하고 만들기
    외부 선택 이미지
        6-3. 앱정보 입력 (필수값만 입력하면 됩니다.)
    앱정보 입력 이미지

  7. 웹 애플리케이션 선택
    구현하고자 하는 유형을 선택해주시면 됩니다. 예시는 웹으로 되어 있기 때문에 웹 애플리케이션을 선택했습니다.
    웹 애플리케이션 선택 이미지
  8. 웹 > 요청할 URL 추가
    http://localhost:3000 리액트 프로젝트 추가
    요청할 URL 추가 이미지
  9. 우측에 있는 클라이언트 ID를 사용하면됩니다.
    클라이언트 ID 이미지


리액트

  1. 라이브러리 설치
    npm install react-google-login
    
    1. clientId 정의
      const googleClientId = "구글 클라이언트 ID 설정은 readme 참고";
    2.  
    3. 로그인 버튼 정의
      <GoogleLogin
      clientId={googleClientId}
      responseType={"id_token"}
      onSuccess={onSuccessGoogle}
      onFailure={onFailureGoogle}/>
    4. 성공/실패 함수 구현
      const onSuccessGoogle = async(response) => {
      alert('로그인에 성공했습니다.')
      }

      const onFailureGoogle = (error) => {
      alert('로그인에 실패했습니다.')
      }


    주요 코드

    로그인 페이지 Login.jsx

    로그인 순서

    1. 프로바이더(예로 구글) OAuth 로그인
      (OAuth 토큰을 받은 상태)
    2. OAuth 로그인 성공시 해당 토큰으로 API서버에 로그인시도
      const onSuccessGoogle = async(response) => {
      const loginRes = await login(response.tokenId, 'google')
      if (loginRes.error) {
      alert('로그인에 실패했습니다.')
      } else {
      window.location.replace('//' + window.location.host + params.redirect_uri)
      }
      }
      구글 로그인 성공시 response.tokenId에서 토큰을 획득한다.
      해당 토큰으로 API에 로그인 요청한다.
    3. 성공시 Access Token과 Refresh Token 저장
      export const login = async (tokenId, type) => {
      return await axios.post('/api/v1/auth/login/' + type, {
      accessToken : tokenId
      }, {
      }).then(response => {
      AuthUtils.setToken(response.data.data)
      return response
      }).catch(error => {
      console.log(error)
      return error.response.data
      })
      }
      API에 로그인 성공시 AuthUtils.setToken 함수로 인증값(Access Token과 Refresh Token)을 저장한다.
    4. redirect_uri로 이동, 없는경우 /로 이동

    인증 유틸리티 AuthUtils.js

    토큰 관리를 도와준다. 

    예제 코드로 편의상 localStorage에 저장했는데, 실제 운영환경에서는  Secure Cookie와 HTTP Only를 사용하여 저장하기를 바랍니다.


    API 유틸리티 ApiUtils.js

    API 요청 편하게 할 수 있도록 기본 설정이 되어 있다.

    1. 인증(Authorization) 전달
    2. 인증필요 API 요청시, 로그인 페이지로 이동
    3. 인증 만료시 토큰 리프레시후 재요청

    예시>

    apiIClient.get('/api/v1/myProfile').then(response => {
    console.log(response)
    }).catch(error => {
    console.log('error', error)
    })

    axios 인터셉터

    // 요청 인터셉터 추가
    axios.interceptors.request.use(
    (config) => {
    // 요청을 보내기 전에 호출
    return config;
    },
    (error) => {
    // 오류 요청을 보내기전 호출
    return Promise.reject(error);
    });

    // 응답 인터셉터 추가
    axios.interceptors.response.use(
    (response) => {
    // 응답 보내기 전에 호출
    return response;
    },
    (error) => {
    // 오류 응답 보내기 전에 호출
    return Promise.reject(error);
    });

    공통으로 처리될 부분은 interceptors를 정의한다. ex> 인증, 오류 처리

    client.interceptors.response.use(
    (response) => {
    return response.data;
    },
    async (error) => {
    if (!error.response) { // timeout
    return Promise.reject(error);
    }

    if (error.response.status === 401) {
    if (AuthUtils.getToken() && AuthUtils.getRefreshToken()) {
    const refreshTokenRes = await refreshToken()
    if (refreshTokenRes.error) {
    // console.log('토큰 리프레시 실패')
    } else {
    AuthUtils.setToken(refreshTokenRes.data.data)
    return client.request(error.config)
    }
    }

    alert('로그인이 필요합니다.')
    goLogin()
    }

    return Promise.reject(error);
    }
    );






    댓글

    이 블로그의 인기 게시물

    Spring boot redis cache config

    Spring boot redis RedisTemplate

    MySQL PK 중복 (duplicate key ) 해결 방법