Techit 12th 4th

Huisu·2023년 7월 6일
0

Techit

목록 보기
28/42
post-thumbnail

Greedy

Meeting Room

1931번: 회의실 배정

문제

한 개의 회의실이 있는데 이를 사용하고자 하는 N개의 회의에 대하여 회의실 사용표를 만들려고 한다. 각 회의 I에 대해 시작시간과 끝나는 시간이 주어져 있고, 각 회의가 겹치지 않게 하면서 회의실을 사용할 수 있는 회의의 최대 개수를 찾아보자. 단, 회의는 한번 시작하면 중간에 중단될 수 없으며 한 회의가 끝나는 것과 동시에 다음 회의가 시작될 수 있다. 회의의 시작시간과 끝나는 시간이 같을 수도 있다. 이 경우에는 시작하자마자 끝나는 것으로 생각하면 된다.

입력

첫째 줄에 회의의 수 N(1 ≤ N ≤ 100,000)이 주어진다. 둘째 줄부터 N+1 줄까지 각 회의의 정보가 주어지는데 이것은 공백을 사이에 두고 회의의 시작시간과 끝나는 시간이 주어진다. 시작 시간과 끝나는 시간은 231-1보다 작거나 같은 자연수 또는 0이다.

출력

첫째 줄에 최대 사용할 수 있는 회의의 최대 개수를 출력한다.

예제 입력 1

11
1 4
3 5
0 6
5 7
3 8
5 9
6 10
8 11
8 12
2 13
12 14

코드

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.Comparator;
import java.util.StringTokenizer;

// https://www.acmicpc.net/problem/1931
public class one1931 {
    public int solution() throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        int meetingCount = Integer.parseInt(reader.readLine());
        int[][] meetings = new int[meetingCount][2];
        for (int i = 0; i < meetingCount; i++) {
            StringTokenizer tokenizer =
                    new StringTokenizer(reader.readLine());
            meetings[i][0] = Integer.parseInt(tokenizer.nextToken());
            meetings[i][1] = Integer.parseInt(tokenizer.nextToken());
        }
        // 배열을 정렬한다
        // 회의 정보를 종료 시간 기준으로 정렬
        // 종료 시간이 같으면 시작 시간을 기준으로 정렬
        Arrays.sort(meetings,
                (o1, o2) -> {
            // o1은 {시작시간, 종료시간}
                    // o2도 {시작시간, 종료시간}
                    // 종료 시간이 다르다면 종료 시간 기준 비교
                    if (o1[1] != o2[1]) return o1[1] - o2[1];
                    // 아니라면 시작 시간 기준 비교
                    return o1[0] - o2[0];
                });

        // 답안을 저장하기 위한 용도
        int answer = 0;
        // 마지막 종료 시간을 저장하기 위한 용도
        int lastEnd = 0;

        for (int i = 0; i < meetingCount; i++) {
            // 이번 미팅이 선택이 가능한지 판단하기 위해
            // i 번쨰 미팅의 시작 시간과 현재의 lastEnd를 비교한다
            if(meetings[i][0] >= lastEnd) {
                answer++;
                lastEnd = meetings[i][1];
            }
        }

        return answer;
    }
    public static void main(String[] args) throws IOException {
        System.out.println(new one1931().solution());
    }
}

Knapsack Problem

예를 들어 도둑이 어떤 가게에서 물건을 훔치는데 가방을 가져왔다. 이 가방은 물건을 담을 수 있는 최대 무게가 정해져 있다. 가방에는 n개의 물건이 있고, 각 물건에는 무게와 값이 정해져 있다면, 가방의 무게를 넘기지 않고 최대한 많은 가치를 가진 물건을 챙기는 알고리즘이다. 이때 0-1 Knapsack은 물건마다 넣고 안 넣고의 선택지가 있어서 시간 복잡도는 2^n이고 이 경우 NP (None Polonomial) 다항식으로 시간 복잡도를 표시할 수 없을 정도로 아주 복잡해진다. 그러나 Fractional Knapsack이라면 물건을 나누어서 넣을 수 있기 때문에 탐욕 기법으로 풀이가 가능하다.

0-1 Knapsack에서 탐욕 기법을 활용하고 싶다면 서로 다른 입력마다 같은 기준을 반복해서 최적해를 구할 수 있어야 한다. 값이 비싼 물건부터 고르거나 무게가 가벼운 물건부터 고르거나 무게 당 값이 높은 물건부터 고르거나 기준이 있어야 한다.

