K-Digital Training/내일배움캠프

숙련 1주차/Spring

hoonssss 2023. 11. 21. 10:28
반응형
SMALL

초기설정 -> web, thymeleaf, lombok -> project 생성

    // Security
    implementation 'org.springframework.boot:spring-boot-starter-security'
 

Bean 수동등록

기술적인 문제나 공통적인 관심사를 처리할 때 사용하는 객체들을 수동으로 등록하는 것

비즈니스 로직 Bean 보다는 그 수가 적기 때문에 수동으로 등록하기 부담스럽지 않음

수동등록된 Bean에서 문제가 발생했을 때 해당 위치를 파악하기 쉽다는 장점이 있음

@Configuration
public class PasswordConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); //hash함수
    }
}
 

method에 @Bean 등록 class에 @Configuration 등록

passwordEncoderTestcode

package com.sparta.springauth;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.password.PasswordEncoder;

@SpringBootTest
public class PasswordEncoderTest {

    @Autowired
    PasswordEncoder passwordEncoder;

    public PasswordEncoderTest(PasswordEncoder passwordEncoder){
        this.passwordEncoder = passwordEncoder;
    }

    @Test
    @DisplayName("수동 등록한 passwordEncoder를 주입 받아와 문자열 암호화")
    void test1(){
        String password = "Robbi's password";

        String encodePassword = passwordEncoder.encode(password);
        System.out.println("encodePassword = " + encodePassword);

        String inputPassword = "Robbie"; //encodePassword와 일치하지 않은 값을 대입

        boolean matches = passwordEncoder.matches(inputPassword,encodePassword); //첫번쨰는 평문, 두번째는 암호화한 코드
        System.out.println("matches = " + matches);
    }
}
//
encodePassword = $2a$10$4uYpMhnt8OsjK3kdjYC8W.QzG7X78KWwlEfJhddeEazIIuWDab2/u //암호화완료
matches = false

 

PasswordConfig클래스 중 PasswordEncoder @Bean등록하여 PasswordEncoderTest 에서 사용

 

Bean이 여러개 있을 때

첫번째방법

 
사진 삭제

사진 설명을 입력하세요.

Bean이 여러개 있으니 선택해줘야함

 
사진 삭제

사진 설명을 입력하세요.

선택하면 됨

Autowird는 Bean타입으로 DI를 지원하는데 연결이 되지않으면 Bean의 이름으로 찾음

 

두번째방법

@Primary를 주입하면 Bean이 여러개 있더라도 Primary Bean을 우선적으로 주입함

 
사진 삭제

사진 설명을 입력하세요.

세번째방법

 
사진 삭제

사진 설명을 입력하세요.

@Qulifier 주입

 
사진 삭제

사진 설명을 입력하세요.

pizza Bean을 @Qualifier을 통해서 주입함

@Qualifier이 @Primary보다 우선순위가 높음(좁은범위가 우선적으로 잡힘)

 

인증과 인가

인증

유저가 실제 유저인지 인증하는 개념

인가

해당 유저가 특정 리소스 접근이 가능한지 허가를 확인하는 개념

 

HTTP프로토콜을 이용해서 통신을 함, 이 통신은 비연결성 무상태로 이루어져 있음

비연결성 - 서버와 클라이언트가 연결이 되어 있지 않다.(리소스를 절약하기 위해서)

무상태 - 서버가 클라이언트의 상태를 저장하고 있지 않다.

 

웹 애플리케이션은 두가지 방법을 통해서 인증을 처리함

쿠키-세션 방식의 인증

쿠키 - 저장 공간

세션 - 인증 정보

 
사진 삭제

사진 설명을 입력하세요.

 

JWT 기반 인증

JWT(JSON Web Token)란 인증에 필요한 정보들을 암호화시킨 토큰을 의미

 
사진 삭제

사진 설명을 입력하세요.

쿠키-세션인증방식과 JWT방식의 다른점은 세션저장소 차이

 

쿠키와 세션

HTTP에 상태 정보를 유지하기 위해 사용

쿠키와 세션을 통해 서버에서는 클라이언트 별로 인증 및 인가를 할 수 있음

쿠키

클라이언트에 저장될 목적으로 생성한 작은 정보를 담은 파일

개발자도구 -> Application에서 쿠키와 세션 확인 가능

구성요소

  • Name (이름): 쿠키를 구별하는 데 사용되는 키 (중복될 수 없음)
  • Value (값): 쿠키의 값
  • Domain (도메인): 쿠키가 저장된 도메인
  • Path (경로): 쿠키가 사용되는 경로
  • Expires (만료기한): 쿠키의 만료기한 (만료기한 지나면 삭제됩니다.)

