목표
mariadb & mybatis 연동

 

1. pom.xml

...

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>org.mariadb.jdbc</groupId>
            <artifactId>mariadb-java-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.0.1</version>
        </dependency>
          
    </dependencies>

...

 

2. application.yml

spring :    
    devtools :
        livereload :
            enalbed : true
    thymeleaf:
        cache : false       
    datasource:
        hikari :
            maximum-pool-size : 4
        url : jdbc:mariadb://localhost:3306/bamdule
        username : root
        password : 12345678
        
        
mybatis :
    type-aliases-package : com.bamdule.model
    mapper-locations : mapper/xml/*.xml

 

3. mapper-member.xml

mybatis를 이용해서 테이블 쿼리를 정의하는 XML 파일 생성 

1) src/main/resources/mapper/xml 디렉토리 생성
2) xml 디렉토리에 mapper-member.xml 파일 생성
<!DOCTYPE mapper
	PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
	"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.bamdule.db.mapper.MemberMapper">
    
    <select id="selectMember" resultType="map" >
        SELECT *
        FROM member
    </select>
</mapper>

Member 테이블 명세

CREATE TABLE `member` (
	`id` INT(11) NOT NULL AUTO_INCREMENT,
	`email` VARCHAR(100) NOT NULL COLLATE 'utf8_general_ci',
	`PASSWORD` VARCHAR(100) NOT NULL COLLATE 'utf8_general_ci',
	`NAME` VARCHAR(100) NOT NULL COLLATE 'utf8_general_ci',
	PRIMARY KEY (`id`) USING BTREE,
	UNIQUE INDEX `email` (`email`) USING BTREE
);

 

4. Member Mapper 생성  

1) com.bamdule.db.mapper package 생성
2) mapper 안에 MemberMapper Interface 생성
3) Mapper 컴퍼넌트 선언 후 selectMember 메소드 정의
package com.bamdule.db.mapper;

import java.util.Map;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface MemberMapper {

    public Map<String, Object> selectMember();
}

 

5. Member Service 생성

1) com.bamdule.db.service.impl package 생성
2 MemberService와 MemberServiceImpl 클래스 생성
package com.bamdule.db.service;

import java.util.Map;

public interface MemberService {

    Map<String, Object> selectMember();

}
package com.bamdule.db.service.impl;

import com.bamdule.db.mapper.MemberMapper;
import com.bamdule.db.service.MemberService;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class MemberServiceImpl implements MemberService {

    @Autowired
    private MemberMapper memeberMapper;

    @Override
    public Map<String, Object> selectMember() {
        return memeberMapper.selectMember();
    }

}

 

6. HomeController에서 selectMember 결과 콘솔 출력

package com.bamdule.controller;

import com.bamdule.db.service.MemberService;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/")
public class HomeController {

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

    @Autowired
    private MemberService memberService;

    @GetMapping("/")
    public String homeView() {
        Map<String, Object> member = memberService.selectMember();
        logger.info("[MYTEST] {}", member);
        return "page/home";
    }

}

 

logback 설정을 하지 않은 분은 아래 링크를 참고하여 설정해주세요.

2020/01/03 - [IT/Spring] - [Spring Boot] Logback 설정 방법

 

7. 결과

[MYTEST] {PASSWORD=123, id=1, email=test, NAME=kim}

Spring Security 란?

Spring Security는 스프링 기반의 어플리케이션 보안을 담당하는 프레임워크입니다. Spring Security를 사용하면 사용자 인증, 권한, 보안처리를 간단하지만 강력하게 구현 할 수 있습니다.

Spring Boot + Hibernate + SpringSecurity + thymeleaf + mariadb를 이용해 간단한 회원 가입 및 로그인 기능을 구현해보겠습니다.

pom.xml

    ...
    <version>2.1.9.RELEASE</version>
    ...
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        
        <dependency>
            <groupId>org.mariadb.jdbc</groupId>
            <artifactId>mariadb-java-client</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        
<!--        thymeleaf에서 Security 명령어를 사용하기 위해 포함-->
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        </dependency>
    </dependencies>
    ...

 


application.yml

spring :
    datasource :
        url : jdbc:mariadb://localhost:3306/local_toy
        username : tester
        password : 1234
    jpa :     
        hibernate :
            ddl-auto : update  

Spring Security 설정하기

Spring Security는 FilterChainProxy라는 이름으로 내부에 여러 Filter들이 동작하고 있습니다.

Spring Security는 Filter를 이용해서 기본적인 기능을 간단하게 구현할 수 있습니다. 

설정은 WebSecurityConfigurerAdapter 클래스를 상속받아 오버라이딩하는 방식으로 진행할 수 있습니다.

(Spring Security Filter 및 동작 설명을 알고 싶다면 아래 링크로 이동해주세요.)

2020/02/07 - [IT/Spring] - [Spring Boot] Spring Security의 동작

참조 https://spring.io/guides/topicals/spring-security-architecture#_web_security

클라이언트가 서버에 데이터를 요청하면 DispatcherServlet에 전달되기 이전에 여러 ServletFilter를 거칩니다.

이때 Spring Security에 등록했었던 Filter를 이용해 사용자 보안 관련된 처리를 진행하는데, 연결된 여러개의 Filter들로 구성 되어있어서 FilterChain 이라고 부릅니다.

SpringConfig.java

import com.example.demo.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
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.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MemberService memberService;

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

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/css/**", "/js/**", "/img/**", "/lib/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.authorizeRequests()
                .antMatchers("/member/**").authenticated()
                .antMatchers("/admin/**").authenticated()
                .antMatchers("/**").permitAll();

        http.formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/")
                .permitAll();

        http.logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .logoutSuccessUrl("/login")
                .invalidateHttpSession(true);

        http.exceptionHandling()
                .accessDeniedPage("/denied");
    }

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

}

Spring Securiry 설정 활성화하기

@Configuration 
해당 클래스를 Configuration으로 등록합니다.
@EnableWebSecurity 
Spring Security를 활성화 시킵니다.
@EnableGlobalMethodSecurity(prePostEnabled = true)
Controller에서 특정 페이지에 특정 권한이 있는 유저만 접근을 허용할 경우 @PreAuthorize 어노테이션을 사용하는데, 해당 어노테이션에 대한 설정을 활성화시키는 어노테이션입니다. (필수는 아닙니다.)

MemberService

로그인 요청 시, 입력된 유저 정보와 DB의 회원정보를 비교해 인증된 사용자인지 체크하는 로직이 정의되어있습니다.

BCryptPasswordEncoder

비밀번호를 복호화/암호화하는 로직이 담긴 객체를 Bean으로 등록합니다.


WebSecurity, HttpSecurity, AuthAuthenticationManagerBuilder configure 설정

오버라이딩configure 메소드들에 대해 설명하겠습니다.

WebSecurity

WebSecurity는 FilterChainProxy를 생성하는 필터입니다. 다양한 Filter 설정을 적용할 수 있습니다.

web.ignoring().antMatchers("/css/**", "/js/**", "/img/**", "/lib/**");  

위 설정을 통해 Spring Security에서 해당 요청은 인증 대상에서 제외시킵니다.

HttpSecurity

HttpSecurity를 통해 HTTP 요청에 대한 보안을 설정할 수 있습니다.

http.authorizeRequests()
    .antMatchers("/member/**").authenticated()
    .antMatchers("/admin/**").authenticated()
    .antMatchers("/**").permitAll();

http 요청에 대해서 모든 사용자가 /** 경로로 요청할 수 있지만, /member/** , /admin/** 경로는 인증된 사용자만 요청이 가능합니다. 

authorizeRequests()
HttpServletRequest 요청 URL에 따라 접근 권한을 설정합니다. 
antMatchers("pathPattern")
요청 URL 경로 패턴을 지정합니다.
authenticated()
인증된 유저만 접근을 허용합니다.
permitAll()
모든 유저에게 접근을 허용합니다.
anonymous()
인증되지 않은 유저만 허용합니다.
denyAll()
모든 유저에 대해 접근을 허용하지 않습니다.

로그인 설정을 진행합니다.

http.formLogin()
    .loginPage("/login")
    .defaultSuccessUrl("/")
    .permitAll();

 

formLogin()
form Login 설정을 진행합니다.
loginPage("path")
커스텀 로그인 페이지 경로와 로그인 인증 경로를 등록합니다.
defaultSuccessUrl("path")
로그인 인증을 성공하면 이동하는 페이지를 등록합니다.

로그아웃 설정을 진행합니다.

http.logout()
    .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
    .logoutSuccessUrl("/login")
    .invalidateHttpSession(true);
logout()
로그아웃 설정을 진행합니다.
logoutRequestMatcher(new AntPathRequestMatcher("path"))
로그아웃 경로를 지정합니다.
logoutSuccessUrl("/path")
로그아웃 성공 시 이동할 경로를 지정합니다.
invalidateHttpSession(true)
로그아웃 성공 시 세션을 제거합니다.

권한이 없는 사용자가 접근했을 경우 이동할 경로를 지정합니다.

http.exceptionHandling()
    .accessDeniedPage("/denied");

AuthenticationManagerBuilder

AuthenticationManager를 생성합니다. AuthenticationManager는 사용자 인증을 담당합니다.

auth.userDetailsService(service)org.springframework.security.core.userdetails.UserDetailsService 인터페이스를 구현한 Service를 넘겨야합니다. 

auth.userDetailsService(memberService).passwordEncoder(passwordEncoder());

로그인 인증을 위한 회원 정보 체크 로직

비밀번호를 체크하는 로직을 구현하려면 UserDetailsService 인터페이스를 구현한 클래스가 필요합니다.

해당 기능을 구현하기 위해 Member Entity와 Repository, Service 등을 구현해보겠습니다.

Member.java

import java.time.LocalDateTime;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "tb_member")
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column(length = 255, nullable = false)
    private String name;

    @Column(length = 255, nullable = false, unique = true)
    private String account;

    @Column(length = 255, nullable = false)
    private String password;

    @Column(name = "last_access_dt")
    private LocalDateTime lastAccessDt;

    @Column(name = "reg_dt")
    private LocalDateTime regDt;

    public Member() {
    }

    public Member(Integer id, String name, String account, String password) {
        this.id = id;
        this.name = name;
        this.account = account;
        this.password = password;
    }
    
    //getter,setter 생략
}

 

MemberTO.java

import com.example.demo.model.entity.Member;
import java.time.LocalDateTime;

public class MemberTO {

    private Integer id;

    private String name;

    private String account;

    private String password;

    private LocalDateTime lastAccessDt;

    private LocalDateTime regDt;

    public Member toEntity() {
        return new Member(id, name, account, password);
    }
//getter,setter 생략
}

 

MemberDao.java

import com.example.demo.model.entity.Member;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberDao extends JpaRepository<Member, Integer> {
    Optional<Member> findByAccount(String account);
}

 

MemberService.java

import com.example.demo.model.TO.MemberTO;
import org.springframework.security.core.userdetails.UserDetailsService;

public interface MemberService extends UserDetailsService {
    Integer save(MemberTO memberTO);
}

 

MemberServiceImpl.java

import com.example.demo.dao.MemberDao;
import com.example.demo.model.TO.MemberTO;
import com.example.demo.model.entity.Member;
import com.example.demo.service.MemberService;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class MemberServiceImpl implements MemberService {

    @Autowired
    private MemberDao memberDao;

    @Override
    public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException {
        Optional<Member> memberEntityWrapper = memberDao.findByAccount(account);
        Member memberEntity = memberEntityWrapper.orElse(null);

        List<GrantedAuthority> authorities = new ArrayList<>();

        authorities.add(new SimpleGrantedAuthority("ROLE_MEMBER"));

        return new User(memberEntity.getAccount(), memberEntity.getPassword(), authorities);
    }

    @Transactional
    @Override
    public Integer save(MemberTO memberTO) {
        Member member = memberTO.toEntity();
        member.setLastAccessDt(LocalDateTime.now());
        member.setRegDt(LocalDateTime.now());

        // 비밀번호 암호화
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        member.setPassword(passwordEncoder.encode(member.getPassword()));
        return memberDao.save(member).getId();
    }
}

loadUserByUsernae 메소드는 입력한 account를 이용해 회원을 조회합니다. 그리고 회원 정보와 권한 정보가 담긴 User 클래스를 반환합니다. (User 클래스는 UserDetails 인터페이스를 구현하고 있습니다.)

비밀번호 인증은 SpringSecurity의 AuthenticationProvider 객체에서 진행합니다. 직접 커스텀해서 비밀번호 인증 로직을 구현할 수 있지만, 이번에는 기본적으로 지원하는 AuthenticationProvide를 사용하겠습니다.   

 

HomeController.java

import com.example.demo.model.TO.MemberTO;
import com.example.demo.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping(value = "/")
public class HomeController {

    @Autowired
    private MemberService memberService;

    @GetMapping("/")
    public String homeView() {
        return "pages/home";
    }

    @GetMapping("/login")
    public String loginView() {
        return "pages/login";
    }

    @GetMapping("/signup")
    public String signupView() {
        return "pages/signup";
    }

    @PostMapping("/signup")
    public String signup(MemberTO memberTO) {
        memberService.save(memberTO);
        return "redirect:/login";
    }

    @PreAuthorize("hasRole('ROLE_MEMBER')")
    @GetMapping("/member/info")
    public String userInfoView() {
        return "pages/user_info";
    }

    @PreAuthorize("hasRole('ROLE_ADMIN')")
    @GetMapping("/admin")
    public String adminView() {
        return "pages/admin";
    }

    @GetMapping("/denied")
    public String deniedView() {
        return "pages/denied";
    }
}

"src/main/resources/templates/pages/" 경로에 html 파일을 저장해주세요.

home.html - 메인화면

<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml" 
      xmlns:sec="http://www.w3.org/1999/xhtml">
    <head>
        <meta charset="UTF-8">
        <title>Home</title>
    </head>
    <body>
        <h1>Home Page</h1>
        <hr>
        <div>
            <a sec:authorize="isAnonymous()" th:href="@{/login}">로그인</a>
            <a sec:authorize="isAuthenticated()" th:href="@{/logout}">로그아웃</a>
            <a sec:authorize="isAnonymous()" th:href="@{/signup}">회원가입</a>
        </div>
        <div>
            <a th:href="@{/member/info}">내 정보</a>
            <a th:href="@{/admin}">관리자</a>
        </div>
    </body>
</html>

 

login.html - 로그인 화면

<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml">
    <head>
        <meta charset="UTF-8">
        <title>로그인 페이지</title>
    </head>
    <body>
        <h1>로그인</h1>
        <hr>
        <form th:action="@{/login}" method="post">
            <input type="text" name="username" placeholder="account를 입력해주세요.">
            <input type="password" name="password" placeholder="password를 입력해주세요.">
            <button type="submit">로그인</button>
        </form>
    </body>
</html>

form 요청을 할 경우 csrf 토큰을 함께 보내야합니다. 하지만 th:action으로 요청을 할 경우 자동으로 생성해줍니다.

<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}">

th:action으로 요청하지 않을 경우 form태그 아래에 위와 같은 input 태그를 입력해야합니다.

 

signup.html - 회원가입 화면

<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml">
    <head>
        <meta charset="UTF-8">
        <title>회원가입 페이지</title>
    </head>
    <body>
        <h1>회원 가입</h1>
        <hr>

        <form th:action="@{/signup}" method="post">
            <input type="text" name="account" placeholder="account 입력">
            <input type="text" name="name" placeholder="user name 입력">
            <input type="password" name="password" placeholder="password 입력">
            <button type="submit">가입하기</button>
        </form>
    </body>
</html>

 

user_info.html - 내 정보 화면

<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml">
    <head>
        <meta charset="UTF-8">
        <title>내 정보</title>
    </head>
    <body>
        <h1>내 정보</h1>
        <hr>
        <span sec:authentication="name"></span> 님 반갑습니다.
        <div sec:authentication="principal.authorities"></div>

        <a th:href="@{/}">홈으로 가기</a>
    </body>
</html>

 

admin.html - 어드민 화면

<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml">
    <head>
        <meta charset="UTF-8">
        <title>admin</title>
    </head>
    <body>
        <h1>admin</h1>
        <hr>
    </body>
</html>

 

denied.html - 권한 거부 화면

<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml">
    <head>
        <meta charset="UTF-8">
        <title>접근 거부</title>
    </head>
    <body>
        <h1>접근 불가 페이지입니다.</h1>
        <hr>
        <a th:href="@{/}">홈으로 가기</a>
    </body>
</html>

결과 화면

관리자 페이지 진입 시


참조

https://victorydntmd.tistory.com/328

Spring Security 란?

Spring Security는 스프링 기반의 어플리케이션 보안을 담당하는 프레임워크이다.

Spring Security를 사용하면 사용자 인증, 권한, 보안처리를 간단하지만 강력하게 구현 할 수 있다. 

Filter 기반으로 동작하기 때문에 Spring MVC와 분리되어 동작한다. 

Spring Security를 이해하기 위해서는 먼저 보안관련 용어를 숙지해야 한다.

접근 주체(Principal)
보안 시스템이 작동되고 있는 애플리케이션에 접근하는 유저
인증(Authentication)
접근한 유저를 식별하고, 애플리케이션에 접근할 수 있는지 검사
인가(Authorize)
인증된 유저가 애플리케이션의 기능을 이용할 수 있는지 검사

SecurityFilterChain

일반적으로 브라우저가 서버에 데이터를 요청하면 DispatcherServlet에 전달되기 이전에 여러 ServletFilter를 거친다.

이때 Spring Security에서 등록했었던 Filter를 이용해 사용자 보안 관련된 처리를 진행한다.

Spring Security와 관련된 Filter들은 연결된 여러 Filter들로 구성되어있다. 이 때문에 Chain이라는 표현을 사용하고 있다.

SecurityFilterChain 상세

 

SecurityContextPersistenceFilter
SecurityContextRepository에서 SecurityContext를 가져와 유저 Authentication에 접근 할 수 있게 한다.
LogoutFilter
로그아웃 요청을 처리한다.
UsernamePasswordAuthenticationFilter 
ID와 Password를 사용하는 Form 기반 유저 인증을 처리한다. 
DefaultLoginPageGeneratingFilter
커스텀 로그인 페이지를 지정하지 않았을 경우 Default Login Page를 반환한다.
AnonymousAuthenticationFilter
이 필터가 호출되는 시점까지 사용자 정보가 인증되지 않았다면 익명 사용자 토큰을 반환한다.
ExceptionTranslationFilter
필터 체인 내에서 발생되는 모든 예외(AccessDeniedException, AuthenticationException...)를 처리한다.
FilterSecurityInterceptor
권한부여와 관련한 결정을 AccessDecisionManager에게 위임해 권한부여 결정 및 접근 제어를 처리한다.
RequestCacheAwareFilter
로그인 성공 후, 이전 요청 정보를 재구성하기 위해 사용한다.
SessionManagementFilter
로그인 이후 인증된 사용자인지 확인하거나 설정된 Session 메커니즘에 따라 작업을 수행한다. (동시 로그인 확인 등...)
BasicAuthenticationFilter 
HTTP 요청의 인증 헤더를 처리하여 결과를 SecurityContextHolder에 저장한다. (HttpBasic 방식) 
RememberMeAuthenticationFilter
세션이 사라지거나 만료 되더라도 쿠키 또는 DB를 사용하여 저장된 토큰 기반으로 인증을 처리하는 필터

Spring Security 동작

1. AuthenticationFilter (UsernamePasswordAuthenticationFilter)는 사용자의 요청을 가로챈다. 그리고 인증이 필요한 요청이라면 사용자의 JSESSIONID가 Security Context에 있는지 판단한다. 없으면 로그인 페이지로 이동시킨다

로그인 페이지에서 요청이 온 경우라면 로그인 페이지에서 입력받은 username과 password를 이용해 UsernamePasswordAuthenticationToken을 만든다.  그리고 UsernamePasswordAuthenticationToken 정보가 유효한 계정인지 판단하기 위해 AuthenticationManager로 전달한다.


2. AuthenticationManager 인터페이스의 구현체는 ProviderManger이고  AuthencationProvider에게 비밀번호 인증 로직 책임을 넘긴다. (AuthencationProvider는 개발자가 직접 커스텀해서 비밀번호 인증로직을 직접 구현할 수 있다.)

3. AuthencationProvider는 UserDetailsService를 실행해 비밀번호 인증 로직을 처리한다.

UserDetailsService는 DB에 저장된 회원의 비밀번호와 비교해 일치하면 UserDetails 인터페이스를 구현한 객체를 반환하는데, UserDetailsService는 인터페이스이며 UserDetailsService를 구현한 서비스를 직접 개발해야한다.

4. 인증 로직이 완료되면 AuthenticationManager는 Authentication를 반환하며, 결과적으로 SecurityContext에 사용자 인증 정보가 저장된다.

5. 인증 과정이 끝났으면 AuthenticationFilter에게 인증에 대한 성공 유무를 전달하고
성공하면 AuthenticationSuccessHandler를 호출하고 실패하면 AuthenticationFailureHandler를 호출한다.


참조

https://sjh836.tistory.com/165?category=680970

https://okky.kr/article/382738

https://tramyu.github.io/java/spring/spring-security/

Spring Boot에서 Hibernate 테스트를 하고 있던 도중 예기치못한 상황을 마주했습니다.

영속성이 끝났다고 생각되는 시점에서 프록시 객체를 조회하면 LazyInitializationException - No Session에러가 발생해야 하는데 Select Query가 실행되는 것이었습니다.

웹 검색을 통해 다음과 같은 사실을 알았습니다.

Open Session In View

Transaction이 종료된 후에도 Controller의 Session이 close되지 않았기 때문에, 영속 객체는 Persistence 상태를 유지할 수 있으며, Session이 열려있고 Persistence 상태이기 때문에 프록시 객체에 대한 Lazy Loading을 수행할 수 있게 됩니다.

출처: https://kingbbode.tistory.com/27 [개발노트 - kingbbode]

위 기능을 사용하지 않으려면 application.properties에 다음과 같은 옵션을 추가해주세요.

spring.jpa.open-in-view=false

 

1. Hibernate란

  • Boss에서 개발한 ORM(Object Relation Mapping)프레임워크 중 한개입니다.
  • ORM이란 객체와 DB 테이블의 매핑을 의미합니다.
  • ORM 매핑을 이용하면 객체로 DB 테이블 조작할 수 있습니다.

2. pom.xml

     ...
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        
        <dependency>
            <groupId>org.mariadb.jdbc</groupId>
            <artifactId>mariadb-java-client</artifactId>
        </dependency>
      ... 
  • Spring Boot 버전 - 2.1.0.RELEASE
  • hibernate-core-5.3.7.Final

프로젝트 구조

 


3. application.yml

spring :
    datasource :
        url : jdbc:mysql://localhost:3306/db_name
        username : (DB User Name)
        password : (DB Password)
    jpa :
        properties :
            hibernate :
                format_sql:true
        show-sql : true        
        hibernate :
            ddl-auto : update    
            # ddl-auto - @Entity 테이블 정보를 실제 DB에 반영 할건지 설정(create/update/none..)

 


4. User.java

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column(length = 20, nullable = false)
    private String name;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
  • @Entity 어노테이션은 해당 클래스가 DB Table과 1:1 매핑이 될 수 있게 합니다.
  • @Id - Primery Key 지정
  • @GeneratedValue(strategy = GenerationType.IDENTITY) - 기본키 생성을 DB에게 위임합니다.
    Mysql에 경우, AUTO_INCREMENT를 사용합니다.
  • @Column(length = 20, nullable = false) - 테이블 컬럼을 선언합니다. (길이 20, null을 허용하지 않음)
  • 위 클래스 생성 후, 서버를 실행 시키면 실제 DB에 해당 테이블에 대한 정보가 반영됩니다.

서버 구동 시 아래와 같이 로그에 user테이블 생성 로그가 남았다면, 실제로 테이블이 생성되었는지 확인


5. UserDao.java 생성

import com.example.bamdule.model.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserDao extends JpaRepository<User, Integer> {

}

6. UserController.java 생성

import com.example.bamdule.dao.UserDao;
import com.example.bamdule.model.entity.User;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping(value = "/user")
public class UserController {

    @Autowired
    private UserDao userDao;

    @GetMapping(value = "/save")
    @ResponseBody
    public List<User> saveUser(User user) {
        userDao.save(user);
        
        return userDao.findAll();
    }
}
  • user save 요청 추가
  • "/user/save" 요청 받는 유저를 DB에 저장하고, 유저 리스트를 반환합니다.
  • host/{contextPath}/user/save?name=park

브라우저에 위와 같은 응답이 왔으면 하이버네이트가 정상 동작 하는 것입니다.

+ Recent posts