자바 ORM 표준 JPA 프로그래밍 | 김영한 | 에이콘출판- 교보ebook

스프링 데이터 예제 프로젝트로 배우는 전자정부 표준 데이터베이스 프레임, <b>★ 이 책에서 다루는 내용 ★</b> ■ JPA 기초 이론과 핵심 원리 ■ JPA로 도메인 모델을 설계하는 과정을 예제 중심

ebook-product.kyobobook.co.kr

소스

https://github.com/rkwhr0010/jpa

 

SQL 직접 다룰 발생하는 문제점

지루한 반복 코드

JDBC API 사용해 DB CRUD 하려면 너무 많은 직접 작성한 유사한 SQL DB 연결을 위한 반복성 코드가 생긴다.

데이터 접근 계층(DAO) 대부분 반복성 코드와 유사한 SQL 많다.

 

SQL 의존적인 개발

비즈니스 요구사항은 언제나 변한다.

public class Member {
        private Long id;
        private String name;
        private String email; // 새로 추가된 컬럼
}

비즈니스 요구사항을 모델링한 객체를 엔티티라 한다.

요구사항의 변경으로 Member 엔티티에서 email 추가로 저장해야 하는 상황이 오면, 모든 관계된 CRUD 코드를 확인하고 SQL 수정해야 한다.

 

JPA 문제 해결

JPA 객체를 DB 저장할 JPA 제공하는 API 사용하면 된다.

JPA 자동으로 SQL 생성해서 DB에게 전달한다.

 

다음은 CRUD 대표적인 메서드들이다

저장

 

 

조회

 

 

수정

수정의 경우 특별하다. find() 조회한 엔티티를 수정하면, 트랜잭션을 커밋할 DB 수정사항이 반영된다.

 

삭제

 

 

페러다임의 불일치

애플리케이션은 발전하면서 복잡성도 증대된다.

개발자는 복잡성을 제어해 유지보수하기 좋은 코드를 만들려 노력한다.

 

객체지향 캡상추다, SOLID, 디자인패턴 등은 복잡성을 제어하기 위한 도구이다.

 

비즈니스 요구사항은 코드로 구현을 해도 결국 산출물로 데이터가 나오고 이는 영구히 저장할 곳이 필요하다. 이때 RDB(관계형 DB) 역할을 한다. 여기서 DB 편의상 RDB 한정한다.

 

런타임에 객체로 존재하는 데이터를 RDB 저장해야 한다.

문제는 객체와 RDB 지향하는 목적이 다르다는 것이다.

RDB에는 객체지향이라는 개념이 존재하지 않는다. 데이터 중심으로 구조화되어 있다.

객체 구조를 테이블 구조에 저장하는 것에 한계가 있다. 이를 패러다임 불일치라 한다.

 

패러다임 불일치를 해소하기 위해 결국 개발자가 많은 노력을 기울여야 한다.

 

상속

객체는 상속이라는 개념이 존재한다.

DB 모델링에는 유사한 개념인 슈퍼타입 서브타입 관계가 존재한다.

비슷해 보이지만, 다르다. 객체는 extends 키워드로 부모의 속성을 상속 받지만, DB 경우 슈퍼타입과 서브타입 KEY 로만 관계를 맺는다.

, 1개의 서브타입 객체를 DB 저장하려면 2개의 INSERT 쿼리가 필요하다.

이게 패러다임 불일치를 해소하기 위한 비용이다.

 

JPA 상속

JPA 마치 자바 컬렉션에 저장하듯 사용하면, 나머지는 JPA 알아서 처리해 준다.

JPA 알아서 2개의 INSERT 쿼리로 처리해줌

 

 

 

package study.jpa.entity;


import jakarta.persistence.Column;
import jakarta.persistence.DiscriminatorColumn;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Inheritance;
import jakarta.persistence.InheritanceType;


@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {
        @Id
        @GeneratedValue
        @Column(name = "ITEM_ID")
        private Long id;
        
        private String name;
        private Integer price;
        
        public Long getId() {
                return id;
        }
        public void setId(Long id) {
                this.id = id;
        }
        public String getName() {
                return name;
        }
        public void setName(String name) {
                this.name = name;
        }
        public Integer getPrice() {
                return price;
        }
        public void setPrice(Integer price) {
                this.price = price;
        }
}


package study.jpa.entity;


import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;


@Entity
@DiscriminatorValue("A")
public class Album extends Item{
        private String artist;


        public String getArtist() {
                return artist;
        }


        public void setArtist(String artist) {
                this.artist = artist;
        }
}
package study.jpa.entity;


import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;


@Entity
@DiscriminatorValue("B")
public class Book extends Item{
        private String author;
        private String isbn;
        public String getAuthor() {
                return author;
        }
        public void setAuthor(String author) {
                this.author = author;
        }
        public String getIsbn() {
                return isbn;
        }
        public void setIsbn(String isbn) {
                this.isbn = isbn;
        }
}

package study.jpa.entity;


import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;


@Entity
@DiscriminatorValue("M")
public class Movie extends Item{
        private String director;
        private String actor;
        
        public String getDirector() {
                return director;
        }
        public void setDirector(String director) {
                this.director = director;
        }
        public String getActor() {
                return actor;
        }
        public void setActor(String actor) {
                this.actor = actor;
        }
}



package study.jpa;


import java.util.function.Consumer;


import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;


import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.Persistence;
import study.jpa.entity.Album;


public class Main {
        static Logger logger = LogManager.getLogger();
        static EntityManagerFactory emf =
                        Persistence.createEntityManagerFactory("studyjpa");
        
        public static void main(String[] args) {
                createPersistenceContext(em -> {
                        Album album = new Album();
                        album.setName("앨범");
                        album.setArtist("홍길동");
                        album.setPrice(1000);
                        
                        em.persist(album);
                });
        }


        private static void createPersistenceContext(Consumer<EntityManager> consumer) {
                EntityManager em = emf.createEntityManager();
                EntityTransaction tx = em.getTransaction();
                
                try {
                        tx.begin();
                        
                        consumer.accept(em);
                        
                        tx.commit();
                } catch (Exception e) {
                        tx.rollback();
                } finally {
                        em.close();
                }
                emf.close();
        }
}

 

자동으로 2 SQL

 

JPA 자동으로 객체와 테이블 간극을 매핑해준다.

 

 

연관관계

객체는 참조를 통해 해당 객체에 접근한다.

테이블은 외래 키를 통해 조인으로 연관 테이블을 조회한다.

 

객체의 참조와 RDB 외래 사이 패러다임 불일치는 RDB 구조를 객체지향 모델링에 적용 시키는 것이 어렵다.

 

참조의 경우 단방향이다.

 

테이블의 경우 외래 키로 관계를 맺으므로, 방향이라는 것이 없다. 굳이 따지면 양방향이다.

 

 

객체를 테이블에 맞추어 모델링

패러다임 불일치를 가장 크게 체감할 있는 방법은 객체를 테이블에 맞춰서 설계해보는 것이다.

 

위와 같은 설계는 데이터를 DB 저장하기는 편하다.

문제는 RDB 경우 값으로 조인을 관계를 맺어준다.

객체는 값으로 관계를 맺어줄 수가 없다. 참조로 연결해야 연관된 객체를 있다.

 

 

처럼 객체지향에 맞춰서 설계를 하면, DB 데이터를 객체에 맞춰야한다.

과정을 JPA 자동으로 해준다.

, 개발자는 마치 컬렉션에서 객체를 꺼내듯 사용하면, DB에서 엔티티가 조회된다.

 

 

 

 

JPA 알아서 패러다임 불일치를 해결해준다.

 

처럼 객체 참조를 통해 이동하는 것을 객체 그래프 탐색이라고 한다.

 


