Spring

Spring Security - User Registration

728x90

참조한 강의 : www.udemy.com/course/spring-hibernate-tutorial/

 

 

- Spring Security & JDBC

앞의 포스팅까지는 DB 와 연동하는게 아니라, 그냥 스프링 앱 메모리에 유저 데이터를 저장해서 인증하는 방식이었는데, 당연히 실제 업무나 상품으로써 내놓을려면 DB 에 저장해놓는게 일반적이다.

 

그래서 이번 포스팅에서는 DB 와 Spring Security 를 연동하는 방법과 BCrypt 를 이용해서 암호화하여 비밀번호를 저장하는 방법 그리고 회원가입을 만드는 방법에 대해 알아본다.

 

먼저 DB 와 Spring Security 를 연동하는 방법에 대해 알아본다

 

Spring Security 와 DB 를 연동할때 주의할점은, Spring Security 가 기본값으로 사용하는, 테이블과 칼럼명이 존재한다.

예를들면 아래 그림 같은 것이 있다.

DB 에 스키마를 정의할때 반드시 위의 칼럼명과 테이블명이 똑같아야 한다.

그리고 DB 에 비밀번호를 저장할때는 정해진 형식이 있다.

 

{id}encodedPassword 

 

이런식으로 형식을 잡아놓고 집어넣어야 한다

 

{id} 에 들어가는 값으로는 아래와 같은게 있다.

 

1) noop

: 암호화를 전혀 하지 않고 plain text 로 넣는 방식

 

2) bcrypt

: BCrypt 암호화 방식을 이용해서 넣는 방식

 

비밀번호를 암호화할때는 보통 BCrypt 암호화 방식을 많이 사용한다.

 

그러면, 먼저 SQL 코드를 작성해서, 테이블을 만들고 샘플 데이터를 몇개 넣어보겠다.

 

- sql

DROP DATABASE  IF EXISTS `spring_security_demo_plaintext`;
 
CREATE DATABASE  IF NOT EXISTS `spring_security_demo_plaintext`;
USE `spring_security_demo_plaintext`;
 
