워크플레이스 : 이클립스(개발환경툴)의 실행 환경. 프로젝트 폴더와 상관없다.

프로젝트 안에 워크플레이스는 만들지 말자.

 

# 개념 정리 중요

 

# REST API

STS board/read 부분 작성.

 

PUT

@PutMapping("/update")
public Board read(@RequestBody Board board) {

}

이렇게 사용할 때, postman에선 raw로 보내보자. (JSON 형식으로)

 

<update id="updateByBno" parameterType="board">

update board set btitle=#{btitle}, bcontent=#{bcontent}

where bno=#{bno}

</update>

 

attach 부분도 변경하도록 해보자.

동적 SQL을 활용!

<update id="updateByBno" parameterType="board">

update board set btitle=#{btitle}, bcontent=#{bcontent}

<if test="battachoname != null">

, battachoname=#{battachoname}

, battachtype=#{battachtype}

, battachdata=#{battachdata}

</if>

where bno=#{bno}

</update>

 

# 업데이트 메소드 완성

@PutMapping("/update")
//public Board update(@RequestBody Board board) {
public Board update(Board board) {  // 첨부가 넘어왔을 경우 @RequestBody를 빼야한다.

    if(board.getBattach() != null && !board.getBattach().isEmpty()) {
        // 첨부파일이 넘어왔을 경우 처리
        MultipartFile mf = board.getBattach();
        // 파일 이름을 설정
        board.setBattachoname(mf.getOriginalFilename());
        // 파일 종류를 설정
        board.setBattachtype(mf.getContentType());
        try {
            // 파일 데이터를 설정
            board.setBattachdata(mf.getBytes());
        } catch (IOException e) {
        }
    }

    // board 수정하기
    boardService.update(board);
    //return board; // 완전한 데이터가 들어가지 않음. 수정할 내용만 들어가있다.
    // 따라서 현재 해당 bno에 대한 board(수정한 보드)를 얻어 리턴한다.
    // 수정된 내용의 Board 객체 얻기
    board = boardService.getBoard(board.getBno());
    // JSON으로 변환되지 않는 필드는 null처리
    board.setBattach(null);
    board.setBattachdata(null);

    return board;
}

 

# 삭제 메소드

@DeleteMapping("/delete/{bno}")
public void delete(@PathVariable int bno) {
    boardService.delete(bno);
}

 

 

 

 

# 첨부 파일 다운로드

 

board.xml 의 이 부분을 이용해도 가능하다!

<select id="selectByBno" parameterType="int" resultType="board">

select bno, btitle, bcontent, mid as bwriter, bdate, bhitcount,

battachoname, battachsname, battachtype, battachdata

from board

where bno=#{bno}

</select>

 

// 첨부 파일 다운로드

@GetMapping("/battach/{bno}")

public void download(@PathVariable int bno, HttpServletResponse response) {

// 해당 게시물 가져오기

Board board = boardService.getBoard(bno);

 

// 크롬이나 사파리, 엣지도 포함해서

// 첨부 파일의 이름이 '한글'일 경우에는 문자가 깨질 수 있다.

// 파일 이름이 한글일 경우, 브라우저에서 한글 이름으로 다운로드 받기 위해 헤더에 추가할 내용

/*String fileName;

try {

// 첨부가 있는 경우에만 요청 할 수 있다. 없는 경우에는 이 요청 자체를 할 수가 없다.

fileName = new String(board.getBattachoname().getBytes("UTF-8"), "ISO-8859-1");

response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");

} catch (UnsupportedEncodingException e) {

}

 

// 파일 타입을 헤더의 Content 부분에 추가

response.setContentType(board.getBattachtype());

// 응답 바디에 파일 데이터를 출력

try {

OutputStream os = response.getOutputStream(); // 하나의 실행동작이기 때문에 위의 try구문에 넣어도 가능할 것이다.

os.write(board.getBattachdata());

os.flush();

os.close();

} catch (IOException e) {

}*/

 

try {

// 첨부가 있는 경우에만 요청 할 수 있다. 없는 경우에는 이 요청 자체를 할 수가 없다.

String fileName = new String(board.getBattachoname().getBytes("UTF-8"), "ISO-8859-1");

response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");

// 파일 타입을 헤더의 Content 부분에 추가

response.setContentType(board.getBattachtype());

OutputStream os = response.getOutputStream();

os.write(board.getBattachdata());

os.flush();

os.close();

} catch (IOException e) {

log.error(e.toString());

}

 

}

 

 

 