JPA 비교와 그냥 비교

 

 

객체 비교의 두가지 방법

동일성은 == 비교이다.

동등성 비교는 equals() 비교이다

 

동일성은 참조 변수의 경우 같은 인스턴스인지 확인한다.

동등성은 서로 다른 객체여도 내용이 같은지 확인한다.

 

 

 

여기서 중요한 점은 JPA 조회한 회원 객체이다.

같은 영속성 컨텍스트에서 조회한 멤버는 동일성을 보증한다.

 

정리

객체 모델과 RDB 모델은 지향하는 패러다임이 다르다.

객체지향 어플리케이션을 더욱 객체지향스럽게 디자인 수록

패러다임 불일치가 커진다.

JPA 이런 패러다임 불일치 문제를 해결해준다.

JPA 개발자로 하여금 계속 객체지향 모델링을 유지하도록 도와준다.

 

 

 

JPA 무엇인가

JPA Java Persistence API

자바 진영의 ORM 기술 표준

ORM Object-Relational Mapping

객체와 RDB 매핑한다는

 

JPA 제외하고도 모든 자바 기반 기술은 DB 접속은 JDBC API 사용한다.

 

 

ORM 프레임워크는 객체와 테이블 매핑에서 오는 패러다임 불일치 문제를 해결해준다.

개발자는 자바 코드에서 컬렉션에 객체를 저장하듯 ORM 프레임워크에 저장하면, 나머지는 작업은 ORM 프레임워크가 알아서 해준다.

가장 유명한 ORM 프레임워크는 하이버네이트다.

 

JPA 자바 ORM 기술에 대한 API 표준 명세로, 바꿔 말하면, 인터페이스 묶음이다.

, 실질적인 구현체 하나를 선택해 사용한다.

 

JPA 사용해야 하나

장점으로 생산성, 유지보수성, 패러다임 불일치 해결, 성능, 데이터 접근 추상화(벤더 독립성) 있다.

 

지루하고 반복적인 CRUD SQL SQL 객체 사이 데이터 매핑을 필요가 없다.

엔티티에 필드가 변경되어도 이전 JPA사용하기 전보다 훨씬 적은 양의 코드만 손보면 된다.

JPA 객체 RDB 매핑을 해주기 때문에 개발자는 객체의 참조를 그대로 사용해 객체 그래프 탐색을 있다. 원래라면 개발자가 조인 쿼리를 짜고, 나온 데이터를 전부 객체에 알맞게 매핑해줘야하는 번거로운 일이다. 때문에 과거에는 객체를 테이블에 맞춰 데이터 중심으로 사용했다.

JPA 애플리케이션과 DB 사이에서 동작한다. 사이에서 캐시 역할 등으로 성능 최적화 기회를 얻는다.

데이터 접근이 단계 추상화된 방언(Dialect) 계층이 있다.

따라서 DB 변경이 자유롭다. 알맞은 벤더의 방언으로 교체하기만 하면 방언이 알아서 SQL 변경해준다.

극단적으로 개발은 Mysql 운영은 오라클로 바꿀 있다.

 

 

 

 

'개발 > JPA' 카테고리의 다른 글

06 다양한 연관관계 매핑  (0) 2024.04.14
05 연관관계 매핑 기초  (1) 2024.03.22
04 엔티티 매핑  (1) 2024.02.26
03 영속성 관리  (1) 2024.02.19
01 JPA 환경 설정  (0) 2023.11.23

DB 설치

Mysql 진행

나는 도커로 Mysql 설치했으나, 로컬에 설치해도 상관 없음

도커

docker run --name test-mysql -e MYSQL_ROOT_PASSWORD=root -p 55555:3306 -d mysql:8.1

로컬 포트는 55555 사용했다. 도커에서 Mysql 포트 3306 매핑한다.

 

DB 스키마 계정 생성

mysql -u root -p




CREATE DATABASE jpa CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'root' IDENTIFIED BY 'root';
GRANT ALL PRIVILEGES ON jpa.* TO 'root';

 

 

DB 접속 툴은 DBeaver

 

STS(이클립스) 기준

프레임워크 없이 단순 JPA만 실습하기 위한 프로젝트 설

 

 

메이븐 의존성 pom.xml



<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>jpa-basic</groupId>
    <artifactId>jpa-01</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <hiberate.version>6.0.0.Final</hiberate.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>${hiberate.version}</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-hikaricp</artifactId>
            <version>${hiberate.version}</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.28</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.21.1</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

 

영속성 유닛 설정

특이사항으로 이제 jakarta 패키지 명을 사용한다.

 



<?xml version="1.0" encoding="utf-8"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
    version="3.0">
    <persistence-unit name="studyjpa" transaction-type="RESOURCE_LOCAL">
        <properties>
            <!-- DB 연결 정보 -->
            <property name="jakarta.persistence.jdbc.driver"
                value="com.mysql.cj.jdbc.Driver" />
            <property name="jakarta.persistence.jdbc.url"
                value="jdbc:mysql://localhost:55555/jpa?characterEncoding=utf8" />
            <property name="jakarta.persistence.jdbc.user" value="root" />
            <property name="jakarta.persistence.jdbc.password" value="root" />
            <property name="hibernate.hikari.poolName" value="pool" />
            <property name="hibernate.hikari.maximumPoolSize" value="10" />
            <property name="hibernate.hikari.minimumIdle" value="10" />
            <property name="hibernate.hikari.connectionTimeout" value="1000" />
            <!--  MySql 방언 -->
            <property name="hibernate.dialect"
                value="org.hibernate.dialect.MySQLDialect" />
            <!-- SQL 출력 -->
            <property name="hibernate.show_sql" value="true" />
            <!-- SQL 이쁘게 출력 -->
            <property name="hibernate.format_sql" value="true" />
            <!-- 주석도 함께 출력 -->
            <property name="hibernate.use_sql_comments" value="true" />
            <!-- JPA 표준에 맞춘 새로운 키 생성 전략 사용 -->
            <property name="hibernate.id.new_generator_mappings" value="true" />
            <!-- 실습에서만 사용할 것, @Entity에 따라 DDL 명령을 자동 실행해 준다. -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
        </properties>
    </persistence-unit>
</persistence>

 

logf4.xml



<?xml version="1.0" encoding="UTF-8"?>
<configuration status="debug">


    <Appenders>
        <Console name="console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{DEFAULT} %5p [%c] %m%n"/>
        </Console>
    </Appenders>
   
    <loggers>
        <root level="debug" additivity="true">
            <AppenderRef ref="console"/>
        </root>
    </loggers>


</configuration>

 

테스트



package study.jpa.entity;


import java.time.LocalDateTime;


import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;


@Entity
public class User {
        
        @Id
        @GeneratedValue
        private Long id;
        private String name;
        private LocalDateTime registDate;
        
        public Long getId() {
                return id;
        }
        public void setId(Long id) {
                this.id = id;
        }
        public String getName() {
                return name;
        }
        public void setName(String name) {
                this.name = name;
        }
        public LocalDateTime getRegistDate() {
                return registDate;
        }
        public void setRegistDate(LocalDateTime registDate) {
                this.registDate = registDate;
        }
}



import java.time.LocalDateTime;


import org.apache.logging.log4j.*;


import jakarta.persistence.*;
import study.jpa.entity.User;


public class Main {
        static Logger logger = LogManager.getLogger();
        