--
-- Table structure for table `users`
--
 
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
  `username` varchar(50NOT NULL,
  `password` varchar(50NOT NULL,
  `enabled` tinyint(1NOT NULL,
  PRIMARY KEY (`username`)
ENGINE=InnoDB DEFAULT CHARSET=latin1;
 
--
-- Inserting data for table `users`
--
 
INSERT INTO `users` 
VALUES 
('john','{noop}test123',1),
('mary','{noop}test123',1),
('susan','{noop}test123',1);
 
 
--
-- Table structure for table `authorities`
--
 
DROP TABLE IF EXISTS `authorities`;
CREATE TABLE `authorities` (
  `username` varchar(50NOT NULL,
  `authority` varchar(50NOT NULL,
  UNIQUE KEY `authorities_idx_1` (`username`,`authority`),
  CONSTRAINT `authorities_ibfk_1` FOREIGN KEY (`username`REFERENCES `users` (`username`)
ENGINE=InnoDB DEFAULT CHARSET=latin1;
 
--
-- Inserting data for table `authorities`
--
 
INSERT INTO `authorities` 
VALUES 
('john','ROLE_EMPLOYEE'),
('mary','ROLE_EMPLOYEE'),
('mary','ROLE_MANAGER'),
('susan','ROLE_EMPLOYEE'),
('susan','ROLE_ADMIN');
 
 
 
cs

 

다음으로 스프링 프로젝트가 DB 와 연결할 수 있도록 pom.xml 파일에 JDBC, c3p0 를 설정해야 한다.

 

- pom.xml

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>${mysql.connector.version}</version>
</dependency>
 
<dependency>
    <groupId>com.mchange</groupId>
    <artifactId>c3p0</artifactId>
    <version>${c3po.version}</version>
</dependency>
cs

위의 mysql-connector-java 는 JDBC 를 의미하고, c3p0 는 DB Connection Pool 을 의미한다.

 

 

다음으로는, 스프링이 DB 에 접속할 수 있도록 driver, db url, username, password 등을 제공해야한다.

보통 이런 민감한 정보들은 별도의 파일로 관리하는데 

(node.js 기반의 웹 개발을 해봤다면 .env 파일을 다룬적이 있을 것이다. 이거와 똑같다)

여기서는 .properties 라는 확장자명을 가진 파일로 관리한다.

 

이 파일을 저장하는 위치는 아래와 같이 지정한다

 

 

- persistence-mysql.properties

#
# JDBC connection properties
#
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/DBURL?useSSL=false&serverTimezone=UTC
jdbc.user=username
jdbc.password=password
 
#
# Connection pool properties
#
connection.pool.initialPoolSize=5
connection.pool.minPoolSize=5
connection.pool.maxPoolSize=20
connection.pool.maxIdleTime=3000
cs

 

다음으로 스프링 설정 파일이 properties 파일을 읽어들일 수 있도록 설정해줘야한다.

이때는 @PropertySource 라는 어노테이션을 사용한다.

그리고 properties 에서 읽어들인 값을 저장하기 위한 별도의 객체가 필요한데, spring 에서는 이를 위한 Environment 라는 객체를 제공하고 있다.

 

- DemoAppConfig.java

@Configuration
@EnableWebMvc
@ComponentScan(basePackages="com.luv2code.springsecurity.demo")
@PropertySource("classpath:persistence-mysql.properties")
public class DemoAppConfig implements WebMvcConfigurer {
 
    // set up variable to hold the properties
    @Autowired
    private Environment env;
    
    // set up a logger for diagnostics
    private Logger logger = Logger.getLogger(getClass().getName());
    
    // define a bean for our security datasource    
    @Bean
    public DataSource securityDataSource() {
        
        // create connection pool
        ComboPooledDataSource securityDataSource = new ComboPooledDataSource();
 
        // set the jdbc driver
        try {
            securityDataSource.setDriverClass("com.mysql.jdbc.Driver");        
        }
        catch (PropertyVetoException exc) {
            throw new RuntimeException(exc);
        }
        
        // for sanity's sake, let's log url and user ... just to make sure we are reading the data
        logger.info("jdbc.url=" + env.getProperty("jdbc.url"));
        logger.info("jdbc.user=" + env.getProperty("jdbc.user"));
        
        // set database connection props
        securityDataSource.setJdbcUrl(env.getProperty("jdbc.url"));
        securityDataSource.setUser(env.getProperty("jdbc.user"));
        securityDataSource.setPassword(env.getProperty("jdbc.password"));
        
        // set connection pool props
        securityDataSource.setInitialPoolSize(
        getIntProperty("connection.pool.initialPoolSize"));
 
        securityDataSource.setMinPoolSize(
                getIntProperty("connection.pool.minPoolSize"));
        
        securityDataSource.setMaxPoolSize(
                getIntProperty("connection.pool.maxPoolSize"));
        
        securityDataSource.setMaxIdleTime(
                getIntProperty("connection.pool.maxIdleTime"));
                
        return securityDataSource;
    }
    
    // need a helper method 
    // read environment property and convert to int    
    private int getIntProperty(String propName) {
        
        String propVal = env.getProperty(propName);
        
        // now convert to int
        int intPropVal = Integer.parseInt(propVal);
        
        return intPropVal;
    }
    
}
cs

@PropertySource 에서 classpath 의 기본 경로는, src/main/resources 이다. 

 

그리고 Connection Pool 을 만들기 위한 객체로 ComboPooledDataSource 라는 객체가 사용되었다.

그 밑의 try catch 구문은 딱히 설명하지 않더라도 직관적으로 이해가된다.

 

그리고 아래의 getIntProperty 메소드는, connection pool 관련해서 properties 파일에서 값을 얻어올때, 설정값은 숫자로 넣었지만 Environment 객체가 읽어들이는 값은 문자열로 인식되기 때문에, 이 값을 숫자로 변환하는 과정이 필요하다. 이를 위해서 넣어준 메소드이다.

 

다음으로 Spring Security Config 파일을 수정해야한다.

이전까지는 in-memory 방식이었으므로, 그 코드들을 지우고, DB 와 연결하도록 수정해야한다.

 

- DemoSecurityConfig.java

@Configuration
@EnableWebSecurity
public class DemoSecurityConfig extends WebSecurityConfigurerAdapter {
   @Autowired
   private DataSource securityDataSource;
    
   @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.jdbcAuthentication().dataSource(securityDataSource);
    }    
}
cs

 

DataSource 는 javax.sql.DataSource 에서 import 해야한다.

(DataSource ? : deepweller.tistory.com/6)

 

여기까지 하면 DB 와 연동을 하는 기본작업을 끝낸것이다.

 

 

 

- Password Encryption

: 다음으로 비밀번호를 암호화 하여 DB 에 저장하는 방법에 대해 알아보자

위에서 한 예제는 당연히 비밀번호를 전혀 암호화 하지 않고 저장했기 때문에, 보안상의 문제가 반드시 발생할 가능성이 있다.

일반적으로 비밀번호 암호화 할때 가장 많이 쓰는게 BCrypt 해싱 방식 이다.

BCrypt 는 단방향 해시 함수를 사용하는데, 단방향 해시 함수는 같은 평문을 넣더라도 해싱함수를 거칠때 마다 매번 다른 해시값이 나오게된다. BCrypt 는 해시 함수에 추가적으로 Salting 과 Key Stretching 라는것을 이용해서 만든 암호화 방식이다 

자세한 사항은 아래 참조

velog.io/@sungjun-jin/bcrypt

 

bcrypt

bcrypt란 > 1999년에 Niels Provos와 David Mazieres가 발표한 가장 강력한 단방향 비밀번호 해시 매커니즘 중 하나이다. C, C++, C#, Go, Java, PHP, Perl, Python, Ruby등의 언어를 지원한다. 단방향 해시 함수 > 단방향

velog.io

 

Spring Security 에 이 BCrypt 를 어떻게 적용시키는지 알아보자

 

먼저 위의 예제와는 다르게 DB 의 칼럼을 약간 수정할 필요가 있다.

password 부분의 최소 길이가 68 글자여야한다.

{bcrypt} 라는 키워드가 앞에 붙어서 8글자

해시값 60글자가 붙어서 최소 68 글자 이상 붙도록 password 칼럼에 대한 수정이 필요하다.

(위의 예제에선 50글자로 해놨다)

 

 

 

 

- User Registration

: 다음으로 회원가입 방법에 대해 알아본다

먼저 스키마를 아래 그림처럼 약간 바꿀 것이다

- sql

DROP DATABASE  IF EXISTS `spring_security_custom_user_demo`;
 
CREATE DATABASE  IF NOT EXISTS `spring_security_custom_user_demo`;
USE `spring_security_custom_user_demo`;
 
--
-- Table structure for table `user`
--
 
DROP TABLE IF EXISTS `user`;
 
CREATE TABLE `user` (
  `id` int(11NOT NULL AUTO_INCREMENT,
  `username` varchar(50NOT NULL,
  `password` char(80NOT NULL,
  `first_name` varchar(50NOT NULL,
  `last_name` varchar(50NOT NULL,
  `email` varchar(50NOT NULL,
  PRIMARY KEY (`id`)
ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;
 
--
-- Dumping data for table `user`
--
-- NOTE: The passwords are encrypted using BCrypt
--
-- A generation tool is avail at: http://www.luv2code.com/generate-bcrypt-password
--
-- Default passwords here are: fun123
--
 
INSERT INTO `user` (username,password,first_name,last_name,email)
VALUES 
('john','$2a$04$eFytJDGtjbThXa80FyOOBuFdK2IwjyWefYkMpiBEFlpBwDH.5PM0K','John','Doe','john@luv2code.com'),
('mary','$2a$04$eFytJDGtjbThXa80FyOOBuFdK2IwjyWefYkMpiBEFlpBwDH.5PM0K','Mary','Public','mary@luv2code.com'),
('susan','$2a$04$eFytJDGtjbThXa80FyOOBuFdK2IwjyWefYkMpiBEFlpBwDH.5PM0K','Susan','Adams','susan@luv2code.com');
 
 
--
-- Table structure for table `role`
--
 
DROP TABLE IF EXISTS `role`;
 
CREATE TABLE `role` (
  `id` int(11NOT NULL AUTO_INCREMENT,
  `name` varchar(50DEFAULT NULL,
  PRIMARY KEY (`id`)
ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;
 
--
-- Dumping data for table `roles`
--
 
INSERT INTO `role` (name)
VALUES 
('ROLE_EMPLOYEE'),('ROLE_MANAGER'),('ROLE_ADMIN');
 
--
-- Table structure for table `users_roles`
--
 
DROP TABLE IF EXISTS `users_roles`;
 
CREATE TABLE `users_roles` (
  `user_id` int(11NOT NULL,
  `role_id` int(11NOT NULL,
  
  PRIMARY KEY (`user_id`,`role_id`),
  
  KEY `FK_ROLE_idx` (`role_id`),
  
  CONSTRAINT `FK_USER_05` FOREIGN KEY (`user_id`
  REFERENCES `user` (`id`
  ON DELETE NO ACTION ON UPDATE NO ACTION,
  
  CONSTRAINT `FK_ROLE` FOREIGN KEY (`role_id`
  REFERENCES `role` (`id`
  ON DELETE NO ACTION ON UPDATE NO ACTION
ENGINE=InnoDB DEFAULT CHARSET=latin1;
 
SET FOREIGN_KEY_CHECKS = 1;
 
--
-- Dumping data for table `users_roles`
--
 
INSERT INTO `users_roles` (user_id,role_id)
VALUES 
(11),
(21),
(22),
(31),
(33)
cs

 

테이블과 샘플 데이터 몇개를 넣어줬다.

 

다음으로 hibernate validator, spring transaction, hibernate ORM 등을 사용하기 위해서 pom.xml 에 의존성 주입을 해준다

 

- pom.xml

<dependencies>        
        
    <!-- Hibernate ORM -->
    <dependency>
      <groupId>org.hibernate.validator</groupId>
      <artifactId>hibernate-validator</artifactId>
      <version>6.2.0.Final</version>
    </dependency>
        
    <!-- Spring Transaction -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>${springframework.version}</version>
    </dependency>
        
    <!-- Spring ORM -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-orm</artifactId>
      <version>${springframework.version}</version>
    </dependency>
        
    <!-- Hibernate Core -->
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-core</artifactId>
      <version>${hibernate.version}</version>
    </dependency>
 
</dependencies>
cs

 

그리고 프로젝트의 구조를 레이어드 아키텍처 기반의 구조로 만들 것이므로, DAO, Service, Controller 의 3단계로 구성할 것이다.

그리고, ORM 을 위해서 Entity 클래스도 따로 만들어 줄 것이며, Validation 을 위한 자체적인 어노테이션도 몇개 제작한다.

 

 

먼저 spring security config 파일에 spring security 가 기본적으로 제공하는 BCryptPasswordEncoder 클래스를 Bean 으로 선언해서 Spring 이 관리하도록 해주고, 레이어드 아키텍쳐를 거쳐서 마지막 DAO 를 통해 DB 에 암호를 저장할때 암호화한 상태로 저장할 수 있도록 DaoAuthenticationProvider 를 Bean 으로 선언하여 관리한다,

 

- DemoSecurityConfig.java

@Configuration
@EnableWebSecurity
public class DemoSecurityConfig extends WebSecurityConfigurerAdapter {
 
    // add a reference to our security data source
    @Autowired
    private UserService userService;
            
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
 
    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider auth = new DaoAuthenticationProvider();
        auth.setUserDetailsService(userService); //set the custom user details service
        auth.setPasswordEncoder(passwordEncoder()); //set the password encoder - bcrypt
        return auth;
    }    
}
cs

 

다음으로 회원가입시에 유저가 입력한 내용에 대해서 데이터 유효성 검증을 위한 코드를 넣는다.

 

- CrmUser.java

@FieldMatch.List({@FieldMatch(first = "password", second = "matchingPassword", message = "The Password Fields must match")})
public class CrmUser {
    
    @NotNull(message="is required")
    @Size(min=1, message="is required")
    private String userName;
    
    @NotNull(message="is required")
    @Size(min=1, message="is required")
    private String password;
    
    @NotNull(message="is required")
    @Size(min=1, message="is required")
    private String matchingPassword;
    
    @NotNull(message="is required")
    @Size(min=1, message="is required")
    private String firstName;
    
    @NotNull(message="is required")
    @Size(min=1, message="is required")
    private String lastName;
    
    @ValidEmail
    @NotNull(message="is required")
    @Size(min=1, message="is required")
    private String email;
    
    // getter, setter, constructor 생략
}
cs

@FieldMatch 와 @ValidEmail 은 스프링이 제공하지 않는 자체적으로 만든 어노테이션이다.

어노테이션은 interface 기반이므로, 선언과 정의를 따로 해야한다

 

 

- @FieldMatch Interface

@Constraint(validatedBy = FieldMatchValidator.class)
@Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FieldMatch {
    String message() default "";
    Class<?>[] groups() default {};
    Class<extends Payload>[] payload() default {};
    
    String first();
    String second();
    
    @Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @interface List {
        FieldMatch[] value();
    }
}
cs

 

- @FieldMatch Implementation

public class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object> {
    
    private String firstFieldName;
    private String secondFieldName;
    private String message;
 
    @Override
    public void initialize(final FieldMatch constraintAnnotation) {
            firstFieldName = constraintAnnotation.first();
            secondFieldName = constraintAnnotation.second();
        message = constraintAnnotation.message();
    }
 
    @Override
    public boolean isValid(final Object value, final ConstraintValidatorContext context) {
        boolean valid = true;
        try {
            final Object firstObj = new BeanWrapperImpl(value).getPropertyValue(firstFieldName);
            final Object secondObj = new BeanWrapperImpl(value).getPropertyValue(secondFieldName);
 
            valid =  firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
        }
        catch (final Exception ignore) {
            // we can ignore
        }
 
        if (!valid) {
            context.buildConstraintViolationWithTemplate(message)
                    .addPropertyNode(firstFieldName)
                    .addConstraintViolation()
                    .disableDefaultConstraintViolation();
        }
 
        return valid;
    }
    
}
cs

 

 

- @ValidEmail Interface

@Constraint(validatedBy=EmailValidator.class)
@Target({ ElementType.TYPE, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ValidEmail {
    
    String message() default "Invalid email";
    
    Class<?>[] groups() default {};
    
    Class<extends Payload>[] payload() default {};
}
cs

 

- @ValidEmail Implementation

public class EmailValidator implements ConstraintValidator<ValidEmail, String> {
 
    private Pattern pattern;
    private Matcher matcher;
    private static final String EMAIL_PATTERN = "^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@"
            + "[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$";
    
    @Override
    public boolean isValid(final String email, final ConstraintValidatorContext context) {
        pattern = Pattern.compile(EMAIL_PATTERN);
        if (email == null) {
            return false;
        }
        matcher = pattern.matcher(email);
        return matcher.matches();
    }
}
cs

 

 

다음으로 hibernate 가 스캔할 패키지명과 연동될 SQL 종류를 선정해야한다

기존에 만들었던 persistence-mysql.properties 에 다음의 내용을 추가한다

 

- persistence-mysql.properties

# Hibernate properties
hibernate.dialect=org.hibernate.dialect.MySQLDialect
hibernate.show_sql=true
hiberante.packagesToScan=com.luv2code.springsecurity.demo.entity
cs

 

 

다음으로, Hibernate ORM 이 DB 와 연동할 수 있도록 Entity 클래스를 선언한다

 

- User.java

@Entity
@Table(name="user")
public class User {
    
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(name="id")
    private Long id;
    
    @Column(name="username")
    private String userName;
    
    @Column(name="password")
    private String password;
    
    @Column(name="first_name")
    private String firstName;
    
    @Column(name="last_name")
    private String lastName;
    
    @Column(name="email")
    private String email;
    
    @ManyToMany(fetch=FetchType.LAZY, cascade=CascadeType.ALL)
    @JoinTable(name="users_roles", joinColumns=@JoinColumn(name="user_id"), inverseJoinColumns=@JoinColumn(name="role_id"))
    private Collection<Role> roles;
    
    // getter, setter 등 생략..
}
cs

 

- Role.java

@Entity
@Table(name="role")
public class Role {
    
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(name="id")
    private Long id;
    
    @Column(name="name")
    private String name;
 
    // getter, setter 등 생략..
}
cs

 

 

다음으로, jsp 에 회원가입 버튼과 회원가입을 위한 form 화면을 만든다

 

- login.jsp

<!-- 나머지 로그인 부분 생략... -->
<div>
    <a href="${pageContext.request.contextPath}/register/showRegistrationForm" class="btn btn-primary" role="button" aria-pressed="true">Register New User</a>
</div>
cs

 

- registration-form.jsp

<!-- taglib 생략.. -->
<!doctype html>
<html lang="en">
<head>
    <!-- css, bootstrap 부분 생략... -->
</head>
 
<body>
    <div>        
        <div id="loginbox" style="margin-top: 50px;"
            class="mainbox col-md-3 col-md-offset-2 col-sm-6 col-sm-offset-2">
            
            <div class="panel panel-primary">
 
                <div class="panel-heading">
                    <div class="panel-title">Register New User</div>
                </div>
 
                <div style="padding-top: 30px" class="panel-body">
 
                    <!-- Registration Form -->
                    <form:form action="${pageContext.request.contextPath}/register/processRegistrationForm" 
                                 modelAttribute="crmUser"
                                 class="form-horizontal">
 
                        <!-- Place for messages: error, alert etc ... -->
                        <div class="form-group">
                            <div class="col-xs-15">
                                <div>
                                
                                    <!-- Check for registration error -->
                                    <c:if test="${registrationError != null}">
                                
                                        <div class="alert alert-danger col-xs-offset-1 col-xs-10">
                                            ${registrationError}
                                        </div>
        
                                    </c:if>
                                                                            
                                </div>
                            </div>
                        </div>
 
                        <!-- User name -->
                        <div style="margin-bottom: 25px" class="input-group">
                            <span class="input-group-addon"><i class="glyphicon glyphicon-user"></i></span> 
                            <form:errors path="userName" cssClass="error" />
                            <form:input path="userName" placeholder="username (*)" class="form-control" />
                        </div>
 
                        <!-- Password -->
                        <div style="margin-bottom: 25px" class="input-group">
                            <span class="input-group-addon"><i class="glyphicon glyphicon-lock"></i></span> 
                            <form:errors path="password" cssClass="error" />
                            <form:password path="password" placeholder="password (*)" class="form-control" />
                        </div>
                        
                        <!-- Confirm Password -->
                        <div style="margin-bottom: 25px" class="input-group">
                            <span class="input-group-addon"><i class="glyphicon glyphicon-lock"></i></span> 
                            <form:errors path="matchingPassword" cssClass="error" />
                            <form:password path="matchingPassword" placeholder="confirm password (*)" class="form-control" />
                        </div>
                        
                        <!-- First name -->
                        <div style="margin-bottom: 25px" class="input-group">
                            <span class="input-group-addon"><i class="glyphicon glyphicon-user"></i></span> 
                            <form:errors path="firstName" cssClass="error" />
                            <form:input path="firstName" placeholder="first name (*)" class="form-control" />
                        </div>
                        
                        <!-- Last name -->
                        <div style="margin-bottom: 25px" class="input-group">
                            <span class="input-group-addon"><i class="glyphicon glyphicon-user"></i></span> 
                            <form:errors path="lastName" cssClass="error" />
                            <form:input path="lastName" placeholder="last name (*)" class="form-control" />
                        </div>
                        
                        <!-- Email -->
                        <div style="margin-bottom: 25px" class="input-group">
                            <span class="input-group-addon"><i class="glyphicon glyphicon-user"></i></span> 
                            <form:errors path="email" cssClass="error" />
                            <form:input path="email" placeholder="email (*)" class="form-control" />
                        </div>
                        
                        <!-- Register Button -->
                        <div style="margin-top: 10px" class="form-group">                        
                            <div class="col-sm-6 controls">
                                <button type="submit" class="btn btn-primary">Register</button>
                            </div>
                        </div>
                        
                    </form:form>
                </div>
            </div>
        </div>
    </div>
</body>
</html>
cs

 

- registration-confirmation.jsp

<!doctype html>
<html>
 
<head>
    <title>Registration Confirmation</title>
</head>
 
<body>
 
    <h2>User registered successfully!</h2>
 
    <hr>
    
    <a href="${pageContext.request.contextPath}/showMyLoginPage">Login with new user</a>
    
</body>
 
</html>
cs

 

 

다음으로 컨트롤러를 만든다.

 

- RegistrationController.java

@Controller
@RequestMapping("/register")
public class RegistrationController {
    
    @Autowired
    private UserService userService;
    
    private Logger logger = Logger.getLogger(getClass().getName());
    
    @InitBinder
    public void initBinder(WebDataBinder dataBinder) {
        StringTrimmerEditor stringTrimmerEditor = new StringTrimmerEditor(true);
        
        dataBinder.registerCustomEditor(String.class, stringTrimmerEditor);
    }
    
    @GetMapping("/showRegistrationForm")
    public String showMyLoginPage(Model model) {
        model.addAttribute("crmUser" ,new CrmUser());
        return "registration-form";
    }
    
    @PostMapping("/processRegistrationForm")
    public String processRegistrationForm(@Valid @ModelAttribute("crmUser") CrmUser crmUser, BindingResult bindingResult, Model model) {
        String userName = crmUser.getUserName();
        
        logger.info("Processing registration form for : " + userName);
        
        if (bindingResult.hasErrors()) {
            return "registration-form";
        }
        
        User existingUser = userService.findByUserName(userName);
        
        if (existingUser != null) {
            model.addAttribute("crmUser"new CrmUser());
            model.addAttribute("registrationError""User name already exists");
            
            logger.warning("User name already exists");
            return "registration-form";
        }
        
        userService.save(crmUser);
        
        logger.info("Successfully created user" + userName);
        
        return "registration-confirmation";
    }
}
cs

 

 

 

다음으로 Service 객체와 DAO 객체를 만든다.

 

- UserService.java

public interface UserService extends UserDetailsService {
 
    User findByUserName(String userName);
 
    void save(CrmUser crmUser);
}
cs

 

- UserServiceImpl.java

@Service
public class UserServiceImpl implements UserService {
 
    // need to inject user dao
    @Autowired
    private UserDao userDao;
 
    @Autowired
    private RoleDao roleDao;
    
    @Autowired
    private BCryptPasswordEncoder passwordEncoder;
 
    @Override
    @Transactional
    public User findByUserName(String userName) {
        // check the database if the user already exists
        return userDao.findByUserName(userName);
    }
 
    @Override
    @Transactional
    public void save(CrmUser crmUser) {
        User user = new User();
         // assign user details to the user object
        user.setUserName(crmUser.getUserName());
        user.setPassword(passwordEncoder.encode(crmUser.getPassword()));
        user.setFirstName(crmUser.getFirstName());
        user.setLastName(crmUser.getLastName());
        user.setEmail(crmUser.getEmail());
 
        // give user default role of "employee"
        user.setRoles(Arrays.asList(roleDao.findRoleByName("ROLE_EMPLOYEE")));
 
         // save user in the database
        userDao.save(user);
    }
 
    @Override
    @Transactional
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        User user = userDao.findByUserName(userName);
        if (user == null) {
            throw new UsernameNotFoundException("Invalid username or password.");
        }
        return new org.springframework.security.core.userdetails.User(user.getUserName(), user.getPassword(),
                mapRolesToAuthorities(user.getRoles()));
    }
 
    private Collection<extends GrantedAuthority> mapRolesToAuthorities(Collection<Role> roles) {
        return roles.stream().map(role -> new SimpleGrantedAuthority(role.getName())).collect(Collectors.toList());
    }
}
 
cs

 

 

- UserDao.java

public interface UserDao {
 
    User findByUserName(String userName);
    
    void save(User user);
    
}
cs

 

- UserDaoImpl.java

@Repository
public class UserDaoImpl implements UserDao {
 
    // need to inject the session factory
    @Autowired
    private SessionFactory sessionFactory;
 
    @Override
    public User findByUserName(String theUserName) {
        // get the current hibernate session
        Session currentSession = sessionFactory.getCurrentSession();
 
        // now retrieve/read from database using username
        Query<User> theQuery = currentSession.createQuery("from User where userName=:uName", User.class);
        theQuery.setParameter("uName", theUserName);
        User theUser = null;
        try {
            theUser = theQuery.getSingleResult();
        } catch (Exception e) {
            theUser = null;
        }
 
        return theUser;
    }
 
    @Override
    public void save(User theUser) {
        // get current hibernate session
        Session currentSession = sessionFactory.getCurrentSession();
 
        // create the user ... finally LOL
        currentSession.saveOrUpdate(theUser);
    }
 
}
cs

 

 

- RoleDao.java

public interface RoleDao {
    
    public Role findRoleByName(String roleName);
}
cs

 

- RoleDaoImpl.java

@Repository
public class RoleDaoImpl implements RoleDao {
    
    @Autowired
    private SessionFactory sessionFactory;
 
    @Override
    public Role findRoleByName(String roleName) {
        Session currentSession = sessionFactory.getCurrentSession();
        
        Query<Role> query = currentSession.createQuery("from Role where name=:roleName", Role.class);
        
        query.setParameter("roleName", roleName);
        
        Role role = null;
        try {
            role = query.getSingleResult();
        } catch (Exception e) {
            role = null;
            e.printStackTrace();
        }
        
        return role;
    }
}
cs

 

 

여기 까지하면 기본적인 틀은 다 갖췄다.

아래는 실행화면 이다

회원가입화면

 

회원가입으로 넣은 데이터

 

 

 

 

- References)

1. DataSource : deepweller.tistory.com/6 

2. BCrypt : velog.io/@sungjun-jin/bcrypt

 

 

728x90

'Spring' 카테고리의 다른 글

Spring REST - REST Controller  (0) 2021.03.11
Spring REST - Overview  (0) 2021.03.07
Spring Security - User Roles  (0) 2021.03.06
Spring Security - CSRF (Cross Site Request Forgery)  (0) 2021.03.06
Spring Security - Custom Login Form  (0) 2021.03.05