# Session에 대한 범위와 이 Session으로 이용할 수 있는 기능들 정리해보자.

# 이전의 프로젝트에서 Session의 ID를 따로 설정하지 않아 버그가 있었던 적이 있었다.

- MemberPage, ProductPage, BoardPage 등..

 

# 세션의 정보를 보려면?

검사(F12) - Application - Cookies에서 볼 수 있다.

우리가 저번에 배웠던 강의에서 ID를 저장했는데, Value값이 ID값이다.

 

검사(F12) - Network 부분에서 '새로고침'을 활용하여 Header의 내용에서도 볼 수 있다.

 

# 시큐리티도 세션 객체를 이용한 것이다.

브라우저당 하나의 세션 객체를 사용

ex) 브라우저의 사용자가 1만명이라면 세션도 1만개. --> 그만큼 서버의 메모리 자원이 사용됨

 

# 하나의 WAS에서 사용하는 세션을 다른 WAS에서 재사용 가능할까?

---> 못한다. 세션 아이디는 현재 서버 (WAS)에서 부여된 값(고유한 번호)이기 때문에 불가능하다.

===> '통합 인증 시스템'은 어떻게 활용하여 사용하는 것일까?

 

A(WAS), B(WAS)가 하나의 회사일 경우..

ex) 신세계 사이트 --> 이마트 사이트 이동시 로그인 상태가 유지되어 이동..

 

 

#  JWT (Json Web Token) 기반 인증 시스템을 사용하면 이러한 기능을 사용할 수 있다!

- 많이 쓰는 추세이다.

- Session 인증 기반도 아직까지 많이 사용하고 있다.

브라우저 : 사용자

Sever : 웹 애플리케이션

 

1. 브라우저에서 보내는 사용자의 정보(아이디, 비밀번호) 검증

2. 검증이 성공하면 서버는 JWT 생성

3. 서버는 브라우저에 JWT를 반환

4. (재요청시) 브라우저는 인증(Authorization이라는)값을 헤더에 넣어 JWT를 서버에 보냄

5. ???

6. 서버 측에서는 클라이언트에게 응답을 보낸다.

 

ex) 공공 데이터를 사용하려면 Key를 발급받는데, 여기서 Key값은 Token이라고 할 수 있다.

 

JWT의 구조

헤더

페이로드 : 고정된 값이 아니고, 넣고 싶은 값으로 구성할 수 있다. (토큰을 만들 때 서버측에서 값을 구성할 수 있음)

시그너처 : 암호화된 서명이 들어가는데, 서버가 발행한 토큰과 브라우저에서 서버측에 보낸 토큰을 비교할 때 사용

(즉, 자신이 발행한 서명이 들어가있는지 확인)

로 구성된다.

 

구분자('.')로 나누어 구성되어 있음.(헤더.페이로드.시그너처)

사이트를 들어가서 확인해보자.

브라우저가 켜져있는 상태에서만 살려놓으려면 브라우저의 메모리에 올려놓고,

브라우저를 껐다가 다시 켜도 상태를 살려놓고 싶다면 '로컬 스토리지'라는 파일 시스템에 저장해놓고 사용할 수 있다.

 

ex) 로그인 하고 나서 다시 브라우저를 열면 로그인을 해야하는 경우 : 네이버 사이트

 

# 로그인 사이트에서 '로그인 상태 유지'  기능 구현 생각해보자 ( 첫번째 프로젝트에 )

시간을 갖는 토큰 발급

ex) 국취지 로그인 시간 등

서버측에서 만든 토큰들을 클라이언트 측에 지급하고, 클라이언트는 자신이 갖고있는 토큰을 서버측에 전달하여 유효기간을 갖게 하거나, 유효기간을 더 늘릴 수 있게 만들 수 있다.

처음 접근 토큰 발행 (Access Token)

예를 들어 로그인 연장 토큰 발행 (Refresh Token)

 

# 리프레쉬 토큰을 굳이 사용할 필요가 없다?