세션

  • 서버에서 일정시간 동안 클라이언트 상태를 유지하기 위해 사용
  • 서버에서 클라이언트 별로 유일무이한 '세션 ID' 를 부여한 후 클라이언트 별 필요한 정보를 서버에 저장
  • 서버에서 생성한 '세션 ID'는 클라이언트의 쿠키값('세션 쿠키' 라고 부름)으로 저장되어 클라이언트 식별에 사용
 
사진 삭제

사진 설명을 입력하세요.

쿠키 생성 조회

package com.sparta.springauth.auth;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

@RestController
@RequestMapping("/api")
public class AuthController {

    public static final String AUTHORIZATION_HEADER = "Authorization";

    @GetMapping("/create-cookie")
    public String createCookie(HttpServletResponse res) {
        addCookie("Robbie Auth", res);

        return "createCookie";
    }

    @GetMapping("/get-cookie")
    public String getCookie(@CookieValue(AUTHORIZATION_HEADER) String value) { 
   //@CookieValue("Authorization") 값을 value에 할당 
        System.out.println("value = " + value);

        return "getCookie : " + value;
    }

    public static void addCookie(String cookieValue, HttpServletResponse res) {
        try {
            cookieValue = URLEncoder.encode(cookieValue, "utf-8").replaceAll("\\+", "%20");
            // Cookie Value 에는 공백이 불가능해서 encoding 진행

            Cookie cookie = new Cookie(AUTHORIZATION_HEADER, cookieValue); // Name-Value
            cookie.setPath("/"); //path
            cookie.setMaxAge(30 * 60); //만료기한

            // Response 객체에 Cookie 추가
            res.addCookie(cookie);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e.getMessage());
        }
    }
}
 

HttpServletResponse에 들어있는 쿠키 중 Authorization이라는 이름으로 된 쿠키를 @CookieValue을 통해서 가져옴

쿠키를 생성할 때 name - value형식으로 cookieValue를 만들었음 Authorization이라는 name의 RobbieAuth value를 반환함

 
사진 삭제

사진 설명을 입력하세요.

 
사진 삭제

사진 설명을 입력하세요.

 
사진 삭제

사진 설명을 입력하세요.

get cookie -> Authorization=value로 값이 잘 들어옴

set cookie -> Response에서 보면 설정 값 나옴

 

세션 생성 조회

@GetMapping("/create-session")
public String createSession(HttpServletRequest req) {
    // 세션이 존재할 경우 세션 반환, 없을 경우 새로운 세션을 생성한 후 반환
    HttpSession session = req.getSession(true);

    // 세션에 저장될 정보 Name - Value 를 추가합니다.
    session.setAttribute(AUTHORIZATION_HEADER, "Robbie Auth");

    return "createSession";
}

@GetMapping("/get-session")
public String getSession(HttpServletRequest req) {
    // 세션이 존재할 경우 세션 반환, 없을 경우 null 반환
    HttpSession session = req.getSession(false);

    String value = (String) session.getAttribute(AUTHORIZATION_HEADER); // 가져온 세션에 저장된 Value 를 Name 을 사용하여 가져옵니다.
    System.out.println("value = " + value);

    return "getSession : " + value;
}
 

 

JWT

JSON 포맷을 이용하여 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token

즉, 토큰의 한 종류라고 생각하면됨 . 일반적으로 쿠키 저장소를 사용하여 JWT를 저장

서버가 1대인 경우

 
사진 삭제

사진 설명을 입력하세요.

Session1이 모든 Clinet 로그인 정보를 가지고 있음

서버가 2대 이상인 경우

 
사진 삭제

사진 설명을 입력하세요.

Session마다 다른 Clinet로그인 정보를 가지고 있을 수 있음

만약 Client1의 로그인 정보를 가지고 있지 않은 Server에 Api요청을 할 수도 있음

해결방법

1.Sticky Session : Client 마다 요청 Servere 고정

2.세션 저장소 생성하여 모든 세션을 저장

 
사진 삭제

사진 설명을 입력하세요.

Session Storage가 모든 Client의 정보를 소유하고 있기 때문에 모든 Server에서 Client의 API요청 처리 가능

3.JWT사용

로그인 정보를 Server 에 저장하지 않고, Client 에 로그인 정보를 JWT 로 암호화하여 저장 → JWT 통해 인증/인가

 
사진 삭제

