본문 바로가기
Programming/Spring Security

[Spring Boot] Spring Security (2) - 회원가입 및 로그인을 위한 데이터베이스 연동

by 주리니e 2022. 8. 16.
728x90

Spring Security (2) - 회원가입 및 로그인을 위한 데이터베이스 연동

 

 

개발환경

  • Eclipse IDE 2022-06
  • Spring Boot 2.7.2
  • Gradle 7.0
  • Lombok
  • PostgreSQL


지난 시간에 이어서 이번에는 사용자 정보를 메모리에서 가져오는 것 대신에 데이터베이스에서 가져와 로그인을 해보자. 데이터베이스에서 로그인을 하려면 DB구축, JPA 설정 등 이 선행되어야 한다. 구축되어 있는 데이터베이스가 없다거나 JPA 설정에 대하여 궁금하다면 필자가 이전에 쓴 글을 참고하면 좋을 것 같다. 

 

Windows에 PostgreSQL14 설치하기

Windows에 PostgreSQL14 설치하기 개발환경을 구축하기 위해 Windows에 PostgreSQL을 설치하려고 한다. 공식 사이트에 접속하여 PostgreSQL14 설치파일을 다운로드 하자. https://www.postgresql.org/ PostgreSQL..

jiurinie.tistory.com

 

[Spring Boot] PostgreSQL 연결을 위한 JPA 설정

[Spring Boot] PostgreSQL 연결을 위한 JPA 설정 개발환경 Eclipse IDE 2022-06 Spring Boot 2.7.2 Gradle 7.0 Lombok PostgreSQL 기본 Spring Boot 프로젝트를 생성 시 JPA를 추가하였거나 build.gradle..

jiurinie.tistory.com

 

우선 PostgreSQL에서 데이터를 조회하고 저장하기 위해 사용자 테이블을 하나 만들어보자.

CREATE TABLE public.tb_user (
	id varchar(255) NOT NULL,
	"password" varchar(255) NULL,
	name varchar(255) NULL,	
	CONSTRAINT tb_user_pkey PRIMARY KEY (id)
);


다음은 지난시간에 설정했던 정보를 수정하고 새로운 파일들을 추가해보자.

패키지 구조

  • build.gradle
plugins {
	id 'org.springframework.boot' version '2.7.2'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
	id 'war'
}

group = 'com.lombok'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
	implementation 'org.postgresql:postgresql'

	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.security:spring-security-test'
}

tasks.named('test') {
	useJUnitPlatform()
}

데이터베이스 연동을 위해 spring-boot-starter-data-jpa와 postgresql을 추가하였다.

  • application.properties
##setting server
server.port=80
server.servlet.context-path=/
server.servlet.session.timeout=10m
server.servlet.session.tracking-modes=cookie

## database
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://xxx.xxx.xxx.xxx:5432/playground
spring.datasource.username=jiurinie
spring.datasource.password=password

## jpa
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=true


## thymeleaf
spring.thymeleaf.cache=false
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html



  • SecurityConfiguration.java
package com.example.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import lombok.AllArgsConstructor;

@EnableWebSecurity
@AllArgsConstructor
public class SecurityConfiguration {

	private final UserDetailsService userDetailsService;

	@Bean
	public static BCryptPasswordEncoder bCryptPasswordEncoder() {
		return new BCryptPasswordEncoder();
	}

	@Bean
	public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
		/* @formatter:off */
		http
			.authorizeRequests()
				.antMatchers("/", "/home", "/signUp").permitAll() // 설정한 리소스의 접근을 인증절차 없이 허용
				.anyRequest().authenticated() // 그 외 모든 리소스를 의미하며 인증 필요
				.and()
			.formLogin()
				.permitAll()
				.loginPage("/login") // 기본 로그인 페이지
				.and()
			.logout()
				.permitAll()
				// .logoutUrl("/logout") // 로그아웃 URL (기본 값 : /logout)
				// .logoutSuccessUrl("/login?logout") // 로그아웃 성공 URL (기본 값 : "/login?logout")
				.logoutRequestMatcher(new AntPathRequestMatcher("/logout")) // 주소창에 요청해도 포스트로 인식하여 로그아웃
				.deleteCookies("JSESSIONID") // 로그아웃 시 JSESSIONID 제거
				.invalidateHttpSession(true) // 로그아웃 시 세션 종료
				.clearAuthentication(true); // 로그아웃 시 권한 제거
		
		return http.build();
		/* @formatter:on */
	}

	@Autowired
	public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
		auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
	}
}

 AuthenticationmanagerBuilder에  패스워드 암호화를 위해 Spring Security에서 제공하는 BCryptPasswordEncoder를 추가 후 UserDetailsService를 추가하여 로그인 시 UserDetailsService를 구현한 CustomUserDetailsService에서 사용자 확인 및 권한을 넣어줄 수 있도록 한다.

 

  • CustomUserDetailsService.java
package com.example.security.service;

import java.util.HashSet;
import java.util.Set;

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.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.example.security.persistence.dao.UserRepository;

import lombok.AllArgsConstructor;

@Service
@AllArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

	private final UserRepository userRepository;

	@Override
	@Transactional(readOnly = true)
	public UserDetails loadUserByUsername(String id) {

		Set<GrantedAuthority> grantedAuthorities = new HashSet<>();

		com.example.security.persistence.model.User user = userRepository.findOneById(id);

		if (user != null) {
			grantedAuthorities.add(new SimpleGrantedAuthority("USER")); // USER 라는 역할을 넣어준다.
			return new User(user.getId(), user.getPassword(), grantedAuthorities);
		} else {
			throw new UsernameNotFoundException("can not find User : " + id);
		}
	}
	
}