엑세스 토큰의 유효기간을 충분히 길게 줄 경우.. (24시간?, 1주일, 1달, 1년 등)

굳이 발급해줄 이유가 없기 때문에 유효기간이 끝날 때마다 엑세스 토큰을 발급해주면 될 것이다.

 

# 우리가 사용할 최종 프로젝트 (코사 관리시스템)

리프레쉬 토큰을 설정해야 된다고 생각했다.

하지만 우리 과정에서 리프레쉬 토큰에 대해 배우지 않을 것이다.

그렇다면 엑세스 토큰의 유효기간 설정이 관건일 것.

(세션 스토리지, 로컬 스토리지 -- 무엇을 사용할지도 고민해야 한다.)

쿠키에 저장할 수 있지만 추천하지 않는다.. --> 쿠키에 저장해야만 하는 경우가 있을까?

 

JWT를 생성해주는 라이브러리가 있다. (헤더, 페이로드, 시그너처 생성)

시큐리티를 사용해도 되고, 사용하지 않아도 되지만 우리 강의에선 시큐리티를 사용할 것이다.

헤더에는 알고리즘과 타입이 들어간다.

 

# 토큰의 유효기간 설정

ACCESS_TOKEN_DURATION --> 계산 방법 검색해보기.

 

.compact --> 위의 내용들을 묶어줌.

 

 

# 실습해보자.

 

 

pox.xml에 디펜던시 추가

<dependency>

<groupId>io.jsonwebtoken</groupId>

<artifactId>jjwt-api</artifactId>

<version>0.12.5</version>

</dependency>

 

<dependency>

<groupId>io.jsonwebtoken</groupId>

<artifactId>jjwt-impl</artifactId>

<version>0.12.5</version>

<scope>runtime</scope>

</dependency>

 

<dependency>

<groupId>io.jsonwebtoken</groupId>

<artifactId>jjwt-jackson</artifactId>

<version>0.12.5</version>

<scope>runtime</scope>

</dependency>

 

src/main/java폴더 - com.mycompany.webapp.security 패키지 생성 - JwtProvider 클래스 생성

String Token = Jwts.builder()

.setHeaderParam("alg", "HS256")

.setHeaderParam(userId, authority)

.compact();

JJWT 버전 업데이트로 인해 사용법이 바뀜.

 

package com.mycompany.webapp.security;

import java.util.Date;

import javax.crypto.SecretKey;

import org.springframework.stereotype.Component;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.JwtParserBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class JwtProvider {
   //필드
   private String JWT_SECRET_KEY = "com.mycompany.jsonwebtoken.kosacourse";
   private long ACCESS_TOKEN_DURATION = 24*60*60*1000;
   private SecretKey secretKey;
   
   //생성자
   public JwtProvider() {
      try {
         secretKey = Keys.hmacShaKeyFor(JWT_SECRET_KEY.getBytes("UTF-8"));
      } catch (Exception e) {
         log.info(e.toString());
      } 
   }
   
   //AccessToken 생성
   public String createAccessToken(String userId, String authority) {
      String token = null;
      try {
         JwtBuilder builder = Jwts.builder();
         //header 설정
            //자동으로 설정
         
         //payload 설정
         builder.subject(userId);
         builder.claim("authority", authority);
         builder.expiration(new Date(new Date().getTime() + ACCESS_TOKEN_DURATION));
         
         //signature 설정
         builder.signWith(secretKey);
         token = builder.compact();
      } catch(Exception e) {
         log.info(e.toString());
      }
      return token;
   }
   
   public Jws<Claims> validateToken(String accessToken) {
      Jws<Claims> jws = null;
        try {
           //JWT 파서 빌더 생성
            JwtParserBuilder builder = Jwts.parser();
            //JWT 파서 빌더에 비밀키 설정
            builder.verifyWith(secretKey);
            //JWT 파서 생성
            JwtParser parser = builder.build();
            //AccessToken으로부터 payload 얻기
            jws = parser.parseSignedClaims(accessToken);
        } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
            log.info("잘못된 JWT 서명입니다.");
        } catch (ExpiredJwtException e) {
           log.info("만료된 JWT 토큰입니다.");
        } catch (UnsupportedJwtException e) {
           log.info("지원되지 않는 JWT 토큰입니다.");
        } catch (IllegalArgumentException e) {
           log.info("JWT 토큰이 잘못되었습니다.");
        }
        return jws;
    }
   
    public String getUserId(Jws<Claims> jws) {
       //Payload 얻기
        Claims claims = jws.getPayload();
        //사용자 아이디 얻기
        String userId = claims.getSubject();
        return userId;
    }
    
    public String getAuthority(Jws<Claims> jws) {
       //Payload 얻기
        Claims claims = jws.getPayload();
       //사용자 권한 얻기
       String autority = claims.get("authority").toString();
        return autority;
    }   
   
   public static final void main(String[] args) {
      JwtProvider jwtProvider = new JwtProvider();
      
      String accessToken = jwtProvider.createAccessToken("user", "ROLE_USER");
      log.info("AccessToken: " + accessToken);
      
      Jws<Claims> jws = jwtProvider.validateToken(accessToken);
      log.info("validate: " + ((jws!=null)? true : false));
      
      String userId = jwtProvider.getUserId(jws);
      log.info("userId: " + userId);
      
      String autority = jwtProvider.getAuthority(jws);
      log.info("autority: " + autority);
   }
}

 