사진 설명을 입력하세요.

모든 Server에서 동일한 Secret Key를 소유해야함

 
사진 삭제

사진 설명을 입력하세요.

로그인 암호화할때, JWT를 검증할때 Secret Key사용

JWT 장/단점

장점

동시 접속자가 많을 때 서버 측 부하 낮춤

Client, Sever 가 다른 도메인을 사용할 때

예) 카카오 OAuth2 로그인 시 JWT Token 사용

단점

구현의 복잡도 증가

JWT 에 담는 내용이 커질 수록 네트워크 비용 증가 (클라이언트 → 서버)

기 생성된 JWT 를 일부만 만료시킬 방법이 없음

Secret key 유출 시 JWT 조작 가능(JWT에는 민감한 정보를 넣어두면 안됨)

 

JWT흐름

1.Client가 id, password로 로그인 성공 시

서버에서 로그인정보 -> SecretKey -> JWT암호화

서버에서 직접 쿠키를 생성해 JWT를 담아 Client 응답에 전달(쿠키(JWT) -> Client)

브라우저 쿠키 저장소에 자동으로 JWT저장

2.Client에서 JWT 통해 인증 방법

서버에서 API요청 시 쿠키에 포함된 JWT를 찾아서 사용

- 쿠키를 찾는 코드

// HttpServletRequest 에서 Cookie Value : JWT 가져오기
public String getTokenFromRequest(HttpServletRequest req) {
    Cookie[] cookies = req.getCookies();
    if(cookies != null) {
        for (Cookie cookie : cookies) {
            if (cookie.getName().equals(AUTHORIZATION_HEADER)) {
                try {
                    return URLDecoder.decode(cookie.getValue(), "UTF-8"); 
                    // Encode 되어 넘어간 Value 다시 Decode
                } catch (UnsupportedEncodingException e) {
                    return null;
                }
            }
        }
    }
    return null;
}
//쿠키에 담긴 정보가 여러 개일 수 있기 때문에 그 중 이름이 JWT가 담긴 쿠키의 이름과 동일한지 확인하여 JWT를 가져옴
 

Server

Client가 전달한 JWT 위조 여부 검증(Select Key사용)

JWT유효시간이 지나지 않았는지 검증

검증 성공시 JWT에서 사용자 정보를 가져와 확인

 

JWT구조

JWT 는 누구나 평문으로 복호화 가능

하지만 Secret Key 가 없으면 JWT 수정 불가능

→ 결국 JWT 는 Read only 데이터

jwt.io

이미지 썸네일 삭제
JWT.IO

JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.

jwt.io

Payload에 실제 유저의 정보가 들어있고, HEADER와 VERIFY SIGNATURE부분은 암호화 관련된 정보 양식이라고 생각

 

JWT다루기

build.gradle에 라이브러리 추가

    // JWT
    compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
    
 

application.properties에 secretKey 추가

Base64로 Encording한 것, 사용할 때는 Base64로 Decording해야함

jwt.secret.key=7Iqk7YyM66W07YOA7L2U65Sp7YG065+9U3ByaW5n6rCV7J2Y7Yqc7YSw7LWc7JuQ67mI7J6F64uI64ukLg==
 

JwtUtil

Util클래스란 특정 파라미터에 대한 작업을 수행하는 메서드들이 존재하는 클래스

다른 객체에 의존하지 않고 하나의 모듈로서 동작하는 클래스

JWT관련 기능들을 가진 JwtUtil이라는 클래스를 만들어 JWT관련 기능을 수행

JWT 관련 기능

1. JWT 생성

2. 생성된 JWT를 Cookie에 저장

3. Cookie에 들어있던 JWT 토큰을 Substring

4. JWT 검증

5. JWT에서 사용자 정보 가져오기

 

토큰 생성에 필요한 JWT데이터

package com.sparta.springauth.jwt;

import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Base64;

@Component
public class JwtUtil {
    // Header KEY 값
    public static final String AUTHORIZATION_HEADER = "Authorization";
    // 사용자 권한 값의 KEY
    public static final String AUTHORIZATION_KEY = "auth";
    // Token 식별자, Token앞에 붙임 구분을 위해 공백추가(규칙같은 거)
    public static final String BEARER_PREFIX = "Bearer ";
    // 토큰 만료시간
    private final long TOKEN_TIME = 60 * 60 * 1000L; // 60분