Dijkstra Algorithm

유향 가중치 그래프에서 하나의 시작 정점에서 그래프의 다른 정점들로 도달하는 최단 경로를 찾는 알고리즘이다. 도달 가능한 가장 가까운 정점을 고르는 탐욕 알고리즘이자 선택의 결과를 기준으로 정보를 수정하는 동적 계획법의 모습도 보인다. 다익스트라 알고리즘의 시행 방법은 다음과 같다.

  1. 시작 정점을 결정하고, 시작 정점과 그래프의 다른 모든 정점 사이의 거리를 무한으로 초기화한다.
  2. 아직 방문하지 않은 정점 중 현재 도달 가능한 가장 가까운 정점을 방문한다.
  3. 방문한 정점에서 도달 가능한 주변 정점들까지의 거리를 갱신한다.
    1. 현재 정점까지 최단 거리 + 주변 정점까지의 거리
    2. 현재 기록된 주변 정점까지의 거리
    3. 중에 작은 것을 선택한다
  4. 모든 정점을 방문할 때까지 2 ~ 3을 반복한다.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.StringTokenizer;

public class Dijkstra {
    public static void main(String[] args) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer info = new StringTokenizer(reader.readLine());

        // 정점의 개수, 간선의 개수, 시작 정점
        int nodes = Integer.parseInt(info.nextToken());
        int edges = Integer.parseInt(info.nextToken());
        int start = Integer.parseInt(reader.readLine());

        // 인접 행렬: 연결되어 있을 경우 양수, 없을 경우 음수 (-1)
        int[][] adjMat = new int[nodes][nodes];
        for (int[] row: adjMat
             ) {
            Arrays.fill(row, -1);
        }

        // 인접 행렬 초기화
        for (int i = 0; i < edges; i++) {
            StringTokenizer edgeToken = new StringTokenizer(reader.readLine());
            int from = Integer.parseInt(edgeToken.nextToken());
            int to = Integer.parseInt(edgeToken.nextToken());
            int cost = Integer.parseInt(edgeToken.nextToken());

            adjMat[from][to] = cost;
        }

        // 방문 정보 visited
        boolean[] visited = new boolean[nodes];
        // 현재까지의 최소 거리 정보 dist
        int[] dist = new int[nodes];
        // 1. 모든 정점까지 아직 도달할 길이 없으므로 무한대로 초기화
        Arrays.fill(dist, Integer.MAX_VALUE);
        // 2. 시작 정점까지의 거리는 0
        dist[start] = 0;

        // 다익스트라 시작
        // 반복 기준: 아직 방문한 점이 남아 있을 때
        // -> 노드의 개수만큼 반복
        for (int i = 0; i < nodes; i++) {
            // 이번에 방문할 정점을 선택
            // -> 현재 정점들까지의 최단 경로 정보 중 가장 가까운 정점
            // 최소값 비교용 변수
            int minDist = Integer.MAX_VALUE;
            // 최소 거리 노드 index 저장용 변수
            int minDistNode = -1;
            // dist 배열을 검사해서 최단 거리 정점을 조사
            for (int j = 0; j < nodes; j++) {
                // TODO 이 for가 끝나는 시점에
                // 최단 거리가 minDist에 최단 거리 정점이 minDistNode에 기록
                if (!visited[j] && dist[j] < minDist) {
                    minDistNode = j;
                    minDist = dist[j];
                }
            }
            // 더 이상 도달 가능한 미방문 노드가 없을 떼
            if (minDistNode == -1) break;

            // 최종 선택된 노드 방문 처리
            visited[minDistNode] = true;

            for (int j = 0; j < nodes; j++) {
                // 연결되어 있지 않은 경우
                if (adjMat[minDistNode][j] == -1) continue;
                int cost = adjMat[minDistNode][j];
                // 인접 노드가 현재 가지는 최소 비용 (dist[j])
                // 현재 방문한 노드까지의 최소 비용 + 현재 노드에서 인접 노드까지 가는 최소 비용
                // 좀 더 작은 값을 dist[j] 에 업데이트
                if (dist[j] > dist[minDistNode] + cost)
                    dist[j] = dist[minDistNode] + cost;
            }
        }

        // 최종 출력
        System.out.println(Arrays.toString(dist));
    }
}