SecretKey key = Jwts.SIG.HS256.key().build();

일회성 키 생성하는 코드인데, 이것은 사용하면 안된다. 그때그때마다 키값이 달라지기 때문이다.

 

 

 

* application.properties 파일에 코드를 추가해서 사용하자.

# JWT 비밀키 설정

jwt.security.key = com.mycompany.jsonwebtoken.kosacourse

이렇게 설정하면,

//생성자

public JwtProvider(@Value("${jwtSecurityKey}") String jwtSecurityKey) {

try {

// application.property에서 문자열 키를 읽고, SecretKey를 생성

secretKey = Keys.hmacShaKeyFor(jwtSecurityKey.getBytes("UTF-8")); // SecretKey를 얻어내고

} catch (Exception e) {

log.info(e.toString());

}

}

JwtProvider 에서 값을 주입하여 사용할 수 있다!

 

 

//payload 설정

builder.subject(userId);

builder.claim("authority", authority);

builder.expiration(new Date(new Date().getTime() + accessTokenDuration));

이렇게 페이로드 값이 들어가게된다.

 

# 최종 JwtProvider

package com.mycompany.webapp.security;

import java.util.Date;

import javax.crypto.SecretKey;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.JwtParserBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class JwtProvider {
   //필드
   private SecretKey secretKey; // 서명 및 암호화를 위한 SecuretKey 생성
   private long accessTokenDuration = 24*60*60*1000; // AccessToken의 유효 기간(단위: ms)
   
   //생성자
   public JwtProvider(@Value("${jwtSecurityKey}") String jwtSecurityKey) {
      try {
    	 // application.property에서 문자열 키를 읽고, SecretKey를 생성
         secretKey = Keys.hmacShaKeyFor(jwtSecurityKey.getBytes("UTF-8")); // SecretKey를 얻어내고
      } catch (Exception e) {
         log.info(e.toString());
      } 
   }
   
   //AccessToken 생성
   public String createAccessToken(String userId, String authority/*, String email*/) { // authority(권한)이 두 개 이상 가지고 있는 사람이 있다면 List로 받는 것이 좋을 것이다.
      String token = null;
      try {
    	 // Jwt 빌더 객체 생성
         JwtBuilder builder = Jwts.builder();
         //header 설정 (자동 설정 : 자동으로 알고리즘과 타입을 설정해줌)
         
         //payload 설정
         builder.subject(userId);
         // builder.claim("email", email);
         builder.claim("authority", authority);
         builder.expiration(new Date(new Date().getTime() + accessTokenDuration));
         //builder.expiration(new Date(new Date().getTime() + 3000));
         
         //signature 설정
         builder.signWith(secretKey);
         token = builder.compact();
      } catch(Exception e) {
         log.info(e.toString());
      }
      return token;
   }
   
   // 토큰이 유효한지 확인
   public Jws<Claims> validateToken(String accessToken) {
      Jws<Claims> jws = null;
        try {
           //JWT 파서 빌더 생성
            JwtParserBuilder builder = Jwts.parser();
            //JWT 파서 빌더에 비밀키 설정
            builder.verifyWith(secretKey);
            //JWT 파서 생성
            JwtParser parser = builder.build();
            //AccessToken으로부터 payload 얻기 (해석)
            jws = parser.parseSignedClaims(accessToken);
        } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) { // 서버가 알고있는 서명이 아닌 다른 쪽(서버)에서 받은 서명일 때,
            log.info("잘못된 JWT 서명입니다."); 
        } catch (ExpiredJwtException e) { // 유효기간 확인
           log.info("만료된 JWT 토큰입니다."); 
        } catch (UnsupportedJwtException e) {
           log.info("지원되지 않는 JWT 토큰입니다.");
        } catch (IllegalArgumentException e) { // jwt 구조 자체가 맞지 않은 경우
           log.info("JWT 토큰이 잘못되었습니다.");
        }
        return jws;
    }
   
    public String getUserId(Jws<Claims> jws) {
       //Payload 얻기
        Claims claims = jws.getPayload();
        //사용자 아이디 얻기
        String userId = claims.getSubject();
        return userId;
    }
    
    public String getAuthority(Jws<Claims> jws) {
       //Payload 얻기
        Claims claims = jws.getPayload();
       //사용자 권한 얻기
       String autority = claims.get("authority").toString();
        return autority;
    }
    
    // email 까지 확인할 경우
    /*public String getEmail(Jws<Claims> jws) {
        //Payload 얻기
         Claims claims = jws.getPayload();
        //사용자 권한 얻기
        String email = claims.get("email").toString();
         return email;
     }*/
   
   // 테스트 용도로 만든 main 메소드
   /*public static final void main(String[] args) {
      JwtProvider jwtProvider = new JwtProvider("com.mycompany.jsonwebtoken.kosacourse"); // 테스트용
      
      String accessToken = jwtProvider.createAccessToken("user", "ROLE_USER");
      log.info("AccessToken: " + accessToken);
      
      // try { Thread.sleep(2000); } catch (InterruptedException e) { }
      
      Jws<Claims> jws = jwtProvider.validateToken(accessToken);
      log.info("validate: " + ((jws!=null)? true : false));
      
      // 시간을 주고 유효기간 확인..
      // try { Thread.sleep(2000); } catch (InterruptedException e) { }
      // jws = jwtProvider.validateToken(accessToken);
      // log.info("validate: " + ((jws!=null)? true : false));
      
      if(jws != null) {
    	  String userId = jwtProvider.getUserId(jws);
    	  log.info("userId: " + userId);
    	  
    	  String autority = jwtProvider.getAuthority(jws);
    	  log.info("autority: " + autority);    	  
      }
   }*/
}

 

 