    @Value("${jwt.secret.key}") //@value($"{}") -> application.properties Base64 Encode 한 SecretKey
    private String secretKey;
    private Key key; //JWT암호화 복호화할때 사용하기 위함
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    // 로그 설정(Logging(App동작동안 프로젝트의 상태나 동작 정보를 시간순으로 기록함을 의미)을 위해)
    public static final Logger logger = LoggerFactory.getLogger("JWT 관련 로그");
    //@SL4J로 Logging설정도 가능

    @PostConstruct//딱 한번만 받아오면 되는 값을 사용할 때마다 요청을 새로 호출하는 실수를 방지할 때 사용
    public void init() {
        byte[] bytes = Base64.getDecoder().decode(secretKey); //decode
        key = Keys.hmacShaKeyFor(bytes); //변환 후 key값에 SecretKey가 담김
    }
}
 

권한을 관리하는 InnerClass생성

Entity 패키지 -> Eurm 생성

package com.sparta.springauth.Entity;

public enum UserRoleEnum {
    USER(Authority.USER),  // 사용자 권한
    ADMIN(Authority.ADMIN);  // 관리자 권한

    private final String authority;

    UserRoleEnum(String authority) {
        this.authority = authority;
    }

    public String getAuthority() {
        return this.authority;
    }

    public static class Authority {
        public static final String USER = "ROLE_USER";
        public static final String ADMIN = "ROLE_ADMIN";
    }
}
 

토큰생성

    //JWT 생성(토큰생성)
    public String createToken(String username, UserRoleEnum role) {
        Date date = new Date();

        return BEARER_PREFIX +
                Jwts.builder()
                        .setSubject(username) // 사용자 식별자값(ID)
                        .claim(AUTHORIZATION_KEY, role) // 사용자 권한
                        .setExpiration(new Date(date.getTime() + TOKEN_TIME)) // 만료시간(현재시간 + 설정시간)
                        .setIssuedAt(date) // 발급일
                        .signWith(key, signatureAlgorithm) // 암호화 알고리즘
                        .compact();
    }
 

생성된 JWT를 Cookie에 저장

    // JWT Cookie 에 저장
    public void addJwtToCookie(String token, HttpServletResponse res) {
        try {
            token = URLEncoder.encode(token, "utf-8").replaceAll("\\+", "%20"); // Cookie Value 에는 공백이 불가능해서 encoding 진행

            Cookie cookie = new Cookie(AUTHORIZATION_HEADER, token); // Name-Value
            cookie.setPath("/");

            // Response 객체에 Cookie 추가
            res.addCookie(cookie);
        } catch (UnsupportedEncodingException e) {
            logger.error(e.getMessage());
        }
    }
 

Cookie에 들어있던 JWT토큰을 substring

    
    // JWT 토큰 substring
    public String substringToken(String tokenValue) {
        if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) { //공백,null이 아닌지 && BEARER_PREFIX로 시작하는지
            return tokenValue.substring(7); //BEARER_PREFIX = "BEARER " 공백 까지 7자 제외
        }
        logger.error("Not Found Token");
        throw new NullPointerException("Not Found Token");
    }
 

JWT검증

    // 토큰 검증
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); //위변조 만료 검증
            return true;
        } catch (SecurityException | MalformedJwtException | SignatureException e) {
            logger.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
        } catch (ExpiredJwtException e) {
            logger.error("Expired JWT token, 만료된 JWT token 입니다.");
        } catch (UnsupportedJwtException e) {
            logger.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
        } catch (IllegalArgumentException e) {
            logger.error("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
        }
        return false;
    }
 

JWT에서 사용자 정보 가져오기

