들어가기 전에
이전 시간엔 로그인/로그아웃에 대한 처리를 하였습니다.
단, 로그인 정보를 데이터베이스에서 읽어 들이지 않고 정해진 특정 사용자만 로그인이 되도록 하였습니다.
이번 시간엔 데이터베이스로부터 회원 정보를 읽어 들여 로그인 처리가 되도록 하겠습니다.
학습 목표
데이터베이스로부터 회원정보를 읽어드려 로그인 처리가 되도록 합니다.
핵심 개념
로그인을 위한 데이터베이스 모델링
학습하기
들어가기 전에
이전 시간엔 로그인/로그아웃에 대한 처리를 하였습니다.
단, 로그인 정보를 데이터베이스에서 읽어 들이지 않고 정해진 특정 사용자만 로그인이 되도록 하였습니다.
이번 시간엔 데이터베이스로부터 회원 정보를 읽어 들여 로그인 처리가 되도록 하겠습니다.
학습 목표
데이터베이스로부터 회원정보를 읽어드려 로그인 처리가 되도록 합니다.
핵심 개념
로그인을 위한 데이터베이스 모델링
학습하기
이번 시간에는 데이터베이스로부터 회원 정보와 권한 정보를 읽어들여 로그인 처리되도록 기존의 코드를 수정합니다.
1. 데이터 베이스 모델링, 테이블 생성, 데이터 추가하기
회원 정보(member 테이블)와 회원 권한(member_role 테이블)은 아래 그림과 같이 1:N 구조로 되어 있습니다.
위와 같은 구조로 테이블을 생성하기 위해 아래와 같은 sql을 사용합니다. 기존에 테이블이 있을 경우 삭제합니다.
<ddl.sql>
drop table if exists member_role;
drop table if exists member;
-- -----------------------------------------------------
-- Table `member`
-- -----------------------------------------------------
CREATE TABLE `member` (
`id` INT(11) NOT NULL AUTO_INCREMENT COMMENT 'member id',
`name` VARCHAR(255) NOT NULL COMMENT 'member name',
`password` VARCHAR(255) NOT NULL COMMENT '암호회된 password',
`email` VARCHAR(255) NOT NULL UNIQUE COMMENT 'login id, email',
`create_date` DATETIME NULL DEFAULT NULL COMMENT '등록일',
`modify_date` DATETIME NULL DEFAULT NULL COMMENT '수정일',
PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- -----------------------------------------------------
-- Table `member_role`
-- -----------------------------------------------------
CREATE TABLE `member_role` (
`id` INT(11) NOT NULL AUTO_INCREMENT COMMENT 'role id',
`member_id` INT(11) NOT NULL COMMENT 'member id fk',
`role_name` VARCHAR(100) NOT NULL COMMENT 'role 이름 ROLE_ 로 시작하는 값이어야 한다.',
PRIMARY KEY (`id`),
FOREIGN KEY (`member_id`)
REFERENCES `member` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
만들어진 테이블에 회원 정보와 권한 정보를 저장합니다.
<dml.sql>
insert into member (id, name, password, email, create_date, modify_date) values ( 1, '강경미', '$2a$10$G/ADAGLU3vKBd62E6GbrgetQpEKu2ukKgiDR5TWHYwrem0cSv6Z8m', 'carami@example.com', now(), now());
insert into member (id, name, password, email, create_date, modify_date) values ( 2, '이정주', '$2a$10$G/ADAGLU3vKBd62E6GbrgetQpEKu2ukKgiDR5TWHYwrem0cSv6Z8m', 'toto@example.com', now(), now());
insert into member_role (id, member_id, role_name) values (1, 1, 'ROLE_USER');
insert into member_role (id, member_id, role_name) values (2, 1, 'ROLE_ADMIN');
insert into member_role (id, member_id, role_name) values (3, 2, 'ROLE_USER');
2. 데이터 베이스 사용을 위해 pom.xml 파일 수정하기
pom.xml 파일에 Spring JDBC와 관련된 라이브러리를 추가합니다.
<pom.xml>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.edwith.webbe</groupId>
<artifactId>securityexam</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<!-- eclipse에서 웹 어플리케이션 프로젝트 작성시 web.xml파일을 작성하지 않고 java-config로 설정할 경우 아래의 설정이 있어야 합니다.-->
<failOnMissingWebXml>false</failOnMissingWebXml>
<!-- spring 5.2.3이 나오는 시점에 spring security는 5.2.2가 최신버전이라서 5.2.2.RELEASE로 설정함 -->
<spring.version>5.2.2.RELEASE</spring.version>
</properties>
<dependencies>
<!-- servlet-api이다. tomcat에 배포될 경우엔 사용되지 않도록 하기 위해서 scope를 provided로 설정하였다. -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<!-- jsp-api이다. tomcat에 배포될 경우엔 사용되지 않도록 하기 위해서 scope를 provided로 설정하였다. -->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.2-b02</version>
<scope>provided</scope>
</dependency>
<!-- jstl은 tomcat이 기본 지원하지 않는다. 그렇기 때문에 tomcat에도 배포가 되야 한다.-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!-- spring webmvc에 대한 의존성을 추가한다. spring webmvc에 대한 의존성을 추가하게 되면 spring-web, spring-core등이 자동으로 의존성이 추가된다.-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- mysql jdbc driver -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
<!-- 커넥션 풀 라이브러리를 추가한다. spring boot 2의 경우는 hikariCP가 기본으로 사용된다.-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<version>2.6.0</version>
</dependency>
<!-- DataSource, Transaction등을 사용하려면 추가한다. spring-tx를 자동으로 포함시킨다.-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- java 9 이상에서 추가해줘야 합니다. @PostConstruct 등을 사용하려면 필요함-->
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- Spring Security Core -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- Spring Security Config -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- Spring Security Web -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- Spring Security JSP Custom Tags -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>utf-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
3. 데이터베이스 사용을 위해 설정 파일 수정하기
기존 스프링 설정 파일인 ApplicationConfig.java파일에 DataSource와 트랜잭션에 대한 설정을 합니다.
데이터베이스 접속 정보는 본인에 맞게 수정합니다.
<ApplicationConfig.java>
package org.edwith.webbe.securityexam.config;
import org.apache.commons.dbcp2.BasicDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.TransactionManagementConfigurer;
import javax.sql.DataSource;
// 레이어드 아키텍처에서 Controller가 사용하는 Beanㄷ르에 대해 설정을 한다.
// dao, service를 컴포넌트 스캔하여 찾도록 한다.
// 어노테이션으로 트랜잭션을 관리하기 위해 @EnableTransactionManagement를 설정하였다.
@Configuration
@ComponentScan(basePackages = {"org.edwith.webbe.securityexam.dao", "org.edwith.webbe.securityexam.service"})
@EnableTransactionManagement
public class ApplicationConfig implements TransactionManagementConfigurer {
// mysql 드라이버 클래스 이름은 "com.mysql.jdbc.Driver"에서 버전 6이후에는 "com.mysql.cj.jdbc.Driver"로 변경되었다.
private String driverClassName = "com.mysql.cj.jdbc.Driver";
// java.sql.SQLException: Cannot create PoolableConnectionFactory (The server time zone value 'KST' is unrecognized or represents more than one time zone. You must configure either the server or JDBC driver (via the 'serverTimezone' configuration property) to use a more specifc time zone value if you want to utilize time zone support.)
// DB연결시 위와 같은 오류가 발생한다면 &serverTimezone=UTC 를 url에 붙여줘야 합니다.
private String url = "jdbc:mysql://localhost:3306/connectdb?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC";
private String username = "connectuser";
private String password = "connect123!@#";
/**
* 커넥션 풀과 관련된 Bean을 생성한다.
* @return
*/
@Bean
public DataSource dataSource(){
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName(driverClassName);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
/**
* 트랜잭션 관리자를 생성한다.
* @return
*/
@Bean
public PlatformTransactionManager transactionManager(){
return new DataSourceTransactionManager(dataSource());
}
@Override
public PlatformTransactionManager annotationDrivenTransactionManager() {
return transactionManager();
}
}
4. 데이터베이스로 부터 읽어들이기 위한 DTO와 DAO작성하기
회원 정보 한 건의 정보를 저장하는 Member DTO 클래스를 다음과 같이 작성합니다.
<Member.java>
package org.edwith.webbe.securityexam.dto;
import java.util.Date;
public class Member {
private Long id;
private String name;
private String password;
private String email;
private Date createDate;
private Date modifyDate;
public Member() {
createDate = new Date();
modifyDate = new Date();
}
public Member(Long id, String name, String password, String email) {
this();
this.name = name;
this.password = password;
this.email = email;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Date getCreateDate() {
return createDate;
}
public void setCreateDate(Date createDate) {
this.createDate = createDate;
}
public Date getModifyDate() {
return modifyDate;
}
public void setModifyDate(Date modifyDate) {
this.modifyDate = modifyDate;
}
}
Member DAO에서 읽어들일 SQL정보를 가지고 있는 MemberDaoSqls클래스를 다음과 같이 작성합니다.
email정보와 일치하는 회원 정보를 읽어들이는 것을 알 수 있습니다.
<MemberDaoSqls>
package org.edwith.webbe.securityexam.dao;
public class MemberDaoSqls {
public static final String SELECT_ALL_BY_EMAIL = "SELECT id, name, password, email, create_date, modify_date FROM member WHERE email = :email";
}
MemberDao클래스를 작성합니다. 데이터베이스의 정보를 읽어 들이는 레이어에 속하기 때문에 @Component가 아니라 @Repository가 붙은 것을 확인할 수 있습니다. 설명은 주석문으로 대신합니다.
<MemberDao>
package org.edwith.webbe.securityexam.dao;
import org.edwith.webbe.securityexam.dto.Member;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Repository;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Repository
public class MemberDao {
private NamedParameterJdbcTemplate jdbc;
// BeanPropertyRowMapper는 Role클래스의 프로퍼티를 보고 자동으로 칼럼과 맵핑해주는 RowMapper객체를 생성한다.
// roleId 프로퍼티는 role_id 칼럼과 맵핑이 된다.
// 만약 프로퍼티와 칼럼의 규칙이 맞아 떨어지지 않는다면 직접 RowMapper객체를 생성해야 한다.
// 생성하는 방법은 아래의 rowMapper2를 참고한다.
private RowMapper<Member> rowMapper = BeanPropertyRowMapper.newInstance(Member.class);
public MemberDao(DataSource dataSource){
this.jdbc = new NamedParameterJdbcTemplate(dataSource);
}
public Member getMemberByEmail(String email){
Map<String, Object> map = new HashMap<>();
map.put("email", email);
return jdbc.queryForObject(MemberDaoSqls.SELECT_ALL_BY_EMAIL, map, rowMapper);
}
}
회원의 권한(Role)정보를 저장하기 위한 MemberRole DTO클래스를 다음과 같이 작성합니다.
<MemberRole.java>
package org.edwith.webbe.securityexam.dto;
public class MemberRole {
private Long id;
private Long memberId;
private String roleName;
public MemberRole() {
}
public MemberRole(Long memberId, String roleName) {
this.memberId = memberId;
this.roleName = roleName;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getMemberId() {
return memberId;
}
public void setMemberId(Long memberId) {
this.memberId = memberId;
}
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
}
email에 해당하는 권한 정보를 읽어들이기 위해서 member테이블과 member_role테이블을 조인(JOIN)하여 결과를 얻는 SQL을 가진 MemberRoleDaoSqls 클래스를 다음과 같이 작성합니다.
<MemberRoleDaoSqls.java>
package org.edwith.webbe.securityexam.dao;
public class MemberRoleDaoSqls {
public static final String SELECT_ALL_BY_EMAIL = "SELECT mr.id, mr.member_id, mr.role_name FROM member_role mr JOIN member m ON mr.member_id = m.id WHERE m.email = :email";
}
권한 정보를 읽어들이는 MemberRoleDao 클래스를 다음과 같이 작성합니다.
<MemberRoleDao.java>
package org.edwith.webbe.securityexam.dao;
import org.edwith.webbe.securityexam.dto.MemberRole;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Repository;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Repository
public class MemberRoleDao {
private NamedParameterJdbcTemplate jdbc;
// BeanPropertyRowMapper는 Role클래스의 프로퍼티를 보고 자동으로 칼럼과 맵핑해주는 RowMapper객체를 생성한다.
// roleId 프로퍼티는 role_id 칼럼과 맵핑이 된다.
// 만약 프로퍼티와 칼럼의 규칙이 맞아 떨어지지 않는다면 직접 RowMapper객체를 생성해야 한다.
// 생성하는 방법은 아래의 rowMapper2를 참고한다.
private RowMapper<MemberRole> rowMapper = BeanPropertyRowMapper.newInstance(MemberRole.class);
public MemberRoleDao(DataSource dataSource){
this.jdbc = new NamedParameterJdbcTemplate(dataSource);
}
public List<MemberRole> getRolesByEmail(String email){
Map<String, Object> map = new HashMap<>();
map.put("email", email);
return jdbc.query(MemberRoleDaoSqls.SELECT_ALL_BY_EMAIL, map, rowMapper);
}
}
데이터베이스 접속, MemberDao, MemberRoleDao클래스가 알맞게 동작하는지 테스트 클래스를 작성합니다.
해당 클래스를 실행했을 때 아무 문제도 발생하지 않으면 다음 단계로 진행합니다.
<MemberDaoTest.java>
package org.edwith.webbe.securityexam.dao;
import org.edwith.webbe.securityexam.config.ApplicationConfig;
import org.edwith.webbe.securityexam.dto.Member;
import org.edwith.webbe.securityexam.service.security.UserEntity;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import javax.sql.DataSource;
import java.sql.Connection;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {ApplicationConfig.class})
public class MemberDaoTest {
@Autowired
DataSource dataSource;
@Autowired
MemberDao memberDao;
@Autowired
MemberRoleDao memberRoleDao;
@Test
public void configTest() throws Exception{
// 아무 작업도 하지 않는다. 실행이 잘된다는 것은 Spring 설정이 잘 되어 있다는 것을 의미한다.
}
@Test
public void connnectionTest() throws Exception{
Connection connection = dataSource.getConnection();
Assert.assertNotNull(connection);
}
@Test
public void getUser() throws Exception{
Member member = memberDao.getMemberByEmail("carami@example.com");
Assert.assertNotNull(member);
Assert.assertEquals("강경미", member.getName());
}
}
carami사용자에 대해 무조건 리턴하던 MemberServiceImpl클래스를 다음과 같이 수정합니다.
<MemberServiceImpl.java>
package org.edwith.webbe.securityexam.service;
import org.edwith.webbe.securityexam.dao.MemberDao;
import org.edwith.webbe.securityexam.dao.MemberRoleDao;
import org.edwith.webbe.securityexam.dto.Member;
import org.edwith.webbe.securityexam.dto.MemberRole;
import org.edwith.webbe.securityexam.service.security.UserEntity;
import org.edwith.webbe.securityexam.service.security.UserRoleEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
@Service
public class MemberServiceImpl implements MemberService {
// 생성자에 위해 주입되는 객체이고, 해당 객체를 초기화할 필요가 이후에 없기 때문에 final로 선언하였다.
// final로 선언하고 초기화를 안한 필드는 생성자에서 초기화를 해준다.
private final MemberDao memberDao;
private final MemberRoleDao memberRoleDao;
// @Service가 붙은 객체는 스프링이 자동으로 Bean으로 생성하는데
// 기본생성자가 없고 아래와 같이 인자를 받는 생성자만 있을 경우 자동으로 관련된 타입이 Bean으로 있을 경우 주입해서 사용하게 된다.
public MemberServiceImpl(MemberDao memberDao, MemberRoleDao memberRoleDao) {
this.memberDao = memberDao;
this.memberRoleDao = memberRoleDao;
}
@Override
@Transactional
public UserEntity getUser(String loginUserId) {
Member member = memberDao.getMemberByEmail(loginUserId);
return new UserEntity(member.getEmail(), member.getPassword());
}
@Override
@Transactional
public List<UserRoleEntity> getUserRoles(String loginUserId) {
List<MemberRole> memberRoles = memberRoleDao.getRolesByEmail(loginUserId);
List<UserRoleEntity> list = new ArrayList<>();
for(MemberRole memberRole : memberRoles) {
list.add(new UserRoleEntity(loginUserId, memberRole.getRoleName()));
}
return list;
}
}
수정을 한 후 웹 애플리케이션을 재시작 합니다. 웹 애플리케이션을 재시작한 후
앞에서 저장한 정보로 로그인을 시도합니다.
로그인이 성공하고 main 페이지가 잘 보여지는 것을 확인할 수 있습니다.
생각해보기
미리 입력(insert)해 둔 정보로 회원 가입을 하였습니다.
회원 가입 후 로그인을 하려면 데이터베이스에 저장하는 기능을 구현해야 합니다.
데이터베이스에 회원 정보를 저장하려면 어떻게 해야할까요?
comment
1. 회원가입 정보를 저장할 수 있는 뷰(jsp)를 만들고 Controller와 URL 매핑 (post)
2. Controller에서 받은 파라미터 (입력받은 회원정보)를 가지고 Service 메소드 호출
3. Service interface를 구현하는 ServiceImpl에서 Insert하는 dao 메소드를 호출
4. Dao에서 DTO와 DaoSqls를 통해 insert 수행 (회원정보 저장)
비밀번호를 암호화해서 저장해야 할텐데 이부분은 어디서 해야하는지 모르겠네요.
회원 가입 페이지를 만들어 회원 정보를 데이터베이스에 저장해야 합니다. 컨트롤러에 회원 정보 생성 요청이 들어왔을 때 MemberService를 사용해서 해당 요청을 서비스 레이어로 내리고, 서비스 레이어에서 Dao를 이용해 관련 테이블에 insert 쿼리를 수행해야 합니다.
중간에 강의가 하나 빠져있는 것 같습니다.
MemberService, UserEntity, UserRoleEntity, SecurityConfig 수정, loginform.jsp 같은 내용들이 없네요.