Spring Security (2) - 회원가입 및 로그인을 위한 데이터베이스 연동
개발환경
- Eclipse IDE 2022-06
- Spring Boot 2.7.2
- Gradle 7.0
- Lombok
- PostgreSQL
지난 시간에 이어서 이번에는 사용자 정보를 메모리에서 가져오는 것 대신에 데이터베이스에서 가져와 로그인을 해보자. 데이터베이스에서 로그인을 하려면 DB구축, JPA 설정 등 이 선행되어야 한다. 구축되어 있는 데이터베이스가 없다거나 JPA 설정에 대하여 궁금하다면 필자가 이전에 쓴 글을 참고하면 좋을 것 같다.
우선 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 (3) - 로그인 시 사용자 권한 조회 및 부여
'Programming > Spring Security' 카테고리의 다른 글
[Spring Boot] Spring Security (5) - 역할(hasRole)과 권한(hasAuthority)의 차이는 무엇일까? (0) | 2022.08.22 |
---|---|
[Spring Boot] Spring Security (4) - 역할 별 페이지 접근제어 (0) | 2022.08.19 |
[Spring Boot] Spring Security (3) - 로그인 시 사용자 역할 조회 및 부여 (0) | 2022.08.17 |
[Spring Boot] Spring Security (1) - 기본 프로젝트 생성 (3) | 2022.07.21 |
댓글