// 토큰에서 사용자 정보 가져오기
public Claims getUserInfoFromToken(String token) {
    return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
 

테스트

    @GetMapping("/create-jwt")
    public String createJwt(HttpServletResponse res) {
        // Jwt 생성
        String token = jwtUtil.createToken("Robbie", UserRoleEnum.USER);

        // Jwt 쿠키 저장
        jwtUtil.addJwtToCookie(token, res);

        return "createJwt : " + token;
    }

    @GetMapping("/get-jwt")
    public String getJwt(@CookieValue(JwtUtil.AUTHORIZATION_HEADER) String tokenValue) {
        // JWT 토큰 substring
        String token = jwtUtil.substringToken(tokenValue);

        // 토큰 검증
        if(!jwtUtil.validateToken(token)){
            throw new IllegalArgumentException("Token Error");
        }

        // 토큰에서 사용자 정보 가져오기
        Claims info = jwtUtil.getUserInfoFromToken(token);
        // 사용자 username
        String username = info.getSubject();
        System.out.println("username = " + username);
        // 사용자 권한
        String authority = (String) info.get(JwtUtil.AUTHORIZATION_KEY);
        System.out.println("authority = " + authority);

        return "getJwt : " + username + ", " + authority;
    }
 
 
사진 삭제

사진 설명을 입력하세요.

JWT코드 jwt.io에서 json형태로 변환가능

 

회원가입 기능 구현

초기설정

DB랑 연결하기 위해 MySQL, JPA 라이브러리 추가

    // JPA
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    // MySQL
    runtimeOnly 'com.mysql:mysql-connector-j'
 
spring.datasource.url=jdbc:mysql://localhost:3306/auth
spring.datasource.username=root
spring.datasource.password=비밀번호
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

spring.jpa.hibernate.ddl-auto=update

spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
 
  • 0열 선택0열 다음에 열 추가
  • 1열 선택1열 다음에 열 추가
  • 2열 선택2열 다음에 열 추가
  • 3열 선택3열 다음에 열 추가
  • 0행 선택0행 다음에 행 추가
  • 1행 선택1행 다음에 행 추가
  • 2행 선택2행 다음에 행 추가
  • 3행 선택3행 다음에 행 추가
  • 4행 선택4행 다음에 행 추가
  • 5행 선택5행 다음에 행 추가
셀 전체 선택
열 너비 조절
행 높이 조절
컬럼명
컬럼타입
중복허용
설명
id
Long
X
테이블 ID(PK)
username
String
X
회원 ID
password
String
O
패스워드
email
String
X
이메일 주소
role
String
O
역할
1)사용자 USER
2)관리자 ADMIN
  • 셀 병합
  • 행 분할
  • 열 분할
  • 너비 맞춤
  • 삭제

user -> 암호화 -> DB저장

암호화는 양방향과 단방향이 있음

양방향 암호 알고리즘

암호화: 평문 → (암호화 알고리즘) → 암호문

복호화: 암호문 → (암호화 알고리즘) → 평문

단방향 암호 알고리즘

암호화: 평문 → (암호화 알고리즘) → 암호문

복호화: 불가 (암호문 → (암호화 알고리즘) → 평문)

Password확인 절차

사용자가 로그인을 위해 아이디 패스워드(평문) 으로 입력

DB에 저장된 아이디 패스워드(암호문)과 일치 여부 확인

// 사용예시
// 비밀번호 확인
if(!passwordEncoder.matches("사용자가 입력한 비밀번호", "저장된 비밀번호")) {
		   throw new IllegalAccessError("비밀번호가 일치하지 않습니다.");
 }
 

boolean matches(CharSequence rawPassword, String encodedPassword);

rawPassword : 사용자가 입력한 비밀번호

encodedPassword : 암호화되어 DB 에 저장된 비밀번호

회원가입 API설계

  • 0열 선택0열 다음에 열 추가
  • 1열 선택1열 다음에 열 추가
  • 2열 선택2열 다음에 열 추가
  • 0행 선택0행 다음에 행 추가
  • 1행 선택1행 다음에 행 추가
  • 2행 선택2행 다음에 행 추가
셀 전체 선택
열 너비 조절
행 높이 조절
Name
Method
URL
회원가입 페이지
GET
/api/user/signup
회원가입
POST
/api/user/signup
  • 셀 병합
  • 행 분할
  • 열 분할
  • 너비 맞춤
  • 삭제

 

로그인 구현

로그인API설계

  • 0열 선택0열 다음에 열 추가
  • 1열 선택1열 다음에 열 추가
  • 2열 선택2열 다음에 열 추가
  • 3열 선택3열 다음에 열 추가
  • 0행 선택0행 다음에 행 추가
  • 1행 선택1행 다음에 행 추가
  • 2행 선택2행 다음에 행 추가
셀 전체 선택
열 너비 조절
행 높이 조절
Name
Method
URL
설명
로그인 페이지
Get
/api/user/login-page
회원가입 페이지
로그인
POST
/api/user/login
회원가입
  • 셀 병합
  • 행 분할
  • 열 분할
  • 너비 맞춤
  • 삭제

Filter

 
사진 삭제

사진 설명을 입력하세요.

Filter란 Web 애플리케이션에서 관리되는 영역으로 Client로 부터 오는 요청과 응답에 대해 최초/최종 단계의 위치이며 이를 통해 요청과 응답의 정보를 변경하거나 부가적인 기능을 추가할 수 있음

주로 범용적으로 처리해야하는 작업들, ex) 로깅, 보안 처리에 활용

