[KB IT's Your Life TIL] 오늘의 학습 내용 : Spring Security + DB 연동

JUN·2024년 9월 8일
0

KB IT's your life

목록 보기
9/16

오늘의 학습 내용

주요 개념 및 키워드

  • Spring Security + DB 연동
    • 기본 제공하는 것과 Custom 과정
    • security taglib
    • 필수 객체 메서드

학습한 내용

  • UsernamePasswordAuthenticationFilter: 사용자가 입력한 사용자 이름과 비밀번호를 처리하여 인증을 시도하는 필터
  • AuthenticationManager: 인증을 처리하는 핵심 관리자. 다양한 인증 프로바이더(AuthenticationProvider)를 통해 인증을
    시도
  • AuthenticationProvider: 실제로 인증을 수행하는 컴포넌트. 예를 들어, DaoAuthenticationProvider는 데이터베이스에서
    사용자 정보를 조회하여 인증을 수행
  • UserDetailService: 사용자 정보를 로드하는 서비스. 주로 데이터베이스에서 사용자 정보를 가져와 UserDetails 객체로
    반환
  • UserDetails: 인증에 필요한 사용자 정보를 담고 있는 객체. 사용자 이름, 비밀번호, 권한 등의 정보를 포함
  • GrantedAuthority: 사용자의 권한(예: ROLE_USER, ROLE_ADMIN)을 나타내는 객체
  • AuthenticationSuccessHandler: 인증이 성공했을 때 실행되는 핸들러로, 보통 로그인 후 리다이렉션 등을 처리
  • AuthenticationFailureHandler: 인증이 실패했을 때 실행되는 핸들러로, 로그인 실패 후의 처리를 담당
  • ExceptionTranslationFilter: 인증 또는 권한 부여 과정에서 발생하는 예외를 처리하는 필터
  • AccessDeniedHandler: 사용자가 접근할 권한이 없을 때(Authorization 실패) 처리하는 핸들러.

Spring Filter Chain 의 흐름


1. SecurityContextPersistenceFilter이다. 요청이 들어오면 SecurityContext 객체를 생성, 저장, 조회한다. 새로운 SecurityContext를 SecurityContextHolder에 저장한다.
2. LogoutFilter이다. 사용자의 로그아웃 요청을 처리한다. 설정된 로그아웃 URL로 오는 요청을 감시하여 해당 사용자를 로그아웃 처리한다.
3. UsernamePasswordAuthenticationFilter이다. 설정된 로그인 URL로 오는 요청을 감시하며, 사용자 인증을 처리한다. 인증 실패 시 AuthenticationFailureHandler를 실행한다.
4. DefaultLoginPageGeneratingFilter이다. 사용자가 별도의 로그인 페이지를 구현하지 않은 경우, 기본 로그인 페이지를 생성한다.
5. BasicAuthenticationFilter이다. HTTP 요청의 Basic 인증 헤더를 처리하여 결과를 SecurityContextHolder에 저장한다.
6. RememberMeAuthenticationFilter이다. SecurityContext에 인증 객체가 있는지 확인하고, RememberMe 인증 토큰으로 컨텍스트에 주입한다.
7. AnonymousAuthenticationFilter이다. SecurityContextHolder에 인증 객체가 있는지 확인하고, 필요한 경우 Authentication 객체를 주입한다.
8. SessionManagementFilter이다. 인증된 사용자의 세션을 관리한다. 세션 고정 보호, 동시 로그인 확인 등의 세션 관련 활동을 수행한다.
9. ExceptionTranslationFilter이다. 필터 체인 내에서 발생하는 모든 보안 관련 예외(AccessDeniedException, AuthenticationException)를 처리한다.
10. FilterSecurityInterceptor이다. HTTP 리소스의 보안 처리를 수행한다. 접근 권한을 확인하고 권한이 없는 경우 예외를 발생시킨다.
11. 사용자 접근 권한이 업데이트되면 해당 업데이트 정보를 SecurityContext Repository에 업데이트한다.

이후 SecurityContextPersistenceFilter 로 사용자의 접근이 들어오면 해당 Repository를 조회하여 사용자의 최신 권한 정보를 가져와 인증 과정에 반영한다

보안 에러 발생 시

실습

파일 구성

1. 프로젝트 초기 설정

1.1. 필요한 의존성 추가

우선 Spring Boot 프로젝트의 build.gradle 또는 pom.xml 파일에 아래와 같은 의존성을 추가

// build.gradle
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.4'
    implementation 'mysql:mysql-connector-java'
    implementation 'org.springframework.boot:spring-boot-starter-data-rest'
    implementation 'org.springframework.security:spring-security-core'
    implementation 'org.springframework.security:spring-security-config'
    implementation 'org.springframework.security:spring-security-web'
    implementation 'org.springframework.boot:spring-boot-starter-logging'
}

