IT/development

[springBoot] spring security passwordEncode

알 수 없는 사용자 2023. 6. 4.

ddl

/* 사용자 */
CREATE TABLE "USER_INFO"
   ("USER_NO" NUMBER NOT NULL ENABLE,
	"USER_ID" VARCHAR2(20) NOT NULL ENABLE,
	"USER_PASSWORD" VARCHAR2(250) NOT NULL ENABLE,
	"USER_NAME" VARCHAR2(20) NOT NULL ENABLE,
	"USER_EMAIL" VARCHAR2(20) NOT NULL ENABLE,
	"USE_YN" CHAR(1) DEFAULT 'Y' NOT NULL ENABLE,
	"REG_DATE" DATE DEFAULT SYSDATE NOT NULL ENABLE,
	"MOD_DATE" DATE DEFAULT SYSDATE,
	 CONSTRAINT "USER_INFO_PK" PRIMARY KEY ("USER_NO", "USER_ID")
)
;
COMMENT ON COLUMN USER_INFO.USER_NO IS '사용자 순번(시퀀스:USER_NO_SEQ)';
COMMENT ON COLUMN USER_INFO.USER_ID IS '사용자 아이디';
COMMENT ON COLUMN USER_INFO.USER_PASSWORD IS '사용자 비밀번호';
COMMENT ON COLUMN USER_INFO.USER_NAME IS '사용자명';
COMMENT ON COLUMN USER_INFO.USER_EMAIL IS '사용자 이메일';
COMMENT ON COLUMN USER_INFO.USE_YN IS '사용여부';
COMMENT ON COLUMN USER_INFO.REG_DATE IS '등록일';
COMMENT ON COLUMN USER_INFO.MOD_DATE IS '수정일';
--시퀀스
CREATE SEQUENCE USER_NO_SEQ INCREMENT BY 1 MINVALUE 1 MAXVALUE 99999999 CYCLE NOCACHE ORDER ;

의존성 추가

plugins {
    id 'java'
    id 'org.springframework.boot' version '2.7.11'
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

group = 'study'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    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.3.0'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    
    //추가
    implementation 'org.springframework.boot:spring-boot-starter-security'
    
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16'
    implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    runtimeOnly 'com.oracle.database.jdbc:ojdbc8'
    testCompileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
    useJUnitPlatform()
}

메인메소드에서 시큐리티 설정 제외(안하면 시큐리티 기본 로그인 화면 표시됨)

package study.thboard2;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;

//exclude 추가
@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
public class ThBoard2Application {

    public static void main(String[] args) {
        SpringApplication.run(ThBoard2Application.class, args);
    }

}

config

package study.thboard2.config;

import org.springframework.context.annotation.Bean;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 인증 및 인가에 대한 설정
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                // 인증 없이 허용할 url
                .antMatchers("/", "/listAjax", "/register", "/login", "/css/**", "/assets/**", "/js/**").permitAll()
                .anyRequest().authenticated();
    }
}

xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://www.mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="study.thboard2.mapper.UserMapper">

<!--사용자 정보 저장-->
    <insert id="insertUser" parameterType="UserVo">
        <selectKey keyProperty="userNo" resultType="INTEGER" order="BEFORE">
            SELECT USER_NO_SEQ.NEXTVAL FROM DUAL
        </selectKey>
        /* 사용자 정보 저장 */
        INSERT INTO USER_INFO
            (
                USER_NO                 /* 사용자 순번 */
              , USER_ID                 /* 사용자 아이디 */
              , USER_PASSWORD           /* 사용자 비밀번호 */
              , USER_NAME               /* 사용자명 */
              , USER_EMAIL              /* 사용자 이메일 */
              , REG_DATE                /* 등록일 */
            )
        VALUES
            (
                #{userNo}
              , #{userId}
              , #{userPassword}
              , #{userName}
              , #{userEmail}
              , SYSDATE
            )
    </insert>

    <!--사용자 정보 확인(로그인 시 활용)-->
    <select id="selectByUserId" parameterType="map" resultType="UserVo">
        /* 사용자 정보 확인(로그인 시 활용) */
        SELECT USER_NO AS userNo                /* 사용자 순번 */
             , USER_ID AS userId                /* 사용자 아이디 */
             , USER_PASSWORD AS userPassword    /* 사용자 비밀번호 */
             , USER_NAME AS userName            /* 사용자명 */
             , USER_EMAIL AS userEmail          /* 사용자 이메일 */
             , USE_YN AS useYn                  /* 사용여부 */
             , REG_DATE AS regDate              /* 등록일 */
             , MOD_DATE AS modDate              /* 수정일 */
          FROM USER_INFO
         WHERE USER_ID = #{userId}
    </select>