또한 인증, 인가와 관련된 로직들을 처리할 수도 있음

Filter를 사용하면 인증, 인가와 관련된 로직을 비즈니스 로직과 분리하여 관리할 수 있다는 장점이 있음

 
사진 삭제

사진 설명을 입력하세요.

Filter는 한개만 존재하는 것이 아니라 이렇게 여러개가 Chain형식으로 묶여서 처리될 수 있음

 

Request URL Logging

package com.sparta.springauth.filter;

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Slf4j(topic = "LoggingFilter") //name 명시해준것
@Component
@Order(1)//Filter 순서
public class LoggingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 전처리, 요청을 먼저 수행
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String url = httpServletRequest.getRequestURI(); //URI정보 가져옴
        log.info(url); //요청 로그 확인

        chain.doFilter(request, response); // 다음 Filter 로 이동 FilterChain을 받아온 이유가 Filter 이동하기위해

        // 후처리
        log.info("비즈니스 로직 완료");
    }
}
 

AuthFilter : 인증 및 인가 처리 필터

package com.sparta.springauth.filter;

import com.sparta.springauth.entity.User;
import com.sparta.springauth.jwt.JwtUtil;
import com.sparta.springauth.repository.UserRepository;
import io.jsonwebtoken.Claims;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.io.IOException;

@Slf4j(topic = "AuthFilter")
@Component
@Order(2)
public class AuthFilter implements Filter {

    private final UserRepository userRepository;
    private final JwtUtil jwtUtil;

    public AuthFilter(UserRepository userRepository, JwtUtil jwtUtil) {
        this.userRepository = userRepository;
        this.jwtUtil = jwtUtil;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String url = httpServletRequest.getRequestURI();
        //StringUtils.hasText -> 문자 유효성 검사(null, 공백 등)
        if (StringUtils.hasText(url) && (url.startsWith("/api/user") || url.startsWith("/css") || url.startsWith("/js"))) {
            log.info("인증처리를 하지 않은 url : " + url);
            // 회원가입, 로그인 관련 API 는 인증 필요없이 요청 진행
            chain.doFilter(request, response); // 다음 Filter 로 이동
        } else {
            // 나머지 API 요청은 인증 처리 진행
            // 토큰 확인(JWT)
            String tokenValue = jwtUtil.getTokenFromRequest(httpServletRequest);

            if (StringUtils.hasText(tokenValue)) { // 토큰이 존재하면 검증 시작
                // JWT 토큰 substring
                String token = jwtUtil.substringToken(tokenValue);

                // 토큰 검증
                if (!jwtUtil.validateToken(token)) {
                    throw new IllegalArgumentException("Token Error");
                }

                // 토큰에서 사용자 정보 가져오기
                Claims info = jwtUtil.getUserInfoFromToken(token);

                User user = userRepository.findByUsername(info.getSubject()).orElseThrow(() ->
                        new NullPointerException("Not Found User")
                );//해당 유저 있는지 검증(Select)

                request.setAttribute("user", user);//user라는 name으로 user를 가져올 수 있음
                chain.doFilter(request, response); // 다음 Filter 로 이동 -> ProductController로 감 ->LogginFilter로 감
            } else {
                throw new IllegalArgumentException("Not Found Token");
            }
        }
    }

}
 

JWT가져오기

    // HttpServletRequest 에서 Cookie Value : JWT 가져오기
    public String getTokenFromRequest(HttpServletRequest req) {
        Cookie[] cookies = req.getCookies();
        if(cookies != null) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals(AUTHORIZATION_HEADER)) {
                    try {
                        return URLDecoder.decode(cookie.getValue(), "UTF-8"); // Encode 되어 넘어간 Value 다시 Decode
                    } catch (UnsupportedEncodingException e) {
                        return null;
                    }
                }
            }
        }
        return null;
    }
 

ProductController

package com.sparta.springauth.controller;

import com.sparta.springauth.entity.User;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/api")
public class ProductController {

    @GetMapping("/products")
    public String getProducts(HttpServletRequest req) {
        System.out.println("ProductController.getProducts : 인증 완료");
        User user = (User) req.getAttribute("user"); //AuthFilter set.Attribute에 name에 저장한 값을 가져옴
        System.out.println("user.getUsername() = " + user.getUsername());

        return "redirect:/";
    }
}
 

order(1) -> order(2) -> ProductController -> order(1)

 

'Spring Security' 프레임워크

build.gradle