# JWT 와 Security 연결!

* Spring에서는 xml파일로 설정하였었다.

지금 배우는 SpringBoot에서는 자바코드로 작성할 것이다.

 

# security 패키지에 WebSecurityConfig 클래스 생성

 

파일은 어느 패키지에 만들든 상관은 없다.

@Configuration --> '설정' 파일이면 붙여주는 어노테이션!

 

@Slf4j

@Configuration

public class WebSecurityConfig {

@Bean

public SecurityFilterChain filterChanin(HttpSecurity http) throws Exception {

 

}

}

SecurityFilterChanin과 HttpSecurity가 서로 인식을 못하는 상태.

 

처음 의존 설정했을 때, 시큐리티를 따로 빼놓고 했었다.

우리 SpringBoot의 버전을 맞추어 Maven해주자.

<!-- Spring Security를 위한 의존 설정 -->

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-security</artifactId>

<version>2.7.18</version>

</dependency>

버전은 

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>2.7.18</version>

<relativePath/> <!-- lookup parent from repository -->

</parent>

위쪽에서 명시해주었기 때문에 시큐리티 쪽의 버전은 지워도 된다.

 

클래스로 다시 돌아와서 '컨트롤+쉬프트+O' 누르면 HttpSecurity 라이브러리가 import 된다.

 

* Spring에서 배우고 활용했던 로그인 기능과 로그아웃 기능은 SpringBoot에서는 다르게 사용한다..

SpringBoot에서는 세션을 이용하지 않는다? --> ex) 장바구니.

 

 