        public static void main(String[] args) {
                
                
                EntityManagerFactory emf = Persistence.createEntityManagerFactory("studyjpa");
                EntityManager em = emf.createEntityManager();
                EntityTransaction tx = em.getTransaction();
                
                try {
                        tx.begin();
                        
                        logger.info("#################로그 테스트#################");
                        User user = new User();
                        user.setName("홍길동");
                        user.setRegistDate(LocalDateTime.now());
                        em.persist(user);
                        
                        tx.commit();
                } catch (Exception e) {
                        tx.rollback();
                } finally {
                        em.close();
                }
                emf.close();
        }
}

 

 

 

 

 

 

 

'개발 > JPA' 카테고리의 다른 글

06 다양한 연관관계 매핑  (0) 2024.04.14
05 연관관계 매핑 기초  (1) 2024.03.22
04 엔티티 매핑  (1) 2024.02.26
03 영속성 관리  (1) 2024.02.19
02 JPA 소개  (0) 2023.12.01

 

AuthenticationProvider 인터페이스가 인증 논리를 담당한다.

 

AuthenticationProvider 이해

실제 인증은 여러 방식이 있을 있다.

단순한 id/pw 인증이 전부가 아니다.

 

스프링 시큐리티는 AuthenticationProvider 모든 인증을 추상화하고, 이게 맞게 구현만 하면 어떠한 인증 시나리오도 구축할 있다.

 

인증에 사용되는 Authentication

Authentication 인증에 사용되는 필수 인터페이스다.

아래 처럼 인자로 사용되어 인증에 사용된다.

 

Authentication Principal 확장한다. Principal 자바 시큐리티 인터페이스다.

 

 

인증 논리 구현

authenticate() 인증 성공 완전히 인증된 Authentication 반환해야 한다.

isAuthenticated() true 반환하고, 모든 필수 정보가 포함되야한다.

, 보안 상의 이유로 자격증명은 제거될 있다.

인증이 이미 성공했기 때문에 이상 자격증명 같은 민감한 정보가 필요없기 때문이다.

 

supports() 매개변수 Authentication 인증 사용할 있는지 여부를 리턴한다.

주의할 것은 메서드가 true 리턴해도 authenticate()메서드에서 null 리턴해 인증을 거부할 있다.

supports() false 경우 AuthenticaitonManager 인증을 시도하지 않는다. 바로 다음 AuthenticationProvider에게 인증을 위임한다.

 

맞춤형 인증 논리 구현

 

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.security.authentication.AuthenticationProvider;

import org.springframework.security.authentication.BadCredentialsException;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

import org.springframework.security.core.Authentication;

import org.springframework.security.core.AuthenticationException;

import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.security.core.userdetails.UserDetailsService;

import org.springframework.security.crypto.password.PasswordEncoder;

import org.springframework.stereotype.Component;

 

@Component

public class CustomAuthenticationProvider implements AuthenticationProvider{

        Logger logger = LoggerFactory.getLogger(getClass());

        

        @Autowired

        private UserDetailsService userDetailsService;

        

        @Autowired

        private PasswordEncoder passwordEncoder;

        

        @Override

        public Authentication authenticate(Authentication authentication) throws AuthenticationException {

                logger.info("=========== 인증 시작 ===========");

                

                String username = authentication.getName();

                String password = authentication.getCredentials().toString();

                

                UserDetails userDetails = userDetailsService.loadUserByUsername(username);

                

                if(passwordEncoder.matches(password, userDetails.getPassword())) {

                        return new UsernamePasswordAuthenticationToken(

                                        username, password, userDetails.getAuthorities());

                }

             

                throw new BadCredentialsException("인증 실패");

        }

        @Override

        public boolean supports(Class<?> authentication) {

                return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);

        }

}

 

참고로 권한 정보까지 사용해서 UsernamepasswordAuthenticationToken 생성하면

아래와 같이 완전히 인증됨을 의미하는 플래그 값을 true 설정한다.

실행

 

 

 

SecurityContext 이용

SecurityContext(이하 보안 컨텍스트)

 

인증 프로세스가 끝난 인증된 Authentication 대한 세부 정보가 필요할 가능성이 크다.

주로 이름과 권한을 지속적으로 확인해야할 있다.

SecurityContext Authentication 객체가 요청이 유지되는 동안 내부에 저장한다.

 

 

 

보안 컨텍스트는 이게 전부다. 보면 있듯 인증 객체를 저장하는 것이 역할이다.

 

보안 컨텍스트를 관리하는 SecurityContextHolder는 3 가지 모드를 지원한다.

 

MODE_THREADLOCAL

스레드에 안전한 저장소에 보안 문맥을 저장한다.

일반적으로 WAS 하나의 요청 하나의 쓰레드를 가지므로 기본이 되는 전략이다.

 

MODE_INHERITABLETHREADLOCAL

MODE_THREADLOCAL와 유사하다.

비동기 메서드의 경우, 다음 스레드로 복사해서 사용한다.

예를들어, @Async 메서드를 실행하는 스레드가 보안 컨텍스트를 상속한다.

 

MODE_GLOBAL

애플리케이션의 모든 스레드가 같은 보안 컨텍스트를 공유한다.

 

스프링 시큐리티는 이외에도 사용자가 커스텀한 전략을 사용하도록 설정할 있다.

 

보안 컨텍스트를 위한 보유 전략 이용

MODE_THREADLOCAL

기본 전략은 MODE_THREADLOCAL 이다.

내부적으로 ThreadLocal 사용한다.

ThreadLocal 저장된 데이터는 다른 쓰레드에선 접근이 불가능하다.

이는 요청마다 새로운 쓰레드를 생성하는 일반적인 WAS 맞아떨어지는 동작방식이다.

 

만약 서버가 비동기 기술을 사용한다면 문제가 있다. 기존 쓰레드에서 새로운 쓰레드를 생성해 실행하면, 새로 생성된 쓰레드는 접근을 하지 못하기 때문이다.

6번에 저장된 보안 컨텍스트를 7번에서 꺼내서 사용할 있다.

 

 

 

MODE_INHERITABLETHREADLOCAL

비동기를 사용하는 상황에서 전략

 

비동기 설정

 

 

 

아직 보안 컨텍스트 모드를 설정하지 않아 기본 모드인 MODE_THREADLOCAL 동작하기에 값이 없다.

 

InitializingBean 빈을 구현하면, 스프링이 컨테이너를 초기화하는 과정에 호출해준다.

 

 

정상적으로 값이 나온다.

 

주의할 것은 방법은 선언적으로 작성한 코드에 대해서만 동작을 한다.

선언적으로 작성한 코드는 프레임워크에서 런타임 중에 비동기임을 알기 때문에 자동으로 처리가 된다.

 

개발자가 메서드 안에서 코드로 쓰레드를 만들어 호출하면, 프레임워크는 비동기가 발생한지 없어, Null 나오게 된다.

 

MODE_GLOBAL

보안 컨텍스트가 모든 스레드에 공유된다.

 

모든 스레드가 공유하기 때문에 이게 따른 동시성 이슈를 개발자가 직접 해결해야 한다.

 

 

 

DelegatingSecurityContextRunnable 보안 컨텍스트 전달

보안 컨텍스트 저장 전략은 가지로 기본 모드는 MODE_THREADLOCAL 이다

모드는 하나의 요청 마다 새로운 쓰레드를 사용하는 보편적인 서버에서 사용된다.

상황에서 비동기 작업을 하는 경우, 개발자가 직접 쓰레드로 보안 컨텍스트를 전파하는 작업을 해줘야 한다

 

이를 위해 스프링이 제공하는 유틸들이 있다.

DelegatingSecurityContextRunnable 반환 값이 없는 경우 사용

DelegatingSecurityContextCallable 반환 값이 있는 경우 사용한다.

 

 

 

 

비동기 작업을 비동기 메서드를 통해, 쓰레드로 복사한 실행한다.

실행

 

 

 

DelegatingSecurityContextExecutorService