Spring Security

Spring Security

서비스에서는 사용자라는 개념이 존재한다. 회원 가입이나 로그인 로그아웃을 할 때의 기능을 대부분 Spring Security Framwork로 구현한다.

인증 (Authentication): 사용자가 자신이 누구인지를 증명하는 과정

권한 (Authorization): 사용자가 어떤 작업을 수행할 수 있는지를 결정하는 과정

인증과 권한을 스프링에서 사용할 수 있도록 해 주는 프래임워크가 Spring Security이다. 의존성을 추가하면 바로 사용할 수 있다.

아래와 같은 간단한 Controller를 작성해 본 뒤 서버를 띄워 보면 작성하지도 않는 페이지가 뜬다.

package com.example.auth;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RootController {

    // http://localhost:8080/
    @GetMapping
    public String root() {
        return "hello";
    }
}

그 이유는 모든 요청을 인증이 필요한 상태로 바꿔 버렸기 때문이다. 내가 만든 모든 url이 보호받기 시작했다고 생각하면 된다. 컨트롤러에 몇 개의 url을 더 추가해 보자.

package com.example.auth;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RootController {

    // http://localhost:8080/
    @GetMapping
    public String root() {
        return "hello";
    }

    **// http://localhost:8080/no-auth
    // 누구나 접근 가능
    @GetMapping("/no-auth")
    public String noAuth() {
        return "no auth success!";
    }

    // http://localhost:8080/re-auth
    // 인증된 사용자만 접근 가능
    @GetMapping("/re-auth")
    public String reAuth() {
        return "re auth success!";
    }**
}

이후 웹에 관련된 설정들을 관리하기 위한 빈 객체를 등록하기 위해 config 파일을 하나 생성한다.

package com.example.auth.config;

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.web.SecurityFilterChain;

@Configuration
public class WebSecurityConfig {
    @Bean 
    public SecurityFilterChain securityFilterChain(HttpSecurity http) 
        throws Exception {
        return http.build();
    }
        
}

@Bean: method의 결과를 bean 객체로 관리되게 하도록 하는 어노테이션이다. 메소드가 빈이 되는 것이 아닌 메소드의 결과가 빈 객체로 등록되는 것이다.

위 코드를 통해 HttpSecurity 가 자동으로 DI 되고 빌더 패턴처럼 사용한다.

Spring Security도 많은 변화를 겪었다. 5.7 버전 이전에는 extends WebSecurityConfigutetAdapter였다면 6.1 버전 이후에는 Builder를 사용해 Lambda를 이용한 DSL 기반 설정을 사용한다.

@EnableWebSecurity: 2.1 버전 이후로 Spring boot stater security에서 필수는 아니게 되었다.

이후 설정을 해 주는 람다 함수를 작성하면 된다. authorizeHttpRequests 함수는 HTTP 요청 허가 관련 설정을 하는 함수이다. requestMatchers는 어떤 url로 오는 요청에 대해 설정하는지이고, permitAll()은 누가 요청해도 허락하는 것이다.

@Configuration
public class WebSecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http)
        throws Exception {
        http.authorizeHttpRequests(
                authHttp -> authHttp
                        .requestMatchers(
                                "/no-auth"
                        )
                        .permitAll()
        ); // HTTP 요청 허가 관련 설정
        return http.build();
    }

}

이후 실행하면 화면이 잘 뜨는 것을 볼 수 있다.

다른 루트로 접속하면 403 Forbbiden 에러가 난다. 허가되지 않은 접근이라서 나는 에러이다.

/re-auth url에서는 인증이 된 사용자만 허가하고 싶을 때는 authenticated() 함수를 사용한다.

@Configuration
public class WebSecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http)
        throws Exception {
        http.authorizeHttpRequests(
                authHttp -> authHttp
                        .requestMatchers("/no-auth")
                        .permitAll()
                        **.requestMatchers("/re-auth")
                        .authenticated()**
        ); // HTTP 요청 허가 관련 설정
        return http.build();
    }

}

form login

그렇다면 form 형식을 통해 login 기능을 구현해 보자. 로그인은 가장 기본적인 사용자 인증 방식이다.

  1. 사용자가 로그인이 필요한 페이지로 이동
  2. 서버는 사용자를 로그인 페이지로 이동
  3. 사용자는 로그인 페이지를 통해 아이디와 비밀번호 전달
  4. 아이디와 비밀번호 확인 후 사용자를 인식