// CORS 설정 (다른 도메인에서 받은 인증 정보(AccessToken)로 요청할 경우 허가)

http.cors(config -> {});

클라이언트가 A쪽으로 로그인을 해서 토큰을 발급 받으면 A쪽에선 전혀 문제 X

클라이언트가 다른 B쪽으로 토큰을 들고 가면 기본적으로 시큐리티는 자기가 발행한 토큰이 아니면 차단시킨다.

이 때 차단을 하지 않고 받아들이겠다는 요청처리. (CORS 설정 --> 다른 도메인에서 요청할 경우 허가)

A쪽은 이런 설정을 할 필요 없고, B쪽에서 받아들이겠다 했을 때, B쪽에 설정을 해주어야 한다.

(A와 B는 같은 회사일 경우에 제한.)

@Bean

public CorsConfigurationSource corsConfigurationSource() {

    return null;

}

 

 

# WebSecurityConfig

package com.mycompany.webapp.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Configuration
public class WebSecurityConfig {
	// 인증 필터 체인을 관리 객체로 등록
	@Bean
	public SecurityFilterChain filterChanin(HttpSecurity http) throws Exception {
		// REST API에서 로그인 폼을 제공하지 않으므로 폼을 통한 로그인 인증을 하지 않도록 설정.
		// 로그인 폼은 front-end에서 제공해야한다.
		http.formLogin(config -> config.disable()); // 람다식으로 formLogin을 사용하지 않겠다는 의미.
		
		// REST API는 따로 로그아웃을 만들 이유가 없다
		// : 클라이언측에서 AccessToken을 갖고 인증하지 않았다면? -> 로그인을 하지 않은 상태.
		// Token으로 확인하고 서비스를 제공하면 된다.
		
		// HttpSession을 사용하지 않도록 설정
		http.sessionManagement(config -> config.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
		
		// CORS 설정 (다른 도메인에서 받은 인증 정보(AccessToken)로 요청할 경우 허가)
		http.cors(config -> {});
		
		return http.build();
	}
	
	// 인증 관리자를 관리 객체로 등록
	@Bean
	public AuthenticationManager authenticationManager(
			AuthenticationConfiguration authenticationConfiguration) throws Exception{
		return authenticationConfiguration.getAuthenticationManager();
	}
	
	// 권한 계층을 관리 객체로 등록
	@Bean
	public RoleHierarchy roleHierarchy() {
		RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
		hierarchy.setHierarchy("ROLE_ADMIN > ROLE_MANAGER > ROLE_USER");
		return hierarchy;
	}
	
	// 다른(크로스) 도메인 제한 설정 : 모든 도메인을 허용하는 것은 아니다.
	@Bean
	public CorsConfigurationSource corsConfigurationSource() {
		CorsConfiguration configuration = new CorsConfiguration();
		// 요청 사이트 제한
//		configuration.addAllowedOrigin("*"); // 모든 도메인을 허가하겠다... // 이렇게 작성하면 안된다!
		configuration.addAllowedOrigin("*"); // 교육에선 이렇게 사용할 것이다. (현재 우리는 도메인이 없다)
		
		// 요청 방식 제한 (우리가 배웠던 방식들은? : GET / POST / PUT / PATCH / DELETE
//		configuration.addAllowedMethod("GET");
//		configuration.addAllowedMethod("POST"); 
		// 아스타(*)를 사용하면 모든 방식들을 허용하겠다는 의미이다.
		configuration.addAllowedMethod("*");
		
		// 요청 헤더 제한
//		configuration.addAllowedHeader("헤더이름");
		configuration.addAllowedHeader("*"); // 현실적으로 보안상의 이유로 필요한 Header행만 받는다. // 수업에선 아스타 사용
		
		// 모든 URL에 대해 위 설정을 적용
		UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
		source.registerCorsConfiguration("/**", configuration); // "/**": 모든 URL에 대해서 configuartion을 적용
		
		return source;
	}
}

 

 

# 토큰을 발급해주었기 때문에 그 토큰을 검사하는 필터는 우리가 만들어주어야 한다!

security 패키지 안에 JwtAuthenticationFilter클래스 생성

 

* 포스트맨 확인해보자.

요청 헤더의 이름 Authorization.

Bearer로 시작하는 값 뒤에를 읽어와야 한다..

 

* JwtAuthenticationFilter 클래스로 확인

package com.mycompany.webapp.security;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

import org.springframework.stereotype.Component;
import org.springframework.web.filter.GenericFilterBean;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class JwtAuthenticationFilter extends GenericFilterBean{

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		// AccessToken 얻기
		String accessToken = null;
		
		HttpServletRequest httpServletRequest = (HttpServletRequest)request; // 이 request를 가지고 GetHeader라는 메소드를 사용할 수 없다. 따라서 타입변환을 해주는 것이다.
		String headerValue = httpServletRequest.getHeader("Authorization");
		if(headerValue != null && headerValue.startsWith("Bearer")) {
			accessToken = headerValue.substring(7);
			log.info(accessToken);
		}
		
		// 다음 필터를 실행
		chain.doFilter(request, response);
	}
	
}