DelegatingSecurityContextCallable, DelegatingSecurityContextRunnable은 동기화 모드에서 비동기 메서드를 사용해야 보안 컨텍스트를 복사해주는 유용한 클래스다.

 

이보다 우아한 방법으로 DelegatingSecurityContextExecutorService는 스레드 풀에서 전파 작업을 해준다.

 

 

 

 

 

스레드 풀에서 새로운 스레드에 보안 컨텍스트를 전파해 준다.

이것도 기존 클래스에 기능을 데코레이터 패턴이 적용된 클래스다.

 

보안 컨텍스트를 전파해주는 스레드 클래스

 

보안 컨텍스트를 전파해주는 클래스

DelegatingSecurityContextCallable, DelegatingSecurityContextRunnable

 

인증 실패 제어

클라이언트가 인증에 실패하거나, 액세스 권한이 없는 리소스에 인증되지 않은 요청을 하는 경우가 있다.

이 경우 클라이언트로부터 자격 증명을 요청하는 데 AuthenticationEntryPoint 구현이 사용된다.

AuthenticationEntryPoint 구현은 로그인 페이지로 리디렉션을 수행하거나, WWW-Authenticate 헤더로 응답하거나, 다른 작업을 수행할 수 있다.

 

일반적으로 ExceptionTranslationFilter에 인증 체계를 시작하기 전에 호출된다.

 

 

 

import java.io.IOException;


import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;


import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;


public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint{


        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response,
                        AuthenticationException authException) throws IOException, ServletException {
                response.addHeader("message", "Authentication Failed");
                response.setStatus(HttpStatus.UNAUTHORIZED.value());
        }


}

 

import javax.sql.DataSource;


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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;


import com.springsecurity.authentication.CustomAuthenticationEntryPoint;


@Configuration
public class SecurityConfig {
        
        @Bean
        SecurityFilterChain config(HttpSecurity http) throws Exception {
                http.httpBasic(c -> {
                        c.realmName("ADMIN");
                        c.authenticationEntryPoint(new CustomAuthenticationEntryPoint());
                });
                http.authorizeHttpRequests(request -> {
                        request.anyRequest().authenticated();
                });
                
                return http.build();
        }
        
   
    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
    @Bean
    UserDetailsService userDetailsService(DataSource dataSource) {
            return new JdbcUserDetailsManager(dataSource);
    }
}

 

 

인증 실패 해보기

 

 

 

 

Form 기반 로그인 인증 구현

일반적인 사용자가 ID/PW 입력해서, 인증하는 방식을 실습해본다.

 

 

위와 같이 설정 서버를 키고 접속하면, 스프링이 기본으로 제공하는 로그인 페이지가 뜬다.

 

 

이전처럼 RestController 아닌 일반 Controller 실습을 하기 위해 간단한 html파일 생성

 

 

 

 

 

 

 

로그인 성공/실패 처리할 핸들러

성공은 AuthenticationSuccessHandler, 실패는 AuthenticationFailureHandler

각각 구현해서 아래에 메서드에 등록해 주면 된다.

 

 

 

form 타입은 FormLoginConfigurer<HttpSecurity>으로 양식 인증 관련 다양한 설정을 있다.

 

 

AuthenticationSuccessHandler, AuthenticationFailureHandler  구현

import java.io.IOException;


import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;


import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;


@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler{


        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                        Authentication authentication) throws IOException, ServletException {
                var auth = authentication.getAuthorities()
                        .stream()
                        .filter(a -> a.getAuthority().equals("read"))
                        .findFirst();
                
                if(auth.isEmpty()) {
                        //권한 없음
                        response.sendRedirect("/error");
                } else {
                        //권한 존재
                        response.sendRedirect("/home");
                }
        }
}

 

import java.io.IOException;
import java.time.LocalDateTime;


import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;


import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;


public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler{
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                        AuthenticationException exception) throws IOException, ServletException {
                
                response.addHeader("failed", LocalDateTime.now().toString());
        }
}

 

import java.io.IOException;
import java.time.LocalDateTime;


import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;


import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;


@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler{
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                        AuthenticationException exception) throws IOException, ServletException {
                
                response.addHeader("failed", LocalDateTime.now().toString());
        }
}

 

import java.io.IOException;


import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;


import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;


@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint{


        public void commence(HttpServletRequest request, HttpServletResponse response,
                        AuthenticationException authException) throws IOException, ServletException {
                response.addHeader("message", "Authentication Failed");
                response.setStatus(HttpStatus.UNAUTHORIZED.value());
        }


}

 

 

 
 

import javax.sql.DataSource;



import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

org.springframework.security.config.annotation.web.builders.HttpSecurity를 ​​가져옵니다.

org.springframework.security.core.userdetails.UserDetailsService 가져오기;

org.springframework.security.crypto.password.NoOpPasswordEncoder 가져오기;

org.springframework.security.crypto.password.PasswordEncoder 가져오기;

org.springframework.security.provisioning.JdbcUserDetailsManager 가져오기;

org.springframework.security.web.SecurityFilterChain 가져오기;



import com.springsecurity.authentication.CustomAuthenticationEntryPoint;



@구성

공개 클래스 SecurityConfig {



@콩

SecurityFilterChain 구성(HttpSecurity http)에서 예외가 발생합니다.

http.http기본(c -> {

c.realmName("관리자");

c.authenticationEntryPoint(new CustomAuthenticationEntryPoint());

});

http.authorizeHttpRequests(요청 -> {

request.anyRequest().authenticated();

});



http.build()를 반환합니다.

}





@콩

PasswordEncoder 비밀번호Encoder() {

NoOpPasswordEncoder.getInstance()를 반환합니다.

}

@콩

UserDetailsService userDetailsService(DataSource dataSource) {

새로운 JdbcUserDetailsManager(dataSource)를 반환합니다.

}

}

 

PasswordEncoder

사용자 세부정보를 찾은 후에 요청에서 얻은 자격증명과 사용자 세부정보에 자격증명과 비교를 한다.

 

정의

 

encode() 주어진 자격증명을 해시나 암호화를 한다.

amtches() 주어진 자격증명과 암호화나 해시화된 자격증명과 일치하는 확인한다.

upgrageEncoding() 경우 인코딩된 비밀번호를 다시 인코딩해야 하는 경우 true를 반환하도록 재정의한다.

 

 

 

단순한 구현

암호기능 없음

public class PlainTextPasswordEncoder implements PasswordEncoder{

        public String encode(CharSequence rawPassword) {

                return rawPassword.toString();

        }

        public boolean matches(CharSequence rawPassword, String encodedPassword) {

                return Objects.equals(rawPassword, encodedPassword);

        }

}

테스트

 

class SpringSecurity04PasswordApplicationTests {

 

        @Test

        void 단순_암호_인코더_테스트() {

                PlainTextPasswordEncoder encoder = new PlainTextPasswordEncoder();

                String password = "tiger";

                

                assertThat(encoder.matches(password,encoder.encode(password))).isTrue();

        }

}

 

학습 테스트를 위한 구현이지, 실제로는 이미 구현된 클래스를 가져다 쓴다.

암호화 알고리즘 기능은 개발자가 구현할 일도 없다.

 

나머지 테스트

 

SCryptPasswordEncoder 추가로 의존하는 라이브러리가 있다.

 

 

 

 

DelegatingPasswordEncoder

자격증명을 검증할 다양한 알고리즘이 필요한 경우 사용한다.

 

자격증명 앞에 접두로 "{알고리즘}" 붙여 상황에 맞게 알고리즘을 실행한다.

 

예시

{noop}tiger , 인코딩하지 않는다.

{bcrypt}tiger , BCryptPasswordEncoder로 인코딩한다.

 