//Security강의까지의 라이브러리
dependencies {
// Security
    implementation 'org.springframework.boot:spring-boot-starter-security'
    // JWT
    compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
    // JPA
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    // MySQL
    runtimeOnly 'com.mysql:mysql-connector-j'

    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
 
package com.sparta.springauth.config;

import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity // Spring Security 지원을 가능하게 함
public class WebSecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // CSRF 설정
        http.csrf((csrf) -> csrf.disable());

        http.authorizeHttpRequests((authorizeHttpRequests) ->
                authorizeHttpRequests
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources(static,templates) 접근 허용 설정
                        .requestMatchers("/api/user/**").permitAll()//api/user/로 시작하는 모든 요청 허가
                        .anyRequest().authenticated() // 그 외 모든 요청은 인증 처리
                        //AuthFilter부분의 인증방식을 구현
        );

        // 로그인 사용
        http.formLogin(Customizer.withDefaults());

        return http.build();
    }
}
 

CSRF란?

CSRF(사이트 간 요청 위조, Cross-site request forgery)

공격자가 인증된 브라우저에 저장된 쿠키의 세션 정보를 활용하여 웹 서버에 사용자가 의도하지 않은 요청을 전달하는 것

CSRF 설정이 되어있는 경우 html 에서 CSRF 토큰 값을 넘겨주어야 요청을 수신 가능

쿠키 기반의 취약점을 이용한 공격 이기 때문에 REST 방식의 API 에서는 disable 가능

POST 요청마다 처리해 주는 대신 CSRF protection 을 disable 함

 

Spring Security

Filter기반임

Session방식으로 동작을 함

https://www.google.com/search?q=Session%EB%B0%A9%EC%8B%9D&rlz=1C5CHFA_enKR1072KR1072&oq=Session%EB%B0%A9%EC%8B%9D&aqs=chrome..69i57j0i131i433i512l2j0i512l7.5767j0j7&sourceid=chrome&ie=UTF-8

이미지 썸네일 삭제
🔎 Session방식 - Google Search

 

www.google.com

Spring Security - Filter Chain

Spring에서 모든 호출은 DispatcherServlet을 통과하게 되고 이후에 각 요청을 담당하는 Controller 로 분배

이 때, 각 요청에 대해서 공통적으로 처리해야할 필요가 있을 때 DispatcherServlet 이전에 단계가 필요하며 이것이 Filter임

 
사진 삭제

사진 설명을 입력하세요.

Spring Security도 인증 및 인가를 처리하기 위해 Filter를 사용하는데 Spring Security는 FilterChainProxy를

통해서 상세로직을 구현하고 있음

 
사진 삭제

사진 설명을 입력하세요.

인증이 되지않으면 login을 반환함

UsernamePasswordAuthenticationFilter(인증 로직)

 
사진 삭제

사진 설명을 입력하세요.

  1. 인증 과정
  2. 사용자가 username과 password를 제출하면 UsernamePasswordAuthenticationFilter는 인증된 사용자의 정보가 담기는 인증 객체인 Authentication의 종류 중 하나인 UsernamePasswordAuthenticationToken을 만들어 AuthenticationManager에게 넘겨 인증을 시도합니다.
  3. 실패하면 SecurityContextHolder를 비웁니다.
  4. 성공하면 SecurityContextHolder에 Authentication를 세팅합니다.

 

SecurityContextHolder

 
사진 삭제

사진 설명을 입력하세요.

// 예시코드
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication = new UsernamePasswordAuthenticationToken(principal, credentials, authorities);
context.setAuthentication(authentication); // SecurityContext 에 인증 객체 Authentication 를 저장.

SecurityContextHolder.setContext(context);
 

Autyentication

principal : 사용자를 식별

- Username/Password 방식으로 인증할 때 일반적으로 UserDetails 인스턴스

credentials : 주로 비밀번호, 대부분 사용자 인증에 사용한 후 비움

authorities : 사용자에게 부여한 권한을 GrantedAuthority로 추상화하여 사용

<UserDetails>
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
    UserRoleEnum role = user.getRole();
    String authority = role.getAuthority();

    SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
    Collection<GrantedAuthority> authorities = new ArrayList<>();
    authorities.add(simpleGrantedAuthority);

    return authorities;
}

Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
 

Spring Security Login

 
사진 삭제

사진 설명을 입력하세요.

로그인 처리 과정

 
사진 삭제

사진 설명을 입력하세요.

Manager인증처리를 함 그때 UserDetails Service를 사용해서 DB 해당 회원을 조회함

조회한 내용을 UserDetails를 만들어서 Manager로 전달. 실패하면 오류 성공하면 Session을 클라이언트에 반환

 