</mapper>

mapper

package study.thboard2.mapper;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import study.thboard2.domain.vo.UserVo;

import java.util.List;

@Repository @Mapper
public interface UserMapper {

    /* 사용자 정보 저장 */
    void insertUser(UserVo userVo);

    /* 사용자 정보 확인(로그인 시 활용) */
    UserVo selectByUserId(@Param("userId") String userId);
}

service

package study.thboard2.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import study.thboard2.domain.vo.UserVo;
import study.thboard2.mapper.UserMapper;

import java.util.List;

@Service
@Slf4j
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserService {

    private final UserMapper userMapper;
    //아래 추가
    private final PasswordEncoder bCryptPasswordEncoder;

    /**
     * 사용자 정보 저장
     * @param  userVo
     * @throws Exception
     */
    @Transactional
    public void regUser(UserVo userVo) throws Exception{
        //비밀번호 암호화
        userVo.hashPassword(bCryptPasswordEncoder);
        userMapper.insertUser(userVo);
    }

    /**
     * 아이디/비밀번호 확인
     * @param userId
     * @param userPassword
     * @return
     */
    public String login(String userId, String userPassword) throws Exception{
        UserVo userInfo = userMapper.selectByUserId(userId);
        return (userInfo.checkPassword(userPassword, bCryptPasswordEncoder) == true ? userInfo.getUserId() : "none");
    } 
}

Vo

package study.thboard2.domain.vo;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;

@Data
@Slf4j
//사용자 vo
//(vo에 암호화, 비밀번호 비교 메소드를 추가 했음, 서비스에 해도 됨, 이건 스타일)
public class UserVo extends CommonVo {
    private Integer no;                 //사용자 rownum
    private Integer userNo;             //사용자 순번(시퀀스, pk)
    private String userId;              //사용자 아이디(pk)
    private String userPassword;        //사용자 비밀번호
    private String userName;            //사용자명
    private String userEmail;           //사용자 이메일
    private char useYn;                 //사용여부
    private String modDate;


    /**
     * 비밀번호 암호화
     * @param passwordEncoder
     * @return
     */
    public UserVo hashPassword(PasswordEncoder passwordEncoder) {
        this.userPassword = passwordEncoder.encode(this.userPassword);
        return this;
    }

    /**
     * 비밀번호 확인
     * @param orgPassword 평문 암호
     * @param passwordEncoder
     * @return
     */
    public boolean checkPassword(String orgPassword, PasswordEncoder passwordEncoder) {
        //passwordEncoder.matches가 평문 암호를 해싱 암호화값과 비교 후 true or false return
        return passwordEncoder.matches(orgPassword, this.userPassword);
    }
}

controller

package study.thboard2.controller;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import study.thboard2.domain.vo.UserVo;
import study.thboard2.service.UserService;

import javax.servlet.http.HttpSession;