BCryptPasswordEncoder는 기능 구현을 하지 않고, 모든 책임을 구성 PasswordEncoder 에게 위임한다.

 

사용해보기

직접 생성하지 않고, 스프링 시큐리티가 제공하는 팩터리에서 생성해서 사용할 있다.

 

 

 

 

CustomPassword 추가

만일 커스텀한 PasswordEncoder 있다면, 직접 생성해야 한다.

 

createDelegatingPasswordEncoder() 메서드를 그대로 복사해, 커스텀 구현체를 넣어 사용하면 된다.

 

 

 

 

암호화 생성

암호화 복호화 함수와 생성 기능은 자바 언어에서 기본 제공되지 않는다.

스프링 시큐리티는 이를 위해 자체 솔루션을 제공한다.

 

생성기

문자열 기반, 바이트 기반 가지 인터페이스가 존재한다.

 

 

/복호화

 

 

사용해보기

 

import java.nio.charset.StandardCharsets;

import java.util.Arrays;

import java.util.Objects;

 

import org.springframework.security.crypto.encrypt.BytesEncryptor;

import org.springframework.security.crypto.encrypt.Encryptors;

import org.springframework.security.crypto.encrypt.TextEncryptor;

import org.springframework.security.crypto.keygen.KeyGenerators;

import org.springframework.security.crypto.keygen.StringKeyGenerator;

 

public class Test2 {

        public static void main(String[] args) {

                stringBasedEncrption();

                System.out.println();

                byteBasedEncrption();

        }

 

        private static void stringBasedEncrption() {

                System.out.println("문자열 기반 암호화");

                StringKeyGenerator keyGenerator = KeyGenerators.string();

                

                String password = "tiger";

                String solt = keyGenerator.generateKey();

                

                //암호화 하지 않음

//                TextEncryptor encryptor = Encryptors.noOpText();

                //기본

//                TextEncryptor encryptor = Encryptors.text(password, solt);

                //더 강력

                TextEncryptor encryptor = Encryptors.delux(password, solt);

                

                String plainText = "민감한 데이터";

                //encrypt() 매 실행 마다 값이 다르다. 내부적으로 난수를 사용한다.

                String cypherText1 = encryptor.encrypt(plainText);

                String cypherText2 = encryptor.encrypt(plainText);

                

                System.out.println("plainText = " + plainText);

                System.out.println("cypherText1 = " + cypherText1);

                System.out.println("cypherText2 = " + cypherText2);

                System.out.println(encryptor.decrypt(cypherText1));

                System.out.println(Objects.equals(plainText, encryptor.decrypt(cypherText1)));

        }

        private static void byteBasedEncrption() {

                System.out.println("바이트 기반 암호화");

                StringKeyGenerator keyGenerator = KeyGenerators.string();

                

                String password = "민감한 데이터";

                String solt = keyGenerator.generateKey();

                

                //기본

//                BytesEncryptor encryptor = Encryptors.standard(password, solt);

                //더 강력

                BytesEncryptor encryptor = Encryptors.stronger(password, solt);

                

                byte[] plainByte = password.getBytes(StandardCharsets.UTF_8);

                //encrypt() 매 실행 마다 값이 다르다. 내부적으로 난수를 사용한다.

                byte[] cypherByte1 = encryptor.encrypt(plainByte);

                byte[] cypherByte2 = encryptor.encrypt(plainByte);

                

                System.out.println("plainByte" + Arrays.toString(plainByte));

                System.out.println("cypherByte1 = " + Arrays.toString(cypherByte1));

                System.out.println("cypherByte2 = " + Arrays.toString(cypherByte2));

                System.out.println(new String(encryptor.decrypt(cypherByte1), StandardCharsets.UTF_8));

                System.out.println(Objects.equals(plainByte, encryptor.decrypt(cypherByte1)));

        }

}

 

 

 

 

 

spring-security-04-password.zip
0.15MB

 

사용자 관리

스프링 시큐리티에서 사용자 관리는 UserDetailsService 담당한다

사용자를 찾는 역할만 한다.

 

UserDetailsManager 인터페이스를 확장해 User 대한 CUD 기능들을 지원한다.

인터페이스의 상속 관계는 인터페이스 분리 원칙의 모범 사례다.

 

조회만 필요한 경우와 조회된 사용자에 대한 조작이 필요한 경우 나눠서 알맞게 구현하면 된다.

 

UserDetailsService.loadUserByUsername() 조회 결과로 반환되는 UserDetails는 사용자에 대한 정보가 들어있다.

이중 권한에 대한 표현은 GrantedAuthority로 한다. 그리고 권한은 복수일 있어 Collection으로 다룬다.

 

사용자 기술하기

UserDetials

UserDetails을 알맞게 기술하면 스프링 시큐리티는 사용자를 인식할 있다.

헛갈릴 있어 일부러 Authentication 맞게 코맨트를 달았다.

isAccountNonExpired() 같이 이중 부정으로 보여 헷갈린다면,

is-- 4 가지 전부 true일 때 유효하다고 기억하면 된다.

만약 계정 만료, 계정 잠금, 자격 증명 만료, 계정 /활성화 기능이 필요 없다면, 단순히 구현부를 true 리턴하게 만들면 된다.

 

 

 

 

 

authentication 메서드 안에서 UserDetails 사용하고, 메서드 결과를 반환할 때는 Authentication으로 반환한다.

 

아래는 AbstractUserDetailsAuthenticationProvider의 구현부다.

 

 

메서드는 추상 메서드로 템플릿 메서드 패턴이 적용되어 있다.

 

클래스 확장 클래스는 DaoAuthenticationProvider .

메서드에서 아래 그림과 같은 관계를 구현하고 있다.

 

 

GrantedAuthority

사용자에게 허가된 권한을 반환하는 역할을 한다.

사용자에게 권한은 여러 개일 있으므로 Collection으로 관리된다.

 

 

GrantedAuthority 추상 메서드가 하나 뿐인 함수형 인터페이스지만 @FunctionalInterface 애노테이션이 붙지 않았다.

따라서 가급적 람다를 통한 객체 생성을 지양하고, 구현체를 써야 한다.

테스트 같은 간단한 구현에선 람다를 사용하는 것은 상관없다.

 

 

UserDetails 구현

최소한의 구현

 

빌더 사용

 

어떠한 방법을 사용하건 UserDetails 인터페이스 맞게 값만 채워서 리턴하면 된다.

보통은 DB에서 조회해서 값을 채우게 것이다.

 

 

JPA UserDetails 구현

하나의 UserDetails 구현체에 JPA 엔티티 책임과 UserDetails 책임 가지를 함께 구현하면 단일 책임 원칙에 위배되며, 코드가 복잡해진다.

따라서 책임을 분리해 엔티티는 엔티티만, UserDetails 역할은 UserDetails 구현체가 하도록 하면 된다.

 

JPA 엔티티 책임만

스프링 시큐리티가 원하는 UserDetails 책임만

 

JPA 사용하는 코드와 스프링 시큐리티가 사용하는 코드가 분리됐다.

 

 

 

스프링 시큐리티가 사용자를 관리하는 방법 지정

스프링이 이해하는 UserDetails 만드는 법을 알았으니,

다음은 정보를 가져오는 방법을 알아본다.

 

 

 

UserDetailsService 구현

스프링 시큐리티가 제공하는 InMemoryUserDetailsManager 착각하면 안된다.

 

 

 

 

 

JdbcUserDetailsManager 사용

JdbcUserDetailsManager 직접 데이터베이스에 접근해서 데이터를 가져온다.

이를 위해 도커엔진 위에 Mysql 설치했다.

 

사용한 버전, password