Security Login부분 Default 사용하지 않고 구현

package com.sparta.springauth.config;

import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity // Spring Security 지원을 가능하게 함
public class WebSecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // CSRF 설정
        http.csrf((csrf) -> csrf.disable());

        http.authorizeHttpRequests((authorizeHttpRequests) ->
                authorizeHttpRequests
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
                        .requestMatchers("/api/user/**").permitAll() // '/api/user/'로 시작하는 요청 모두 접근 허가
                        .anyRequest().authenticated() // 그 외 모든 요청 인증처리
        );

        // 로그인 사용(디폴트값 -> 만든 로그인페이지)
        http.formLogin((formLogin) ->
                formLogin
                        // 로그인 View 제공 (GET /api/user/login-page)
                        .loginPage("/api/user/login-page")
                        // 로그인 처리 (POST /api/user/login) //구현했던 로그인 @POST가 아닌 Security가 먼저 수행하는 것
                        .loginProcessingUrl("/api/user/login")
                        // 로그인 처리 후 성공 시 URL
                        .defaultSuccessUrl("/")
                        // 로그인 처리 후 실패 시 URL
                        .failureUrl("/api/user/login-page?error")
                        .permitAll()
        );

        return http.build();
    }
}
 
package com.sparta.springauth.security;

import com.sparta.springauth.entity.User;
import com.sparta.springauth.repository.UserRepository;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;

    public UserDetailsServiceImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("Not Found " + username));

        return new UserDetailsImpl(user);
    }
}
 
package com.sparta.springauth.security;

import com.sparta.springauth.entity.User;
import com.sparta.springauth.entity.UserRoleEnum;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

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

public class UserDetailsImpl implements UserDetails {

    private final User user;

    public UserDetailsImpl(User user) {
        this.user = user;
    }

    public User getUser() {
        return user;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        UserRoleEnum role = user.getRole();
        String authority = role.getAuthority();

        SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(simpleGrantedAuthority);

        return authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}
 
package com.sparta.springauth.controller;

import com.sparta.springauth.entity.User;
import com.sparta.springauth.security.UserDetailsImpl;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/api")
public class ProductController {

    @GetMapping("/products")
    public String getProducts(@AuthenticationPrincipal UserDetailsImpl userDetails) {
//        @HttpServletRequest req
//        System.out.println("ProductController.getProducts : 인증 완료");
//        User user = (User) req.getAttribute("user"); //set.Attribute가져옴
//        System.out.println("user.getUsername() = " + user.getUsername());
        User user = userDetails.getUser();
        System.out.println("user.getUsername() = " + user.getUsername());
        return "redirect:/";
    }
}
 

@AuthenticationPrincipal -> principal 부분에 userDetails 가져옴

 

Validation

implementation 'org.springframework.boot:spring-boot-starter-validation'
 
package com.sparta.springauth.dto;

import jakarta.validation.constraints.*;
import lombok.Getter;

@Getter
public class ProductRequestDto {
    @NotBlank
    private String name;
    @Email
    private String email;
    @Positive(message = "양수만 가능합니다.")
    private int price;
    @Negative(message = "음수만 가능합니다.")
    private int discount;
    @Size(min=2, max=10)
    private String link;
    @Max(10)
    private int max;
    @Min(2)
    private int min;
}
 

Validation 예외처리

@PostMapping("/user/signup")
public String signup(@Valid SignupRequestDto requestDto, BindingResult bindingResult) {
    // Validation 예외처리
    List<FieldError> fieldErrors = bindingResult.getFieldErrors();
    if(fieldErrors.size() > 0) {
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            log.error(fieldError.getField() + " 필드 : " + fieldError.getDefaultMessage());
        }
        return "redirect:/api/user/signup";
    }

    userService.signup(requestDto);

    return "redirect:/api/user/login-page";
}
 

예외 발생시 BindingResult 객체에 오류에 대한 정보가 담김

예외 발생시 for문을 통해서 예외 정보 출력, 예외 미발생 시 userService 비즈니스 로직 수행

 

정규표현식 확인

    @Pattern(regexp = "^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$")
    @NotBlank
    private String email;
 

 

반응형
LIST

'K-Digital Training > 내일배움캠프' 카테고리의 다른 글

숙련 2주차/Spring  (0) 2023.11.21
입문 2주차/Spring  (2) 2023.11.21
입문 1주차/Spring  (1) 2023.11.21
자바 5주차  (1) 2023.11.01
자바 4주차  (1) 2023.11.01