1.2. MySQL 데이터베이스 연결 설정

application.yml 파일에 MySQL 데이터베이스 설정을 추가

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/securitydb
    username: yourUsername
    password: yourPassword
    driver-class-name: com.mysql.cj.jdbc.Driver
  mybatis:
    mapper-locations: classpath:mapper/**/*.xml
    type-aliases-package: org.scoula.security.account.domain

2. DB 테이블 구성

MySQL에 사용자 정보와 권한 정보를 담을 테이블을 생성

tbl_member 테이블은 사용자 정보를 저장하고, tbl_member_auth는 사용자 권한을 관리한다.

CREATE TABLE tbl_member (
    username VARCHAR(50) NOT NULL PRIMARY KEY,
    password VARCHAR(128) NOT NULL,
    email VARCHAR(50) NOT NULL,
    reg_date DATETIME DEFAULT CURRENT_TIMESTAMP,
    update_date DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

CREATE TABLE tbl_member_auth (
    username VARCHAR(50) NOT NULL,
    auth VARCHAR(50) NOT NULL,
    PRIMARY KEY(username, auth),
    FOREIGN KEY (username) REFERENCES tbl_member(username)
);

3. VO(Value Object) 구성

3.1. AuthVO.java

권한 정보를 담는 클래스

package org.scoula.security.account.domain;

import lombok.Data;
import org.springframework.security.core.GrantedAuthority;

@Data
public class AuthVO implements GrantedAuthority {
    private String username;
    private String auth;

    @Override
    public String getAuthority() {
        return auth;
    }
}

3.2. MemberVO.java

사용자 정보를 담는 클래스

package org.scoula.security.account.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;
import java.util.List;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class MemberVO {
    private String username;
    private String password;
    private String email;
    private Date regDate;
    private Date updateDate;

    private List<AuthVO> authList;  // 권한 목록
}

4. MyBatis Mapper 설정

4.1. mybatis-config.xml

MyBatis 설정 파일로, 데이터베이스 테이블과 VO 클래스 간 매핑 설정을 작성

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "<http://mybatis.org/dtd/mybatis-3-config.dtd>">
<configuration>
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
    <typeAliases>
        <package name="org.scoula.security.account.domain"/>
    </typeAliases>
</configuration>

4.2. UserDetailsMapper.xml

사용자 정보와 권한을 가져오는 SQL 매퍼

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "<http://mybatis.org/dtd/mybatis-3-mapper.dtd>">
<mapper namespace="org.scoula.security.account.mapper.UserDetailsMapper">

    <resultMap id="authMap" type="AuthVO">
        <result property="username" column="username"/>
        <result property="auth" column="auth"/>
    </resultMap>

    <resultMap id="memberMap" type="MemberVO">
        <id property="username" column="username"/>
        <result property="password" column="password"/>
        <result property="email" column="email"/>
        <result property="regDate" column="reg_date"/>
        <result property="updateDate" column="update_date"/>
        <collection property="authList" resultMap="authMap"/>
    </resultMap>

    <select id="get" resultMap="memberMap">
        SELECT m.username, password, email, reg_date, update_date, auth
        FROM tbl_member m
        LEFT OUTER JOIN tbl_member_auth a ON m.username = a.username
        WHERE m.username = #{username}
    </select>
</mapper>

5. Spring Security 설정

5.1. SecurityConfig.java

Spring Security 설정 파일

사용자 권한 및 인증 절차를 정의

package org.scoula.security.config;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
@Log4j
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final UserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/security/all").permitAll()
                .antMatchers("/security/admin").hasRole("ADMIN")
                .antMatchers("/security/member").hasRole("MEMBER")
                .and()
                .formLogin().loginPage("/security/login").defaultSuccessUrl("/")
                .and()
                .logout().logoutSuccessUrl("/security/logout")
                .and()
                .csrf().disable(); // 개발 시엔 csrf를 임시로 disable
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
}

6. 사용자 정의 UserDetailsService

6.1. CustomUser.java

사용자 정보를 관리하는 UserDetails를 확장한 클래스

package org.scoula.security.account.domain;

import lombok.Getter;
import lombok.Setter;
import org.springframework.security.core.userdetails.User;

import java.util.Collection;

@Getter
@Setter
public class CustomUser extends User {
    private MemberVO member;

    public CustomUser(MemberVO vo) {
        super(vo.getUsername(), vo.getPassword(), vo.getAuthList());
        this.member = vo;
    }
}

6.2. CustomUserDetailsService.java

UserDetailsService 인터페이스를 구현하여 사용자 정보를 가져오는 클래스

package org.scoula.security.service;