docker run --name mysql8 -e MYSQL_ROOT_PASSWORD=root -d mysql:8.0.27

 

 

스프링 부트는 classpath 경로에 schema.sql, data.sql 있으면, 부팅하면서 대신 실행해 준다.

, application.properties "spring.sql.init.mode=always" 설정돼야 실행된다.

application.properties

spring.datasource.url=jdbc:mysql://localhost/spring?useUnicode=true&serverTimezone=Asia/Seoul

spring.datasource.username=root

spring.datasource.password=root

spring.sql.init.mode=always

 

 

data.sql

INSERT INTO `spring`.`authorities` VALUES (NULL, 'scott', 'write');

INSERT INTO `spring`.`users` VALUES (NULL, 'scott', 'tiger', '1');

 

schema.sql

CREATE DATABASE spring default CHARACTER SET UTF8;

 

CREATE TABLE IF NOT EXISTS `spring`.`users` (

  `id` INT NOT NULL AUTO_INCREMENT,

  `username` VARCHAR(45) NULL,

  `password` VARCHAR(45) NULL,

  `enabled` INT NOT NULL,

  PRIMARY KEY (`id`));

CREATE TABLE IF NOT EXISTS `spring`.`authorities` (

  `id` INT NOT NULL AUTO_INCREMENT,

  `username` VARCHAR(45) NULL,

  `authority` VARCHAR(45) NULL,

  PRIMARY KEY (`id`));

그림에서 UserDetailsService JdbcUserDetailsManager이다.

 

이제 서버 기동 테스트를 한다.

 

데이터베이스 확인

 

 

통신 확인

 

 

spring-security-02-user.zip
0.09MB
spring-security-03-user.zip
0.08MB

 

HTTP Basic

기본 구성해보기

openssl req -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365

프로젝트 생성

3가지 Starter 추가

 

컨트롤러 추가

생성 간단한 RestController 만든다.

 

 

동작확인

아무 것도 안한 상태에서 실행하면,

스프링 시큐리티가 기본 계정을 생성해준다.

ID : user  고정

PW : UUID , 실행 마다 변경된다.

 

여기서 API 툴로 API Tester를 사용했다.

위와 같이 401 코드를 반환한다.

 

Add authorization 눌러 아래와 같이 입력한다.

pw 스프링 실행 출력된 값이다.

 

이제 성공한다.

 

Basic 뒤에 값을 복사해서 아래 사이트에 붙여넣고 Decode 해보면,

ID, PW 그대로 노출된 것을 있다.

 

https://www.base64decode.org/

위와 같은 문제로, HTTP Basic 실제 현업에서 사용할 일이 없다.

 

흐름은 UsernamePasswordAuthenticationFilter 추가적으로 첨가한 그림이다.

그림을 보면, 모든 관계가 Interface 통해 이뤄지고 있다.

, 어떤 Filter 인증을 하던 간에 위와 같다.

 

AuthenticationProvider 의존하는 인터페이스는 다음과 같은 책임을 가지고 있다.

UserDetailsSerivce 주어진 username(id) 저장소에 존재하는지 확인

PasswordEncoder 주어진 자격증명(password) 자장소에 저장된 값과 같은지

 

 

기본구성 재정의

UserDetailsService, PasswordEncoder

 

@Configuration(proxyBeanMethods = false)
public class SecurityConfig {
        
        //책임_저장소에서 해당 유저가 존재하는지 확인 및 유저 권한 조회
        @Bean
        UserDetailsService userDetailsService() {
                var userDetailsService = new InMemoryUserDetailsManager();        
                userDetailsService.createUser(
                                User.withUsername("scott")
                                        .password("tiger")
                                        .passwordEncoder(pw -> passwordEncoder().encode(pw))
                                        .authorities(List.of())
                                        .build()
                                );
                
                return userDetailsService;
        }
        //책임_Password를 해싱, 입력된 Password를 해싱된 password와 비교하여 일치하는지 확인
        @Bean
        PasswordEncoder passwordEncoder() {
                return new BCryptPasswordEncoder();
        }
        
        @Bean
        SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
                http
                        .authorizeHttpRequests(authorize -> authorize
                                .anyRequest().authenticated() //모든 경로는 인증돼야 함
                        );
                http.httpBasic(Customizer.withDefaults());
                
                return http.build();
        }
}

변경된 구성 동작 확인

 

주의사항

UserDetailsService 재정의할 반드시 PasswordEncoder 재정의해야한다.

PasswordEncoder password 검증하려고, 하는데 없기 때문이다.

정확히는 아무 PasswordEncoder 등록되지 않을 등록되는 더미 UnmappedIdPasswordEncoder 예외를 던진다.

 

커스텀 AuthenticationProvider 해보기

 

 



@Component
public class CustomAuthenticationProvider implements AuthenticationProvider{
        Logger logger = LoggerFactory.getLogger(getClass());
        
        @Override
        public Authentication authenticate(Authentication authentication)
                        throws AuthenticationException {
                
                String username = authentication.getName();
                String password = authentication.getCredentials().toString();
                
                logger.info("username = " + username + ", password = " + password);
                
                if(!"scott".equals(username) || ! "tiger".equals(password)) {
                        throw new UsernameNotFoundException("인증 실패");
                }
                
                return new UsernamePasswordAuthenticationToken(username, password, List.of());
        }


        @Override
        public boolean supports(Class<?> authentication) {
                return UsernamePasswordAuthenticationToken.class
                                .isAssignableFrom(authentication);
        }
}
//SecurityConfig


        @Bean
        SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
                http
                        .authorizeHttpRequests(authorize -> authorize
                                .anyRequest().authenticated() //모든 경로는 인증돼야 함
                        );
                //인증 활성화
                http.httpBasic(Customizer.withDefaults());
                
                //커스텀 인증 제공자 등록
                http.authenticationProvider(new CustomAuthenticationProvider());
                
                return http.build();
        }

동작 확인

 

커스텀 구성에서 PasswordEncoder UserDetailsService 사용하지 않았다.

실제로는 이렇게 구성하면 안된다.

프레임워크에 설계된 아키텍처 그대로를 지켜야 한다.



@Component
public class CustomAuthenticationProvider implements AuthenticationProvider{
        private final Logger logger = LoggerFactory.getLogger(getClass());
        @Autowired
        private PasswordEncoder encoder;
        @Autowired
        private UserDetailsService userDetailsService;


        @Override
        public Authentication authenticate(Authentication authentication)
                        throws AuthenticationException {
                
                String username = authentication.getName();
                String password = authentication.getCredentials().toString();
                
                logger.info("username = " + username + ", password = " + password);
                
//                simpleAuthentication(username, password);
                
                UserDetails user = userDetailsService.loadUserByUsername(username);
                if(user == null || !encoder.matches(password, user.getPassword())) {
                        throw new UsernameNotFoundException("인증 실패");
                }
                return new UsernamePasswordAuthenticationToken(username, password, List.of());
        }


        private void simpleAuthentication(String username, String password) {
                if(!"scott".equals(username) || ! "tiger".equals(password)) {
                        throw new UsernameNotFoundException("인증 실패");
                }
        }


        @Override
        public boolean supports(Class<?> authentication) {
                return UsernamePasswordAuthenticationToken.class
                                .isAssignableFrom(authentication);
        }
}

테스트 코드



@SpringBootTest
@AutoConfigureMockMvc
class SpringSecurity01ApplicationTests {
        
        @Autowired
        private MockMvc mvc;
        
        @Autowired
        private PasswordEncoder encoder;
        