필터를 이렇게 만들었다.

이제 이 필터를 어디에서 적용시켜야 할까?

--> WebSecurityConfig 에서 주입시켜 사용한다.

 


5-31 최종 소스

 

# JWT와 SECURITY

1. properties 적용

# JWT 비밀키 설정

jwt.security.key = com.mycompany.jsonwebtoken.kosacourse

 

2. JwtProvider

package com.mycompany.webapp.security;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

//	@Override
//	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
//			throws IOException, ServletException {
//		// AccessToken 얻기
//		String accessToken = null;
//		
//		HttpServletRequest httpServletRequest = (HttpServletRequest)request; // 이 request를 가지고 GetHeader라는 메소드를 사용할 수 없다. 따라서 타입변환을 해주는 것이다.
//		String headerValue = httpServletRequest.getHeader("Authorization");
//		if(headerValue != null && headerValue.startsWith("Bearer")) {
//			accessToken = headerValue.substring(7);
//			log.info(accessToken);
//		}
//		
//		// AccessToken 유효성 검사
////		Jws<Claims> jws = 
//		
//		// 다음 필터를 실행
//		chain.doFilter(request, response);
//	}
	
	@Autowired
	private JwtProvider jwtProvider;

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		// AccessToken 얻기
			String accessToken = null;
			
			HttpServletRequest httpServletRequest = (HttpServletRequest)request; // 이 request를 가지고 GetHeader라는 메소드를 사용할 수 없다. 따라서 타입변환을 해주는 것이다.
			String headerValue = httpServletRequest.getHeader("Authorization");
			if(headerValue != null && headerValue.startsWith("Bearer")) {
				accessToken = headerValue.substring(7);
				log.info(accessToken);
			}
			
			// AccessToken 유효성 검사
			Jws<Claims> jws = jwtProvider.validateToken(accessToken);
			if(jws != null) {
				// 유효한 경우
				log.info("AccessToken이 유효함");
				String userId = jwtProvider.getUserId(jws);
				log.info("userId : " + userId);
			} else {
				// 유효하지 않은 경우
				log.info("AccessToken이 유효하지 않음");
			}
			
			// 다음 필터를 실행
			filterChain.doFilter(request, response);
	}

}

 

3. WebSecurityConfig

package com.mycompany.webapp.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Configuration
public class WebSecurityConfig {
	// JwtAuthenticationFilter 주입
	@Autowired
	private JwtAuthenticationFilter jwtAuthenticationFilter;
	
	// 인증 필터 체인을 관리 객체로 등록
	@Bean
	public SecurityFilterChain filterChanin(HttpSecurity http) throws Exception {
		// REST API에서 로그인 폼을 제공하지 않으므로 폼을 통한 로그인 인증을 하지 않도록 설정.
		// 로그인 폼은 front-end에서 제공해야한다.
		http.formLogin(config -> config.disable()); // 람다식으로 formLogin을 사용하지 않겠다는 의미.
		
		// REST API는 따로 로그아웃을 만들 이유가 없다
		// : 클라이언측에서 AccessToken을 갖고 인증하지 않았다면? -> 로그인을 하지 않은 상태.
		// Token으로 확인하고 서비스를 제공하면 된다.
		
		// HttpSession을 사용하지 않도록 설정
		http.sessionManagement(config -> config.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
		
		// CORS 설정 (다른 도메인에서 받은 인증 정보(AccessToken)로 요청할 경우 허가)
		http.cors(config -> {});
		
		// JWT로 인증이 되도록 필터를 등록
		http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); // Before를 쓰는 이유 : 정리 // 아이디 패스워드를 필요로하는 필터 이전에 먼저 해주어야 함
		
		return http.build();
	}
	