이후 로그인 전적이 있는지는 쿠키와 세션으로 관리한다. HTTP 요청에는 상태가 없고 각 요청은 독립적으로 이루어진다. 사용자 브라우저는 인증 사실을 매번 서버에 알려 줘야 한다. 서버가 HTTP 요청을 보고 얘 로그인했던 애네 하고 말해 주는 것이 아닌 브라우저가 알려 줘야 한다. 이때 등장하는 것이 쿠키와 세션의 개념이다.

Cookie VS Session
쿠키는 서버에서 사용자의 브라우저로 보내는 작은 데이터이다. 브라우저는 동일한 서버에 요청을 보낼 때 쿠키를 첨부해서 보낸다. 쿠키에 저장된 ID를 바탕으로 상태를 유지한다. 이렇게 유지되는 상태를 세션이라고 한다.

먼저 로그인 페이지를 보여 주기 위해 Controller를 정의하고 매핑을 만들어 준다. 그리고 template에 html 을 준비해 준다.

package com.example.auth;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class UserController {
    // 1. login 페이지로 온다
    // 2. login 페이지에 아이디 비밀번호를 입력한다
    // 3. 성공하면 my-profile로 이동한다
    @GetMapping("/login")
    public String loginForn() {
        return "login-form";
    }

    @GetMapping("/my-profile")
    public String myProfile() {
        return "my-profile";
    }
}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">
</head>
<body>
<!--  <h1>Sign In</h1>-->
<!--  <form th:action="@{/users/sign-in}" method="post">-->
<!--    <input type="text" name="username" placeholder="아이디">-->
<!--    <input type="password" name="password" placeholder="비밀번호">-->
<!--    <button type="submit">로그인</button>-->
<!--  </form>-->
<main class="flex-shrink-0">
  <section class="py-5">
    <div class="container px-5">
      <!-- login form-->
      <div class="bg-light rounded-3 py-5 px-4 px-md-5 mb-5">
        <div class="row gx-5 justify-content-center">
          <div class="col-lg-8 col-xl-6">
            <h1 class="text-center mb-5">로그인</h1>
            <form action="/users/login" method="post">
              <div class="form-floating mb-3">
                <input class="form-control" id="identifier" name="username" type="text" placeholder="Enter your username...">
                <label for="identifier">ID</label>
              </div>
              <div class="form-floating mb-3">
                <input class="form-control" id="password" name="password" type="password" placeholder="Enter your password...">
                <label for="password">Password</label>
              </div>
              <div class="d-grid"><button class="btn btn-primary btn-lg" id="sign-in-button" type="submit">Submit</button></div>
              <div style="margin-top: 16px; text-align: right"><a href="/users/register">회원가입</a></div>
            </form>
          </div>
        </div>

      </div>
    </div>
  </section>
</main>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa" crossorigin="anonymous"></script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
  <h1>My Profile</h1>
  <form action="/users/logout">
    <input type="submit" value="로그아웃">
  </form>
</body>
</html>

이후 WebSecurityConfig에서 로그인과 관련된 인증 설정을 변경해 준다.

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.web.SecurityFilterChain;

@Configuration
public class WebSecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http)
        throws Exception {
        http.authorizeHttpRequests(
                authHttp -> authHttp
                        .requestMatchers("/no-auth")
                        .permitAll()
                        .requestMatchers("/re-auth")
                        .authenticated()
                        .requestMatchers("/")
                        .anonymous()
        )
                **// form을 이용한 로그인 관련 설정
                .formLogin(
                        formLogin -> formLogin
                                // 로그인하는 페이지를 지정
                                .loginPage("/users/login")
                                // 로그인 성공했다면 이동하는 페이지
                                .defaultSuccessUrl("/users/my-profile")
                                // 로그인 실패에 이동하는 페이지
                                .failureUrl("/users/login?fail")
                                // 로그인 과정에서 필요한 경로들을 모든 사용자가 사용하도록 권한 설정
                                .permitAll()
                )**
        ; // HTTP 요청 허가 관련 설정
        return http.build();
    }

}