        @Test
        void HTTP_BASIC_인증실패() throws Exception {
                mvc.perform(get("/resource"))
                        .andExpect(status().isUnauthorized()); // 401
        }
        @Test
        void HTTP_BASIC_인증성공() throws Exception {
                mvc.perform(get("/resource")
                                        .with(httpBasic("scott", "tiger")))
                                .andExpect(content().string("resource"))
                                .andExpect(status().isOk());
        }
        @Test
        @WithMockUser(username = "root", password = "root")
        void HTTP_BASIC_테스트계정_인증성공() throws Exception {
                mvc.perform(get("/resource")
                                .with(httpBasic("root", "root")))
                .andExpect(content().string("resource"))
                .andExpect(status().isOk());
        }
        
        @Test
        void PasswordEncoder_테스트() {
                String password = "12345";
                assertThat(encoder.matches(password, encoder.encode(password))).isEqualTo(Boolean.TRUE);
        }
}

 

spring-security-01-httpbasic.zip
0.07MB

 

자바스크립트에서 언어 차원에서 지원하는 문법

자주 쓰이는 자료구조인 객체와 배열을 적은 코드로 쉽게 할당할 있게 해준다.

 

간단한 사용 예시

//단순히 scope를 나누기 위함
function scope1(){
    const arr = [1,2,3];
    const data1 = arr[0];
    const data2 = arr[1];
    const data3 = arr[2];
    console.log(`예전 방식`,data1,data2,data3);
    //배열분해할당
    const [a,b,c] = arr;
    console.log(`신 방식`,a,b,c);
}
scope1();

자동으로 매핑되어 할당된 것을 있다.

 

크기가 다른 경우



function scope2(){
    const arr1 = [1,2,3,4,5];
    const arr2 = [1,2,3];
    const [a1, a2, a3] = arr1;
    const [b1, b2, b3, b4, b5] = arr2;
    console.log(`배열 요소가 더 많은 경우`,a1,a2,a3);
    console.log(`변수가 더 많은 경우`, b1,b2,b3,b4,b5);
}


할당할 있는 만큼 할당된 것을 있다.

 

활용



//활용, Arguments
function scope3(a1,a2,a3){
    const [b1,b2,b3] = Array.from(arguments);
    console.log(b1,b2,b3);
}
scope3(1,2,3);
//활용, 나머지 연산자
function scope4(...args){
    const [b1,b2,b3] = args;
    console.log(b1,b2,b3);
}
scope4(1,2,3);


주의사항



//활용, Arguments 주의사항
function scope5(a1 = 1,a2 = 2,a3 = 3){
    console.log(arguments);
    const [b1,b2,b3] = Array.from(arguments);
    console.log(b1,b2,b3);
    console.log(a1,a2,a3);
}
scope5();

기본값을 활용하는 경우 위와 같이 원하던 동작 대로 동작하지 않는다.

 

객체분해할당

배열의 경우도 분해할당이 유용하지만 유용한 것은 객체분해할당이다.

 

매개변수

메서드 혹은 함수에서 좋은 매개변수 숫자는 일까?

"이펙티브 자바" 책에 따르면 좋은 매개변수 숫자는 0개이다.

다음은 1 , 다음은 2 이다.

 

매개변수 2 부터는 인자의 순서도 신경을 써야하기 때문에 사용성이 제곱으로 어려워 진다.

, 인자가 2 이상부터는 인자 순서에 의존하게 된다.

 

점은 알고 다음 예시를 보자

//객체분해할당
class Car {
    constructor(year, model, gas){
        [this.year, this.model, this.gas] = arguments;
    }
}
console.log(new Car(2023,'그렌저','가솔린'));
//사용자가 실수로 순서를 잘못 넣음
console.log(new Car('그렌저',2023,'가솔린'));

데이터가 잘못 들어간 것을 있다.



//객체분해할당, 매개변수 순서 의존 제거
class Car2 {
    constructor({year = '2022', model = '소나타', gas = '가솔린'} = {}){
        this.year = year;
        this.model = model;
        this.gas = gas;
    }
}
console.log(
    new Car2({
        year : 2023 ,
        model : '그렌저' ,
        gas : '가솔린' ,
    })
);
console.log(
    new Car2({
        gas : '가솔린' ,
        model : '그렌저' ,
        year : 2023 ,
    })
);
console.log(
    new Car2()
);

객체의 특성을 사용해 인수의 순서 의존을 제거했다.

추가로 기본값을 주어 안정성을 높였다.

 

https://ko.javascript.info/destructuring-assignment

'개발 > 자바스크립트' 카테고리의 다른 글

배열은 객체?  (0) 2023.08.18
반올림 함수  (0) 2023.04.10
평균 구하기 주의점  (0) 2023.01.21
자바스크립트 중복제거 - 2  (0) 2022.12.15
자바스크립트 중복 제거  (0) 2022.12.12

배열은 객체인 확인하기

배열은 사실 객체라고 하는데, 확인해보기

 

//배열 선언
const arr = [];
//객체 확인
console.log(typeof arr);
object

실제로 타입이 object 것을 있다.

 

객체처럼 다뤄보기1

const funcKey = function() {}
class classKey{}
//배열 객체처럼 다루기 블록 접근
arr['string'] = '리터럴';
arr[{}] = '객체';
arr[[]] = '배열';
arr[()=> 'a'] = '화살표함수';
arr[funcKey] = '함수';
arr[classKey] = '클래스';
console.log(
    Object.entries(arr)
);
[
  [ 'string', '리터럴' ],
  [ '[object Object]', '객체' ],
  [ '', '배열' ],
  [ "()=> 'a'", '화살표함수' ],
  [ 'function() {}', '함수' ],
  [ 'class classKey{}', '클래스' ]
]

배열로 객체이므로, Object.entries() 메서드가 동작한다.

toString() 결과가 key 되고 있다.

 

객체처럼 다뤄보기2

//배열 객체처럼 다뤄보기2, 점 접근
const arr2 = [];
arr2.name = "홍길동";
arr2.age = 10;
console.log(arr2);
[ name: '홍길동', age: 10 ]

 

 

객체처럼 다룬 배열을 배열처럼 사용해보기

console.log(arr.length);
arr.pop();
arr.pop();
arr.unshift();
arr.unshift();
console.log(arr);
0
[
  string: '리터럴',
  '[object Object]': '객체',
  '': '배열',
  "()=> 'a'": '화살표함수',
  'function() {}': '함수',
  'class classKey{}': '클래스'
]

기존 데이터가 배열로 인식을 length 잡히고 있다.

 

arr[9] = 10;
arr[5] = 6;
console.log(arr.length);
console.log(arr.toString());
console.log(arr.join(","));
10
,,,,,6,,,,10
,,,,,6,,,,10

객체라고 가정하고, key 숫자인 경우만 인식하여 동작하고 있다.

 

응용해보기

배열이 객체 관련된 메서드가 적용되므로, 동일한 함수를 만들면 같이 사용이 가능할

//응용해보기
const obj = {};
obj['string'] = '리터럴';
obj[{}] = '객체';
obj[[]] = '배열';
obj[()=> 'a'] = '화살표함수';
obj[funcKey] = '함수';
obj[classKey] = '클래스';
console.log(obj);
console.log("\n\n\n")
//배열과 함수를 받아 순회하는 함수
function each(objOrArr){
    const keys = Object.keys(objOrArr);
    keys.forEach(key => console.log(key, objOrArr[key]));
}
each(arr);
each(obj);


5 6
9 10
string 리터럴
[object Object] 객체
 배열
()=> 'a' 화살표함수
function() {} 함수
class classKey{} 클래스
string 리터럴
[object Object] 객체
 배열
()=> 'a' 화살표함수
function() {} 함수
class classKey{} 클래스

 

결론

배열은 객체처럼 다룰 있다.

객체처럼 쓰는 사람은 없겠지만...

확실하진 않지만, 객체는 요소에 접근할 아마도 해시를 사용할 같고,

