IT/development

[springBoot] spring security passwordEncode

알 수 없는 사용자 2023. 6. 4. 14:11
반응형

목차

    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

    반응형