import lombok.RequiredArgsConstructor;
import org.scoula.security.account.domain.CustomUser;
import org.scoula.security.account.domain.MemberVO;
import org.scoula.security.account.mapper.UserDetailsMapper;
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
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private final UserDetailsMapper mapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        MemberVO vo = mapper.get(username);
        if (vo == null) {
            throw new UsernameNotFoundException("User not found: " + username);
        }
        return new CustomUser(vo);
    }
}

7. SecurityController

7.1. SecurityController.java

로그인 및 권한 기반 페이지 접근을 처리하는 컨트롤러

package org.scoula.controller;

import lombok.extern.log4j.Log4j;
import org.scoula.security.account.domain.CustomUser;
import org.scoula.security.account.domain.MemberVO;
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("/security")
@Log4j
public class SecurityController {

    @GetMapping("/all")
    public String doAll() {
        log.info("모든 사용자 접근 가능");
        return "all";
    }

    @GetMapping("/member")
    public String doMember(@AuthenticationPrincipal CustomUser customUser) {
        MemberVO member = customUser.getMember();
        log.info("멤버 페이지 접근 - 사용자 정보: " + member.getUsername());
        return "member";
    }

    @GetMapping("/admin")
    public String doAdmin(@AuthenticationPrincipal CustomUser customUser) {
        MemberVO member = customUser.getMember();
        log.info("관리자 페이지 접근 - 사용자 정보: " + member.getUsername());
        return "admin";
    }

    @GetMapping("/login")
    public String login() {
        log.info("로그인 페이지");
        return "login";
    }

    @GetMapping("/logout")
    public String logout() {
        log.info("로그아웃 페이지");
        return "logout";
    }
}
  • /all: 모든 사용자에게 접근 허용된 페이지.
  • /member: 멤버 권한을 가진 사용자만 접근 가능한 페이지.
  • /admin: 관리자 권한을 가진 사용자만 접근 가능한 페이지.
  • /login: 사용자 로그인 페이지.
  • /logout: 사용자 로그아웃 페이지.

8. Thymeleaf 템플릿 설정 (HTML)

8.1. login.html

사용자가 로그인할 수 있는 페이지

<!DOCTYPE html>
<html xmlns:th="<http://www.thymeleaf.org>">
<head>
    <meta charset="UTF-8">
    <title>로그인</title>
</head>
<body>
<h1>로그인 페이지</h1>
<form th:action="@{/security/login}" method="post">
    <label>아이디: <input type="text" name="username"></label><br>
    <label>비밀번호: <input type="password" name="password"></label><br>
    <button type="submit">로그인</button>
</form>
</body>
</html>

8.2. member.html

멤버 권한 사용자 전용 페이지

<!DOCTYPE html>
<html xmlns:th="<http://www.thymeleaf.org>">
<head>
    <meta charset="UTF-8">
    <title>멤버 페이지</title>
</head>
<body>
<h1>멤버 전용 페이지</h1>
<p>환영합니다, 멤버님!</p>
<a th:href="@{/security/logout}">로그아웃</a>
</body>
</html>

8.3. admin.html

관리자 권한 사용자 전용 페이지

<!DOCTYPE html>
<html xmlns:th="<http://www.thymeleaf.org>">
<head>
    <meta charset="UTF-8">
    <title>관리자 페이지</title>
</head>
<body>
<h1>관리자 전용 페이지</h1>
<p>환영합니다, 관리자님!</p>
<a th:href="@{/security/logout}">로그아웃</a>
</body>
</html>

9. 실행 및 테스트

이제 모든 설정이 완료되었습니다. 프로젝트를 실행하고 다음 단계를 통해 동작을 확인

  1. 서버 실행: SpringBootApplication 클래스를 실행하여 서버를 실행
  2. 로그인 테스트: 브라우저에서 /security/login으로 이동하여 로그인 페이지가 정상적으로 표시되는지 확인
  3. 접근 권한 확인:
    • /security/all: 모든 사용자 접근 가능.
    • /security/member: 로그인 후 멤버 권한 사용자만 접근 가능.
    • /security/admin: 로그인 후 관리자 권한 사용자만 접근 가능.

새롭게 알게 된 점

  • Spring Security와 MyBatis를 연동하여 데이터베이스 기반의 사용자 인증 및 권한 관리 시스템을 구축하는 방법에 대해 자세히 알게 되었다⁠

오늘의 회고

  • Spring Security의 복잡성에 대해 더 깊이 이해하게 되었다. 초기 설정이 복잡할 수 있지만, 이를 통해 강력하고 유연한 보안 시스템을 구축할 수 있다는 점을 느꼈다.
  • 데이터베이스와 연동된 사용자 인증 시스템을 구현함으로써, 실제 프로덕션 환경에서 사용될 수 있는 보안 시스템에 대한 이해도가 높아졌다.

profile
순간은 기록하고 반복은 단순화하자 🚀

0개의 댓글