우선 의존성으로 DB에 접근하는 ORM 기술인 JPA, 그리고 테스트 용으로 사용되는 h2 데이터베이스를 연결합니다. build.gradle에 다음과 같이 dependencies에 추가해줍니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
}
다음으로, application.properties에 다음과 같이 설정해줍니다.
spring.h2.console.enabled=true
spring.datasource.hikari.driver-class-name=org.h2.Driver
spring.datasource.hikari.jdbc-url=jdbc:h2:tcp://localhost/~/TestSecurity
spring.datasource.hikari.username=sa
spring.datasource.hikari.password=
server.servlet.encoding.charset=utf-8
server.servlet.encoding.enabled=true
server.servlet.encoding.force=true
spring.jpa.hibernate.ddl-auto=none
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
다음과 같이 회원가입을 위한 아주 간단한 UI를 구성해봅니다. join.mustache에 다음과 같이 작성합니다.
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<form action="/joinProc" method="post" name="joinForm">
<input type="text" name="username" placeholder="Username"/>
<input type="password" name="password" placeholder="Password"/>
<input type="submit" value="Join"/>
</form>
</body>
</html>
이에 따라 기존 config 파일에서 모두에게 허용할 url을 추가해줍니다.
...
.requestMatchers("/", "/login", "/join", "/joinProc").permitAll()
...
회원가입시 회원명, 비밀번호를 받고, Role의 경우 비즈니스 로직 상에서 결정합니다. 따라서 다음과 같이 UserEntity 클래스를 작성해줍니다.
package com.example.testsecurity.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Entity
@Setter
@Getter
public class UserEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column(unique = true)
private String username;
private String password;
private String role;
}
@Id는 해당 항목을 primary key로 설정을 해주고, @GeneratedValue를 통해서 자동으로 증가시켜줍니다. 이때, GenerationType.IDENTITY
는 id 값을 비워두면 DB가 알아서 AUTO_INCREMENT, 즉 값을 증가시켜줍니다.
그리고 username은 겹치면 안되므로 unique 제약조건을 걸어줍니다.
데이터를 DB로 전달하는 JoinDTO는 다음과 같습니다.
package com.example.testsecurity.dto;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
public class JoinDTO {
private String username;
private String password;
}
MVC 패턴을 따름에 따라 Controller - Service - Repository - DB 로 진행이 됩니다.
package com.example.testsecurity.controller;
import com.example.testsecurity.dto.JoinDTO;
import com.example.testsecurity.service.JoinService;
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.PostMapping;
@Controller
public class JoinController {
@Autowired
private JoinService joinService;
@GetMapping("/join")
public String joinP() {
return "join";
}
@PostMapping("/joinProc")
public String joinProcess(JoinDTO joinDTO) {
System.out.println(joinDTO.getUsername());
joinService.joinProcess(joinDTO);
return "redirect:/login";
}
}
package com.example.testsecurity.service;
import com.example.testsecurity.dto.JoinDTO;
import com.example.testsecurity.entity.UserEntity;
import com.example.testsecurity.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class JoinService {
@Autowired
private UserRepository userRepository;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
public void joinProcess(JoinDTO joinDTO) {
// DB에 이미 동일한 username을 가진 회원이 존재하는가?
boolean isUser = userRepository.existsByUsername(joinDTO.getUsername());
if (isUser) {
return;
}
UserEntity data = new UserEntity();
data.setUsername(joinDTO.getUsername());
data.setPassword(bCryptPasswordEncoder.encode(joinDTO.getPassword()));
data.setRole("ROLE_USER");
userRepository.save(data);
}
}
서비스의 역할은 두가지입니다.
따로 복잡한 트랜잭션에 대해서 지금 다루지는 않으므로, 로직을 보면, 비밀번호의 경우 그대로 저장하면 위험합니다. DBA가 맘먹으면 비밀번호 마음대로 열람도 가능할뿐더러, 보안상으로 좋지 않습니다. 따라서 Bcrypt 인코딩을 통해 비밀번호를 암호화합니다. 그리고 그 암호화 한 값을 DB에 저장해야됩니다.
그리고 Role의 경우에는 만약 USER라는 role을 주고 싶다면 반드시 ROLE_USER
와 같이 지정해야됩니다. 관리자는 ROLE_ADMIN
과 같이 주면 됩니다.
package com.example.testsecurity.repository;
import com.example.testsecurity.entity.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<UserEntity, Integer> {
boolean existsByUsername(String username);
}
여기까지 보면 따로 쿼리를 작성하지는 않았음을 볼 수 있습니다. 바로 JPA가 메소드명에 따라 자동으로 쿼리를 생성해서 DB에 보내주기 때문에 별도 쿼리를 작성하지 않았습니다.
service와 repository를 확인해보면 메소드 명으로만 쿼리를 자동으로 생성한다는 것을 알 수 있는데 이를 JPA의 쿼리 메소드라고 합니다.