이 로그인 설정은 Spring 내부에 정의된 UserDetailsService 또는 UserDetailsManager 인터페이스를 활용해서 로그인 과정을 설정하게 된다. 지금은 Spring Boot에는 기본으로 만들어진 UserDetailsManager 의 구현체, InMemoryDetailsManager 를 사용하도록 Configuration 내부에 @Bean 메소드를 만들어 보자. 여기에 추가로 비밀번호를 암호화 하기 위한 PasswordEncoder 또한 만들어 준다.

@Configuration
public class WebSecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http)
        throws Exception {
        http.authorizeHttpRequests(
                authHttp -> authHttp
                        .requestMatchers("/no-auth")
                        .permitAll()
                        .requestMatchers("/re-auth")
                        .authenticated()
                        .requestMatchers("/")
                        .anonymous()
        )
                // form을 이용한 로그인 관련 설정
                .formLogin(
                        formLogin -> formLogin
                                // 로그인하는 페이지를 지정
                                .loginPage("/users/login")
                                // 로그인 성공했다면 이동하는 페이지
                                .defaultSuccessUrl("/users/my-profile")
                                // 로그인 실패에 이동하는 페이지
                                .failureUrl("/users/login?fail")
                                // 로그인 과정에서 필요한 경로들을 모든 사용자가 사용하도록 권한 설정
                                .permitAll()
                )
        ; // HTTP 요청 허가 관련 설정
        return http.build();
    }

    @Bean
    // 사용자 관리를 위한 인터페이스 구현체 Bean
    public UserDetailsManager userDetailsManager() {
        // Spring에서 미리 만들어 놓은 사용자 인증 서비스

        // 임시 User
        UserDetails user1 = User.withUsername("user1")
                .password(passwordEncoder().encode("password"))
                .build();
        return new InMemoryUserDetailsManager();
    }

    @Bean
    // 비밀번호 암호화를 위한 Bean
    public PasswordEncoder passwordEncoder() {
        // 기본적으로 사용자 비밀번호는 해독 가능한 형태로 관리자 눈에 보이면 안 된다
        // 기본적으로 비밀번호를 단방향 암호화하는 인코더 사용
        return new BCryptPasswordEncoder();
    }
}

이후 현재는 테스트 모드이기 때문에 CSRF (Cross Site Request Forgery)라는 해킹 공격에 대한 방어를 해제하는 코드도 작성해 준다.

package com.example.auth.config;

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.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class WebSecurityConfig {
    @Bean  // 메소드의 결과를 Bean 객체로 등록해주는 어노테이션
    public SecurityFilterChain securityFilterChain(
            // DI 자동으로 설정됨, 빌더 패턴 처럼 쓴다.
            HttpSecurity http
    )
            throws Exception {
        http
                // CSRF: Cross Site Request Forgery라는 해킹 공격에 대한 방어 제거
                **.csrf(AbstractHttpConfigurer::disable)**
                // 1. requestMatchers를 통해 설정할 URL 지정
                // 2. permitAll(), authenticated() 등을 통해 어떤 사용자가
                //    접근 가능한지 설정
                .authorizeHttpRequests(
                        authHttp -> authHttp // HTTP 요청 허가 관련 설정을 하고 싶다.
                                // requestMatchers == 어떤 URL로 오는 요청에 대하여 설정하는지
                                // permitAll() == 누가 요청해도 허가한다.
                                .requestMatchers("/no-auth")
                                .permitAll()
                                .requestMatchers(
                                        "/re-auth",
                                        "/users/my-profile"
                                )
                                .authenticated()  // 인증이 된 사용자만 허가
                                .requestMatchers("/")
                                .anonymous()  // 인증이 되지 않은 사용자만 허가
                )
                // form 을 이용한 로그인 관련 설정
                .formLogin(
                        formLogin -> formLogin
                                // 로그인 하는 페이지(경로)를 지정
                                .loginPage("/users/login")
                                // 로그인 성공시 이동하는 페이지(경로)
                                .defaultSuccessUrl("/users/my-profile")
                                // 로그인 실패시 이동하는 페이지(경로)
                                .failureUrl("/users/login?fail")
                                // 로그인 과정에서 필요한 경로들을
                                // 모든 사용자가 사용할 수 있게끔 권한설정
                                .permitAll()
                )
                // 로그인: 쿠키를 통해 세션을 생성
                // 로그아웃: 세션을 제거 (세션정보만 있으면 제거 가능)
                // 로그아웃 관련 설정
                .logout(
                        logout -> logout
                                // 로그아웃 요청을 보낼 URL
                                // 어떤 UI에 로그아웃 기능을 연결하고 싶으면
                                // 해당 UI가 /users/logout으로 POST 요청을
                                // 보내게끔
                                .logoutUrl("/users/logout")
                                // 로그아웃 성공시 이동할 URL 설정
                                .logoutSuccessUrl("/users/login")
                );
        return http.build();
    }

    @Bean
    // 사용자 관리를 위한 인터페이스 구현체 Bean
    public UserDetailsManager userDetailsManager(
            PasswordEncoder passwordEncoder
    ) {
        // 임시 User
        UserDetails user1 = User.withUsername("user1")
                .password(passwordEncoder.encode("password"))
                .build();
        // Spring 에서 미리 만들어놓은 사용자 인증 서비스
        return new InMemoryUserDetailsManager(user1);
    }

    @Bean
    // 비밀번호 암호화를 위한 Bean
    public PasswordEncoder passwordEncoder(){
        // 기본적으로 사용자 비밀번호는 해독가능한 형태로 데이터베이스에
        // 저장되면 안된다. 그래서 기본적으로 비밀번호를 단방향 암호화 하는
        // 인코더를 사용한다.
        return new BCryptPasswordEncoder();
    }
}