로그인 인증 시 Spring Security 내부에서는 UserDetailsService 인터페이스의 loadUserByUsername을 호출한다. 이를 구현하여 위와 같이 데이터베이스에서 사용자 정보를 가져오고 있으면 USER라는 권한을 넣어주며, 존재하지 않는 경우에는 UsernameNotFoundException을 호출한다.

 

  • HomeController.java
package com.example.security.web.controller;

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

@Controller
public class HomeController {

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

	@GetMapping("/hello")
	public String hello() {
		return "hello";
	}
}

 

  • LoginController.java
package com.example.security.web.controller;

import javax.servlet.http.HttpServletRequest;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

import com.example.security.service.UserService;
import com.example.security.web.dto.UserDto;

import lombok.AllArgsConstructor;

@Controller
@AllArgsConstructor
public class LoginController {

	private final UserService userService;

	@GetMapping("/login")
	public String login(HttpServletRequest request) {
		return "login";
	}

	@GetMapping("/signUp")
	public String signUp(Model model) {
		model.addAttribute("userDto", new UserDto());
		return "signUp";
	}

	@PostMapping("/signUp")
	public String signUp(@ModelAttribute("userDto") UserDto userDto) {
		userService.insert(userDto);
		return "redirect:/login";
	}
}

 

  • User.java
package com.example.security.persistence.model;

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

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
@Table(name = "tb_user")
public class User {

	@Id
	@Column(name = "id")
	private String id;

	@Column(name = "password")
	private String password;

	@Column(name = "name")
	private String name;

	@Builder
	public User(String id, String password, String name) {
		this.id = id;
		this.password = password;
		this.name = name;
	}
}

 

  • UserDto.java
package com.example.security.web.dto;

import com.example.security.persistence.model.User;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
public class UserDto {
	private String id;

	private String password;

	private String name;

	public User toEntity() {
		return User.builder().id(id).password(password).name(name).build();
	}
}

 

  • UserService.java
package com.example.security.service;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import com.example.security.persistence.dao.UserRepository;
import com.example.security.web.dto.UserDto;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class UserService {

	private final UserRepository userRepository;

	private final BCryptPasswordEncoder bCryptPasswordEncoder;

	public void insert(UserDto userDto) {
		userDto.setPassword(bCryptPasswordEncoder.encode(userDto.getPassword()));
		userRepository.save(userDto.toEntity());
	}
}

 

  • UserRepository.java
package com.example.security.persistence.dao;

import org.springframework.data.jpa.repository.JpaRepository;

import com.example.security.persistence.model.User;

public interface UserRepository extends JpaRepository<User, String> {
	User findOneById(String id);
}

 

  • home.html
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<title>Spring Security Example</title>
</head>
<body>
	<h1>Welcome!</h1>
	<p>
		Click <a th:href="@{/hello}">here</a> to see a greeting.
	</p>
</body>
</html>

 

  • login.html
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<title>Spring Security Example</title>
</head>
<body>
	<h1>Spring Security Login page</h1>
	<div th:if="${param.error}">Invalid username and password.</div>
	<div th:if="${param.logout}">You have been logged out.</div>
	<form th:action="@{/login}" method="post">
		<div>
			<label> User Name : <input type="text" name="username" /></label>
		</div>
		<br>
		<div>
			<label> Password : <input type="password" name="password" /></label>
		</div>
		<br>
		<div>
			<input type="submit" value="Sign In" />
			<input type="button" onclick="location.href='/signUp';" value="Sign Up" />
		</div>
	</form>
</body>
</html>

 

  • signUp.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Spring Security Example</title>
</head>
<body>
	<h1>Spring Security SignUp page</h1>
	<hr>
	<form th:action="@{/signUp}" method="post" th:object="${userDto}">
		<div>
			<input type="text" name="id" placeholder="아이디">
		</div>
		<div>
			<input type="password" name="password" placeholder="비밀번호">
		</div>
		<div>
			<input type="text" name="name" placeholder="이름">
		</div>
		<button type="submit">가입하기</button>
		<button type="button" onclick="location.href='/home';">취소</button>
	</form>
</body>
</html>

 

  • hello.html
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<title>Hello World!</title>
</head>
<body>
	<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
	<form th:action="@{/logout}" method="post">
		<input type="submit" value="Sign Out" />
	</form>
</body>
</html>
http://localhost

홈 화면 페이지
로그인 페이지
회원가입 페이지
로그인 성공 페이지
저장된 사용자 정보

 

이로써 데이터베이스에 회원정보를 저장하고 이를 이용하여 로그인 인증까지 해보았다. 이 부분까지만 해도 뼈의 구조를 갖추었으며 앞으로 여기에는 조금씩 Spring Security에서 제공하는 기능을 덧대어 포스팅을 진행해보도록 하겠다.

 

이전글 : [Spring Boot] Spring Security (1) - 기본 프로젝트 생성

 

[Spring Boot] Spring Security (1) - 기본 프로젝트 생성

[Spring Boot] Spring Security (1) - 기본 프로젝트 생성 개발환경 Eclipse IDE 2022-06 Spring Boot 2.7.2 Gradle 7.0 Lombok PostgreSQL Spring Security는 Spring Boot의 하위 프레임 워크이며 Java 어플리케..

jiurinie.tistory.com

다음글 : [Spring Boot] Spring Security (3) - 로그인 시 사용자 권한 조회 및 부여

 

[Spring Boot] Spring Security (3) - 로그인 시 사용자 권한 조회 및 부여

[Spring Boot] Spring Security (3) - 로그인 시 사용자 권한 조회 및 부여 개발환경 Eclipse IDE 2022-06 Spring Boot 2.7.2 Gradle 7.0 Lombok PostgreSQL 지난 시간에 이어서 이번에는 로그인 시 사용자..

jiurinie.tistory.com

 

728x90

댓글