스프링은 많은 회사들의 상용 웹 시스템을 구동하는데 보편적으로 많이 사용되는 Java 기반의 웹 프레임워크이다. 많은 회사들에서 사용하는 만큼, 기능도 다양하고 기능 별 깊이가 깊다. 그만큼 러닝커브가 높은 편이라, 간단한 기능들을 직접 샘플로 구현해보면서 해당 기능에 대한 이해도를 높이는 방식이 학습 효율이 좋다고 느껴진다.
기능들 중 기초적으로 많이 쓰이는 기능이 회원가입과 로그인이다. 설계에 따라 다양한 방식으로 구현될 수 있겠지만, 최대한 간단한 예제를 찾아 따라해보면서 부족한 부분들을 채워가는 방식으로 포스팅을 작성했다.
회원가입 데이터를 보관하는 데이터베이스 설치 및 세팅부터, 간단한 Form을 이용한 회원가입까지 구현해본다.
데이터베이스는 친숙한 MySQL 커뮤니티 버전을 사용한다.
아래 링크를 통해 별 무리없이 무난하게 데이터베이스 설치가 가능하다.
https://dev.mysql.com/downloads/windows/installer/8.0.html
설치를 진행하게 되면, 아래와 같이 다양한 제품(product)들이 설치되는 것을 볼 수 있는데, 이중 MySQL Server
와 MySQL Workbench
는 반드시 설치하도록 하자.
(설치할 수 있는 제품은 모두 설치하는 것을 권장드린다. 나는 향후 혹시 모를 귀찮음을 해소해주는 방법이라고 생각해서 모두 설치했다.)
TB_USER
라는 테이블을 생성하는 쿼리이다.
MySQL 설치 후, MySQL Workbench로 접속한 다음, 임의의 schema를 생성하고 아래 SQL을 실행하면 테이블 생성이 완료된다.
CREATE TABLE `tb_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '사용자번호',
`member_id` varchar(255) NOT NULL COMMENT '아이디',
`member_pwd` varchar(256) DEFAULT NULL COMMENT '비밀번호',
`m_name` varchar(255) NOT NULL COMMENT '사용자명',
`m_grade_str` varchar(255) NOT NULL COMMENT '권한',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
이전 데이터베이스 세팅
을 했다면, 이제 본격적으로 스프링 프로젝트를 생성해볼 차례이다. 스프링 프로젝트 생성을 위해 start.spring.io에 접속하여 아래와 같이 설정한다.
start.spring.io의 Dependencies에 아래 패키지들을 추가한다.
plugins {
id 'org.springframework.boot' version '2.7.0'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'jtlim'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.2'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'mysql:mysql-connector-java'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
tasks.named('test') {
useJUnitPlatform()
}
main/resources/application.properties
파일에 아래 내용을 추가한다. spring.datasource.url=jdbc:mysql://127.0.0.1:3306/[NAME_OF_SCHEMA]
spring.datasource.username=[DB_USERNAME]
spring.datasource.password=[DB_PASSWORD]
package jtlim.simplesite.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.WebSecurityCustomizer;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SpringSecurity {
@Bean
public WebSecurityCustomizer assetCustomizer() {
return (web -> web.ignoring().antMatchers("/css/**", "/script/**", "image/**", "/fonts/**", "lib/**"));
}
@Bean
public SecurityFilterChain baseChain(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// 로그인 권한은 누구나, resources파일도 모든권한
.antMatchers("/login", "/logout", "/register", "/access_denied", "/resources/**").permitAll()
// "/" 도메인 접근 허용
.antMatchers("/").authenticated()
.and()
.formLogin()
// 로그인 url 설정
.loginPage("/login")
// 로그인 처리 로직 url 설정
.loginProcessingUrl("/login_proc")
// 로그인 성공시 리다이렉트 url 설정
.defaultSuccessUrl("/")
// 로그인 실패시 리다이렉트 url 설정
.failureUrl("/access_denied") // 인증에 실패했을 때 보여주는 화면 url, 로그인 form으로 파라미터값 error=true로 보낸다.
.and()
.csrf().disable(); //로그인 창
return http.build();
}
}
Whitelabel Error Page
가 표기된다. url 정상 리다이렉트만 확인하고 넘어간다.index
페이지를 응답하고,register
페이지를 응답한다.package jtlim.simplesite.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
@GetMapping("/")
public String root() {
return "index";
}
/**
* 회원가입 폼(Get 요청)
*/
@GetMapping("/register")
public String registerForm() {
return "register";
}
/**
* 회원가입 폼 제출(Post 요청)
*/
@PostMapping("/register")
public String register(Member member) {
memberService.joinMember(member);
return "redirect:/register";
}
}
package jtlim.simplesite.service;
import jtlim.simplesite.domain.Member;
import jtlim.simplesite.mapper.MemberMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
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;
import java.text.SimpleDateFormat;
import java.util.Date;
@Service
@RequiredArgsConstructor
public class MemberService implements UserDetailsService {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:sss");
Date time = new Date();
String localTime = format.format(time);
private final MemberMapper memberMapper;
@Transactional
public void joinMember(Member member) {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
member.setMemberPwd(passwordEncoder.encode(member.getPassword()));
member.setMGradeStr("ROLE_USER");
memberMapper.save(member);
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return null;
}
}
main/resources/templates/register.html
파일을 생성한다.<!--회원가입 페이지-->
<!--register.html-->
<!DOCTYPE html>
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<form method="post" action="/register">
<div class="container">
<h1>회원가입</h1>
<div class="form-group">
<label for="inputEmail4">아이디</label>
<input type="text" class="form-control" id="inputEmail4" name="memberId" placeholder="사용자 아이디">
</div>
<div class="form-group">
<label for="inputAddress">이름</label>
<input type="text" class="form-control" id="inputAddress" name="mName" placeholder="사용자 이름">
</div>
<div class="form-group">
<label for="inputPassword4">비밀번호</label>
<input type="password" class="form-control" id="inputPassword4" name="memberPwd" placeholder="사용자 비밀번호">
</div>
<button type="submit" class="btn btn-primary">가입 완료</button>
</div>
</form>
</body>
</html>
localhost:8080/register
로 접근하면 아래와 같이 표기되어야 한다.import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Collections;
// @Getter, @Setter, @RequiredArgsConstructor, @ToString, @EqualsAndHashCode 어노테이션을 한꺼번에 설정해주는 어노테이션
@Data
public class Member implements UserDetails {
private String memberId; // 로그인에 사용하는 id
private String memberPwd; // 로그인에 사용하는 비밀번호
private String mName; // 사용자 닉네임
private String mGradeStr; // 사용자 권한(향후 권한 관리에서 사용한다. 지금은 사용하지 않으므로 필드만 삽입해두고 넘어가면 되겠다.)
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singletonList(new SimpleGrantedAuthority(this.mGradeStr));
}
@Override
public String getUsername() {
return this.memberId;
}
@Override
public String getPassword() {
return this.memberPwd;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
package gurutech.dbmatching.mapper;
import gurutech.dbmatching.domain.Member;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface MemberMapper {
void save(Member member);
}
<?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="jtlim.simplesite.mapper.MemberMapper">
<!-- 회원가입 -->
<insert id="save" parameterType="jtlim.simplesite.domain.Member">
INSERT INTO tb_user
(member_id, member_pwd, m_name, m_grade_str)
VALUES(#{memberId},#{memberPwd}, #{mName}, #{mGradeStr});
</insert>
</mapper>
// 기존 데이터베이스 설정에 사용한 속성들
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/[NAME_OF_SCHEMA]
spring.datasource.username=[DB_USERNAME]
spring.datasource.password=[DB_PASSWORD]
// mybatis 연동
mybatis.mapper-locations=query/*.xml
mybatis.configuration.map-underscore-to-camel-case=true