이후 로그인 사이트에 접속하면 로그인 기능이 구현되는 것을 볼 수 있다.

로그인과 로그아웃이 끝났으니 회원 가입을 진행해 보자. 회원 가입은 로그인 로그아웃과 다르게 설정으로 만들기가 어렵다. 지금 사용하는 inMemoryUserDetailsManager는 컴퓨터의 메모리나 쿠키에 저장하며, 컴퓨터를 종료하면 바로 데이터가 사라진다. 따라서 회원가입 기능은 따로 만들어 준다. 이를 위해 form을 만든다.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">
</head>
<body>
<main class="flex-shrink-0">
  <section class="py-5">
    <div class="container px-5">
      <!-- login form-->
      <div class="bg-light rounded-3 py-5 px-4 px-md-5 mb-5">
        <div class="row gx-5 justify-content-center">
          <div class="col-lg-8 col-xl-6">
            <h1 class="text-center mb-5">회원가입</h1>
            <form action="/users/register" method="post">
              <div class="form-floating mb-3">
                <input class="form-control" id="identifier" name="username" type="text" placeholder="Enter your identifier...">
                <label for="identifier">ID</label>
              </div>
              <div class="form-floating mb-3">
                <input class="form-control" id="password" name="password" type="password" placeholder="Enter your password...">
                <label for="password">Password</label>
              </div>
              <div class="form-floating mb-3">
                <input class="form-control" id="password-check" name="password-check" type="password" placeholder="Re-enter your password...">
                <label for="password-check">Password Check</label>
              </div>
              <div class="d-grid"><button class="btn btn-primary btn-lg" id="sign-in-button" type="submit">Submit</button></div>
              <div style="margin-top: 16px; text-align: right"><a href="/users/login">로그인</a></div>
            </form>
          </div>
        </div>

      </div>
    </div>
  </section>
</main>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa" crossorigin="anonymous"></script>
</body>
</html>

이후 Security Configration에서 다시 보안 설정을 해 준다.