	// 인증 관리자를 관리 객체로 등록
	@Bean
	public AuthenticationManager authenticationManager(
			AuthenticationConfiguration authenticationConfiguration) throws Exception{
		return authenticationConfiguration.getAuthenticationManager();
	}
	
	// 권한 계층을 관리 객체로 등록
	@Bean
	public RoleHierarchy roleHierarchy() {
		RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
		hierarchy.setHierarchy("ROLE_ADMIN > ROLE_MANAGER > ROLE_USER");
		return hierarchy;
	}
	
	// 다른(크로스) 도메인 제한 설정 : 모든 도메인을 허용하는 것은 아니다.
	@Bean
	public CorsConfigurationSource corsConfigurationSource() {
		CorsConfiguration configuration = new CorsConfiguration();
		// 요청 사이트 제한
//		configuration.addAllowedOrigin("*"); // 모든 도메인을 허가하겠다... // 이렇게 작성하면 안된다!
		configuration.addAllowedOrigin("*"); // 교육에선 이렇게 사용할 것이다. (현재 우리는 도메인이 없다)
		
		// 요청 방식 제한 (우리가 배웠던 방식들은? : GET / POST / PUT / PATCH / DELETE
//		configuration.addAllowedMethod("GET");
//		configuration.addAllowedMethod("POST"); 
		// 아스타(*)를 사용하면 모든 방식들을 허용하겠다는 의미이다.
		configuration.addAllowedMethod("*");
		
		// 요청 헤더 제한
//		configuration.addAllowedHeader("헤더이름");
		configuration.addAllowedHeader("*"); // 현실적으로 보안상의 이유로 필요한 Header행만 받는다. // 수업에선 아스타 사용
		
		// 모든 URL에 대해 위 설정을 적용
		UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
		source.registerCorsConfiguration("/**", configuration); // "/**": 모든 URL에 대해서 configuartion을 적용
		
		return source;
	}
}

 

4. JwtAuthenticationFilter

package com.mycompany.webapp.security;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

//	@Override
//	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
//			throws IOException, ServletException {
//		// AccessToken 얻기
//		String accessToken = null;
//		
//		HttpServletRequest httpServletRequest = (HttpServletRequest)request; // 이 request를 가지고 GetHeader라는 메소드를 사용할 수 없다. 따라서 타입변환을 해주는 것이다.
//		String headerValue = httpServletRequest.getHeader("Authorization");
//		if(headerValue != null && headerValue.startsWith("Bearer")) {
//			accessToken = headerValue.substring(7);
//			log.info(accessToken);
//		}
//		
//		// AccessToken 유효성 검사
////		Jws<Claims> jws = 
//		
//		// 다음 필터를 실행
//		chain.doFilter(request, response);
//	}
	
	@Autowired
	private JwtProvider jwtProvider;

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		// AccessToken 얻기
			String accessToken = null;
			
			HttpServletRequest httpServletRequest = (HttpServletRequest)request; // 이 request를 가지고 GetHeader라는 메소드를 사용할 수 없다. 따라서 타입변환을 해주는 것이다.
			String headerValue = httpServletRequest.getHeader("Authorization");
			if(headerValue != null && headerValue.startsWith("Bearer")) {
				accessToken = headerValue.substring(7);
				log.info(accessToken);
			}
			
			// AccessToken 유효성 검사
			Jws<Claims> jws = jwtProvider.validateToken(accessToken);
			if(jws != null) {
				// 유효한 경우
				log.info("AccessToken이 유효함");
				String userId = jwtProvider.getUserId(jws);
				log.info("userId : " + userId);
			} else {
				// 유효하지 않은 경우
				log.info("AccessToken이 유효하지 않음");
			}
			
			// 다음 필터를 실행
			filterChain.doFilter(request, response);
	}

}

 

+ Recent posts