배열은 그대로 배열로 만들어 인덱스로 접근하는 같다.

 

 

 

https://ko.javascript.info/array#ref-1135

https://www.udemy.com/course/clean-code-js/  - 배열 - 배열은 객체다.

'개발 > 자바스크립트' 카테고리의 다른 글

구조 분해 할당  (0) 2023.08.21
반올림 함수  (0) 2023.04.10
평균 구하기 주의점  (0) 2023.01.21
자바스크립트 중복제거 - 2  (0) 2022.12.15
자바스크립트 중복 제거  (0) 2022.12.12

 

스프링 시큐리티 인 액션 | 로렌티우 스필카 - 교보문고

스프링 시큐리티 인 액션 | 모든 스프링 개발자에게 권장하는 스프링 시큐리티 필수 가이드!보안은 타협할 수 없는 중요한 요소다. 스프링 시큐리티로 안전하게 데이터를 전송하고 자격 증명을

product.kyobobook.co.kr

 

보안은 실제 어플리케이션의 목적인 기능적인 요소가 아닌 비기능적인 요소지만, 필수적인 비기능적 요소다.

보안은 어플리케이션 개발 처음부터 고려해야 하는 요소다.

 

비기능적 요소

필수 요소를 제외한 요소로, 군대로 예를 들면 전투 보직을 제외한 (),(),(),(),() 이라고 있다.

 

스프링 시큐리티 : 개념과 장점

스프링 시큐리티는 유연하다. 낮은 수준의 보안에서 높은 수준의 보안까지 적용이 가능하다.

낮은 수준에서 높은 수준으로 수록, 복잡도가 증가해 유지 관리가 어려워 진다.

 

보안이라 함은 무언가를 지키는 것이다. 예를 들어 금고는 안에 내용물이 '무언가', 앞에 잠금 장치가 보안일 것이다.

스프링 시큐리티에선 데이터가 '무언가' 스프링 시큐리티가 보안이다.

 

스프링 시큐리티는 데이터가 시스템으로 들어올 데이터를 가로채 검증을 한다.

 

소프트웨어 보안이란?

 유출되면 민감한 데이터들이 있다. 이메일 주소처럼 상대적으로 민감한 데이터부터 신용 카드처럼 유출되면 민감한 데이터가 있다. 이러한 데이터는 의도된 사용자 외에는 접근을 못하게 해야한다.

 

보안에는 네트워크 보안, OS 보안 모두는 포함하는 복잡한 주제이다.

스프링 시큐리티는 사용자와 가장 가까이 맏닿아 있는 어플리케이션 보안을 다룬다.

 

모놀리식과 마이크로서비스 아키텍처

모놀리식 아키텍처는 하나의 시스템으로 모든 책임을 구현한다.

마이크로 서비스 아키텍처는 책임별로 여러 시스템을 구현한다.

 

마이크로 서비스는 확장성 유연성이 좋지만, 모놀리식에 비해 신경쓸 많아진다.

보안으로만 한정해서 보면, 시스템마다 보안 설정을 해줘야 한다. , 관리 포인트가 늘어난다.

 

스프링 시큐리티는 크게 사용자를 식별하는 인증(Authentication) 인증된 사용자에 대한 알맞은 권한부여(Authorization) 단계로 이뤄진다.

 

 

 

보안이 중요한 이유는 무엇인가?

보안을 등한 해서 해킹을 당해 정보 유출이되면, 법적인 문제, 복구 비용, 브랜드 이미지 실추 등이 발생한다.

선제적으로 보안에 신경을 썼으면, 발생하지 않았을 비용이다.

미리 보안에 신경쓰는 비용이 복구 비용보다 적다.

 

애플리케이션의 일반적인 보안 취약성

일반적인 취약성 목록

  • 인증 취약성
  • 세션 고정
  • XSS(교차 사이트 스크립팅)
  • CSRF(사이트 요청 위조)
  • 주입
  • 기밀 데이터 노출
  • 메서드 접근 제어 부족
  • 알려진 취약성이 있는 종속성 이용

인증과 권한 부여의 취약성

인증은 직관적으로 이해가 쉽다. 권한 부여만 예를 들어보면 아래와 같다.

인증은 정상적으로 수행 구매 내역을 조회하고 있다.

 

임꺽정이 홍길동 구매 내역을 확인할 있다. 이는 인증은 성공했으나, 자기 자신으로 조회만 가능하게 권한 부여를 해야 했으나, 이를 설정하지 않을 발생하는 상황이다.

세션 고정

인증 세션 ID 부여하는데 고유한 세션ID 부여하지 않아 중복이 발생하거나 기존 세션 ID 재사용 가능할 발생하는 취약성이다.

XSS

교차 사이트 스크립팅은 사이트에 스크립트를 주입해 다른 사용자가 이를 실행하도록 하는 공격이다.

다른 말로 "클라리언트 코드 삽입 공격"이라고도 한다.

 

스크립트는 "<script>악의적인 코드</script>" 의미한다.

 

, 3자의 스크립트가 실행되는 것으로 자바스크립트로 있는 모든 것이 위험하다.

개인정보 탈취, DDos 참여 등…

 

<script>태그가 실행될 있는 곳이 위험한 곳이다. (이메일, 댓글 )

 

CSRF

사이트 요청 위조는 정상적인 사이트로 로그인 로그아웃 하지 않은채

악의적인 사이트로 접근했을 , 숨겨진 스크립트가 동작해 서버로 요청을 위조하는 것이다.

 

 

애플리케이션의 주입 취약성 이해

주입 공격은 다양하다. XSS 주입 취약성 하나다. 이외도 유명한 SQL 주입이 있다.

주입 공격은 비정상 코드 묶음을 주입해 시스템을 조작하는 유형의 취약성이다.

 

민감한 데이터 노출

개발자의 부주의로 발생한다.

  • 로그에 불필요한 정보를 노출
  • application.properties 비밀번호 같은 정보같은 민감한 정보를 입력해 사용
  • 400, 500 에러에 서버의 예외 스택을 응답으로 보냄
  • 로그인 실패 ID/PW 무엇이 틀렸는지 정확히 응답

 

메서드 접근 제어 부족

일반적인 Controller - Service - Repository 계층에서 주로 Controller 계층에만 접근 제어를 한다.

이는 향후 기능 추가 접근 제어의 헛점이 생길 있다. 따라서 계층에 권한을 부여해야 한다.

 

알려진 취약성이 있는 종속성 이용

스프링 시큐리티를 포함하여, 다른 프레임 워크나 라이브러리를 사용할 사용하는 버전에 알려진 취약성이 있는 경우를 말한다. 취약성이 있는 버전은 제거해야 한다.

 

다양한 아키텍처에 적용된 보안

다양한 아키텍처 마다 요구되는 사항을 준구하면서, 스프링 키큐리티를 알맞게 구성해야 한다.

높은 보안은 복잡도와 성능을 잡아먹고, 낮은 보안은 위험도를 높이게 된다.

상황에 맞는 알맞은 구성을 적용해야 한다.

 

일체형 보안 설계

프론트엔드-백엔드 분리된 아닌 어플리케이션을 말한다.

일체형 어플리케이션은 프론트엔드와 백엔드 개발이 묶여 있어 비교적 보안 설계가 쉽다.

 

백엔드-프론트엔드 분리 보안 설계

프론트는 뷰나 리엑트를 이용해 별도로 개발하는 형태를 말한다.

경우 프론트와 백엔드는 REST API 통신을 하게되는데, 이때 반드시 백엔드와 같은 출처의 프론트엔드가 아닐 있다. 따라서 보안 설계가 복잡해진다.

 

 

+ Recent posts