package com.example.auth.config;

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.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class WebSecurityConfig {
    @Bean  // 메소드의 결과를 Bean 객체로 등록해주는 어노테이션
    public SecurityFilterChain securityFilterChain(
            // DI 자동으로 설정됨, 빌더 패턴 처럼 쓴다.
            HttpSecurity http
    )
            throws Exception {
        http
                // CSRF: Cross Site Request Forgery라는 해킹 공격에 대한 방어 제거
                .csrf(AbstractHttpConfigurer::disable)
                // 1. requestMatchers를 통해 설정할 URL 지정
                // 2. permitAll(), authenticated() 등을 통해 어떤 사용자가
                //    접근 가능한지 설정
                .authorizeHttpRequests(
                        authHttp -> authHttp // HTTP 요청 허가 관련 설정을 하고 싶다.
                                // requestMatchers == 어떤 URL로 오는 요청에 대하여 설정하는지
                                // permitAll() == 누가 요청해도 허가한다.
                                .requestMatchers("/no-auth")
                                .permitAll()
                                .requestMatchers(
                                        "/re-auth",
                                        "/users/my-profile"
                                )
                                .authenticated()  // 인증이 된 사용자만 허가
                                .requestMatchers("/")
                                .anonymous()  // 인증이 되지 않은 사용자만 허가
                                **.requestMatchers("/users/register")
                                .anonymous()**
                )
                // form 을 이용한 로그인 관련 설정
                .formLogin(
                        formLogin -> formLogin
                                // 로그인 하는 페이지(경로)를 지정
                                .loginPage("/users/login")
                                // 로그인 성공시 이동하는 페이지(경로)
                                .defaultSuccessUrl("/users/my-profile")
                                // 로그인 실패시 이동하는 페이지(경로)
                                .failureUrl("/users/login?fail")
                                // 로그인 과정에서 필요한 경로들을
                                // 모든 사용자가 사용할 수 있게끔 권한설정
                                .permitAll()
                )
                // 로그인: 쿠키를 통해 세션을 생성
                // 로그아웃: 세션을 제거 (세션정보만 있으면 제거 가능)
                // 로그아웃 관련 설정
                .logout(
                        logout -> logout
                                // 로그아웃 요청을 보낼 URL
                                // 어떤 UI에 로그아웃 기능을 연결하고 싶으면
                                // 해당 UI가 /users/logout으로 POST 요청을
                                // 보내게끔
                                .logoutUrl("/users/logout")
                                // 로그아웃 성공시 이동할 URL 설정
                                .logoutSuccessUrl("/users/login")
                );
        return http.build();
    }

    @Bean
    // 사용자 관리를 위한 인터페이스 구현체 Bean
    public UserDetailsManager userDetailsManager(
            PasswordEncoder passwordEncoder
    ) {
        // 임시 User
        UserDetails user1 = User.withUsername("user1")
                .password(passwordEncoder.encode("password"))
                .build();
        // Spring 에서 미리 만들어놓은 사용자 인증 서비스
        return new InMemoryUserDetailsManager(user1);
    }

    @Bean
    // 비밀번호 암호화를 위한 Bean
    public PasswordEncoder passwordEncoder(){
        // 기본적으로 사용자 비밀번호는 해독가능한 형태로 데이터베이스에
        // 저장되면 안된다. 그래서 기본적으로 비밀번호를 단방향 암호화 하는
        // 인코더를 사용한다.
        return new BCryptPasswordEncoder();
    }
}

이후 Controller에 GetMapping과 PostMapping을 구현해 준다.

    // 1. 사용자가 register 페이지로 온다
    // 2. 사용자가 register 페이지에 id, password, password확인을 입력
    // 3. register 페이지에서 /users/register로 post 요청
    // 4. UserDetailsManager에 새로운 사용자 정보 추가
    @GetMapping("/register")
    public String registerForm() {
        return "register-form";
    }

    @PostMapping
    public String registerPost(
            @RequestParam("username") String username,
            @RequestParam("password") String password,
            @RequestParam("password-check") String passwordCheck
    ) {
        return "redirect:/users/login";
    }
}

이후 User를 관리하고 비밀번호를 암호화하기 위해 Interface 기반으로 의존성을 주입받아서 유저를 추가한다.

    // 1. 사용자가 register 페이지로 온다
    // 2. 사용자가 register 페이지에 id, password, password확인을 입력
    // 3. register 페이지에서 /users/register로 post 요청
    // 4. UserDetailsManager에 새로운 사용자 정보 추가
    @GetMapping("/register")
    public String registerForm() {
        return "register-form";
    }

    // 어떻게 사용자를 관리하는지는 interface 기반으로 의존성 주입
    **private final UserDetailsManager manager;
    private final PasswordEncoder passwordEncoder;**

    public UserController(UserDetailsManager manager, PasswordEncoder passwordEncoder) {
        this.manager = manager;
        this.passwordEncoder = passwordEncoder;
    }
    @PostMapping
    public String registerPost(
            @RequestParam("username") String username,
            @RequestParam("password") String password,
            @RequestParam("password-check") String passwordCheck
    ) {
        **if (password.equals(passwordCheck)) {
            manager.createUser(User.withUsername(username)
                    .password(passwordEncoder.encode(password))
                    .build());
        }**
        return "redirect:/users/login";
    }
}

회원가입이 성공하거나 실패할 때마다 로그를 남기면 다음과 같이 잘 뜨는 것을 확인할 수 있다.

0개의 댓글