@Controller
@Slf4j
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    /**
     * 로그인 화면
     * @param session
     * @return
     */
    @GetMapping("/login")
    public String loginForm(HttpSession session) {
        String id = (String) session.getAttribute("id");
        log.info("stored session id =[{}]", id);
        return id != null ? "redirect:/" : "pages/login" ;
    }

    /**
     * 회원가입 화면
     * @return
     */
    @GetMapping("/register")
    public String registerForm() {
        return "pages/register";
    }

    /**
     * 회원가입 처리
     * @param userVo
     * @return
     */
    @PostMapping("/register")
    public String register(@ModelAttribute UserVo userVo) {
        try {
            userService.regUser(userVo);
        } catch (Exception e) {
            log.info("Exception => [{}] ", e.getMessage());
        }
        return "redirect:/login";
    }

    /**
     * 사용자 로그인
     * @param userId
     * @param userPassword
     * @param session
     * @return
     */
    @PostMapping("/login")
    @ResponseBody
    public ResponseEntity<?> login(@RequestParam String userId,
                                @RequestParam String userPassword,
                                HttpSession session) throws Exception {

        String id = userService.login(userId, userPassword);
        if(id == "none") return new ResponseEntity<>(0, HttpStatus.BAD_REQUEST);

        session.setAttribute("id", id);
        return new ResponseEntity<>(1, HttpStatus.OK);
    }

}

html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layouts/default_layout}">

<!-- Content -->
<div layout:fragment="content">
    <main>
        <form id="frm" action="/login" method="post">
                <div class="container">
                    <div class="row justify-content-center">
                        <div class="col-lg-5">
                            <div class="card shadow-lg border-0 rounded-lg mt-5">
                                <div class="card-header"><h3 class="text-center font-weight-light my-4">Login</h3></div>
                                <div class="card-body">
                                    <form>
                                        <div class="form-floating mb-3">
                                            <input class="form-control" id="userId" name="userId" type="id" placeholder="name@example.com" />
                                            <label for="inputEmail">ID</label>
                                        </div>
                                        <div class="form-floating mb-3">
                                            <input class="form-control" id="userPassword" name="userPassword" type="password" placeholder="Password" />
                                            <label for="inputPassword">Password</label>
                                        </div>
                                        <div class="form-check mb-3">
                                            <input class="form-check-input" id="inputRememberPassword" type="checkbox" value="" />
                                            <label class="form-check-label" for="inputRememberPassword">Remember Password</label>
                                        </div>
                                        <div class="d-flex align-items-center justify-content-between mt-4 mb-0">
                                            <a class="small" href="password.html">Forgot Password?</a>
<!--                                            <button type="submit" class="btn btn-success">Login</button>-->
                                            <button type="button" id="loginBtn" class="btn btn-success">Login</button>
                                        </div>
                                    </form>
                                </div>
                                <div class="card-footer text-center py-3">
                                    <div class="small"><a th:href="@{/register}">회원가입</a></div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
        </form>
    </main>
</div>


    <script layout:fragment="script" th:inline="javascript" type="text/javascript">
        $(document).ready(function () {

            /* 로그인 ajax */
            $("#loginBtn").on("click", function () {
                $.ajax({
                    url : '/login',
                    type : 'post',
                    dataType : 'json',
                    data : $("#frm").serialize(),
                    success: function (result) {
                        if(result === 1)
                        window.location = "/";
                    },
                    error: function (request, status, error) {
                        alert("로그인 실패했어. 아이디와 패스워드를 확인하렴");
                        console.log(error);
                        window.location = "/login";
                    }
                });
            });

        });
    </script>

</html>

참조: https://hou27.tistory.com/entry/Spring-Boot-Spring-Security-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-%EC%95%94%ED%98%B8%ED%99%94

 

[Spring Boot] Spring Security 적용하기 - 암호화

프로젝트를 진행하면서 사용자 시스템을 구축한다면 필연적으로 인증 로직도 구현해야한다. 이 과정에서 만약 사용자의 비밀번호를 평문(Plain Text)으로 저장한다면, 심각한 보안상 문제를 초래...

hou27.tistory.com

댓글