메뉴 건너뛰기

Spring Security 적용법

김지훈 2024.09.20 15:18 조회 수 : 53

 

[ Spring Security 적용법 → STS : 4.22.0  /  spring boot version : 3.3.3  /  java : 21  /  spring security 6 ]       

 

*** html과 @RestController 사용하여 구현 ***

→ @RestController와 정적 HTML 자원을 함께 사용하는 것은 RESTful 아키텍처를 유지하면서 사용자 인터페이스를 제공하는 효과적인 방법이며, 이러한 접근 방식은 클라이언트와 서버의 독립성을 유지하며, 다양한 클라이언트에서 API를 통해 데이터에 접근할 수 있게 해주기 때문에 @RestController HTML을 사용하여 구현하였다.

→ @Controller 사용 시, Security의 permiAll() 메서드가 작동하지 않는 문제 발생

 

1. Project 생성

File New Project Spring Starter Project

그림1.png

 

 

2. Project 설정 및 의존성(Dependency) 설정

그림3.png


그림2.png

Finish 클릭하여 Project 생성

 

3. com.eugeneprogram 패키지 안에 dao 패키지를 생성하고 dao 패키지 안에 TestMapper 인테페이스 생성

그림4.png

 

package com.eugeneprogram.dao;

 

import java.util.List;

import java.util.Map;

 

 

public interface TestMapper {

       public int comparePw(String pw, String id) throws Exception;

       public List<Map<String, Object>> getAllList() throws Exception;

       public void insertTest(String name, String id, String pw, String phone) throws Exception;

}

 

4. com.eugeneprogram 패키지 안에 service 패키지를 생성하고 TestService 클래스 생성

그림5.png

 

package com.eugeneprogram.service;

 

import java.util.List;

import java.util.Map;

 

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Service;

 

import com.eugeneprogram.dao.TestMapper;

 

@Service

public class TestService {

    @Autowired

    TestMapper testMapper;

       public int comparePw(String pw, String id) throws Exception  {

        return testMapper.comparePw(pw, id);

    }

      

       public List<Map<String, Object>> getAllList() throws Exception  {

        return testMapper.getAllList();

    }

      

       public void insertTest(String name, String id, String pw, String phone) throws Exception      {

               tcMapper.insertTest(name, id, pw, phone);

       }

 

}

 

5. src/main/resources 위치에 mapper 폴더를 추가하고 testMapper xml파일 생성

그림6.png

 


<?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="com.eugeneprogram.dao.TestMapper">

    <select id="comparePw" resultType="Integer" parameterType="String">

        SELECT COUNT(*) AS tc_pw FROM tc WHERE tc_pw = #{pw} AND tc_mail = #{id};

    </select>

   

    <select id="getAllList" resultType="java.util.Map">

        SELECT * FROM tc;

    </select>

   

    <insert id="insertTest" parameterType="String">

        insert into tc(tc_name, tc_mail, tc_pw, tc_phone) values(#{name}, #{id}, password(#{pw}), #{phone});

    </insert>

</mapper>

 

6. com.eugeneprogram 패키지 안에 config 패키지 생성 후 DatabaseConfig 클래스 생성

그림7.png

 

package com.eugeneprogram.config;

 

import javax.sql.DataSource;

 

import org.apache.ibatis.session.SqlSessionFactory;

import org.mybatis.spring.SqlSessionFactoryBean;

import org.mybatis.spring.SqlSessionTemplate;

import org.mybatis.spring.annotation.MapperScan;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import org.springframework.transaction.annotation.EnableTransactionManagement;

 

@Configuration

@MapperScan(basePackages = "com.eugeneprogram.dao")

@EnableTransactionManagement

public class DatabaseConfig {

    @Bean

    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {

          final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();

          sessionFactory.setDataSource(dataSource);

          PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();

          sessionFactory.setMapperLocations(resolver.getResources("classpath:mapper/*.xml"));

          return sessionFactory.getObject();

    }

 

    @Bean

    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) throws Exception {

          final SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory);

          return sqlSessionTemplate;

    }

}

 

7. com.eugeneprogram 패키지 안에 security 패키지 생성 후 CustomAuthenticationProvider 클래스 생성

그림8.png

 

package com.eugeneprogram.security;

 

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;

import org.springframework.security.authentication.AuthenticationProvider;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

import org.springframework.security.core.Authentication;

import org.springframework.security.core.AuthenticationException;

import org.springframework.stereotype.Component;

 

import com.eugeneprogram.service.TestService;

 

import java.util.Arrays;

 

@Component

public class CustomAuthenticationProvider implements AuthenticationProvider {

    @Autowired

    //TcService에서 tc 테이블에 있는 id, pw들로 로그인을 해야한다.

    TestService testService;

   

    @Override

    // 만약 인증이 필요하게 경우 인증에 필요한 id, pw 설정해야 하는데, authenticate 메서드를 통해 임의의 id, pw 통해 인증하게끔 하는 메서드이다.

public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        int num = 0;

        // username password 각각 인증 입력된 값을 받아온다.

String username = authentication.getName();

        String password = String.valueOf(authentication.getCredentials());

        try {

            // 임의로 만든 쿼리를 username password 사용하여 tc테이블에 값이 존재하는지 여부를 판별한다.

            num = testService.comparePw(password, username);

        } catch (Exception e) {

            e.printStackTrace();

        }

        System.out.println(num);

       

       // num 값이 존재한다면, username password tc 테이블에 존재하므로 인증 허가

        // num 값이 존재하지 않다면, username password tc 테이블에 존재하지 않으므로 에러 발생

       if (num > 0) {

            return new UsernamePasswordAuthenticationToken(username, password, Arrays.asList());

        } else {

            throw new AuthenticationCredentialsNotFoundException("Error!");

        }

    }

 

    @Override

    public boolean supports(Class<?> authenticationType) {

        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authenticationType);

    }

 

}

 

8. com.eugeneprogram.config 패키지 안에 SecurityConfig 클래스 생성

그림9.png

 

package com.eugeneprogram.config;

 

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;

import org.springframework.security.web.SecurityFilterChain;

import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

 

import com.eugeneprogram.security.CustomAuthenticationProvider;

 

import static org.springframework.security.config.Customizer.withDefaults;

 

import org.springframework.beans.factory.annotation.Autowired;

 

@Configuration

@EnableWebSecurity

public class SecurityConfig {

   

    @Autowired

    private CustomAuthenticationProvider authenticationProvider;

 

    protected void configure(AuthenticationManagerBuilder auth) {

        auth.authenticationProvider(authenticationProvider);

    }

   

    @Bean

    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception  {

        http

               // test.html 보면 알겠지만 POST 처리를 통해 값을 '/tc/postTest.do' 값을 넘긴다.

               // POST 처리 CSRF(Cross-Site Request Forgery) 사용되어야 하는데,

               // csrf.ignoringRequestMatchers 경로들은 CSRF토큰을 설정하지 않아 SecurityConfig에서 임의로 CSRF 요청을 무시한다는 내용.

               .csrf(csrf -> csrf.ignoringRequestMatchers(

                new AntPathRequestMatcher("/tc/postTest.do")    

            ))

            .authorizeHttpRequests((authz) -> authz

                    .requestMatchers(

                            new AntPathRequestMatcher("/"),

                            new AntPathRequestMatcher("/tc/**")             // '/tc' 시작하는 모든 경로들을 의미

                    ).permitAll()    // permitAll() 메서드를 사용하여, requestMatchers안에 있는 경로들은 인증 없이 접속할 있다.

                    .anyRequest().authenticated())   // 외의 요청(=경로) 인증이 필요하게 된다.

            .httpBasic(withDefaults());

        return http.build();

    }

   

   

 

}

 

9. com.eugeneprogram 패키지 안에 controller 패키지 생성 후 MainController 클래스 생성

그림10.png

 

 

package com.eugeneprogram.controller;

 

import java.util.List;

import java.util.Map;

 

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.PostMapping;

import org.springframework.web.bind.annotation.RestController;

 

import jakarta.servlet.http.HttpServletRequest;

import jakarta.servlet.http.HttpServletResponse;

 

import com.eugeneprogram.service.TestService;

 

@RestController

public class MainController {

       @Autowired

    TestService testService;

      

       @GetMapping(value="/tc/selectTC.do")

       public List<Map<String, Object>> getTc() throws Exception {

               return testService.getAllList();

       }

      

       //서버 사이드에서 요청을 포워드

       @GetMapping("/tc/testPage.do")

       public void getTestPage(HttpServletRequest request, HttpServletResponse response) throws Exception {

        request.getRequestDispatcher("/tc/test.html").forward(request, response);

    }

      

       @PostMapping("/tc/postTest.do")

    public void handlePostRequest(String name, String id, String pw, String phone, HttpServletRequest request, HttpServletResponse response) throws Exception {      

        System.out.print(name);

       

        testService.insertTest(name, id, pw, phone);

       

        //Post 처리하는데 getRequestDispatcher get요청이므로 오류 발생, 따라서 다른 컨트롤러의 url경로로 sendRedirect처리를 한다.

        response.sendRedirect("/tc/tcList.do");

    }

      

       @GetMapping("/tc/tcList.do")

       public void getTcList(HttpServletRequest request, HttpServletResponse response) throws Exception     {

              request.getRequestDispatcher("/tc/testDbList.html").forward(request, response);

    }

}

 

10. src/main/resources에 위치한 application.properties 수정                                

그림11.png

 

spring.application.name=TestWeb

 

spring.datasource.driver-class-name=org.mariadb.jdbc.Driver

spring.thymeleaf.prefix=classpath:/static/

spring.thymeleaf.suffix=.html

spring.thymeleaf.mode=HTML

spring.thymeleaf.encoding=UTF-8

 

spring.datasource.url=jdbc:mariadb://127.0.0.1:3306/institute

spring.datasource.username=root

spring.datasource.password=1234

 

#디버그 활용 시 사용할 것

#logging.level.org.springframework.security=DEBUG

#logging.level.org.springframework.web=DEBUG

 

[ “institute” DB tc 테이블 구성 ]

tc_id(pk)

tc_name

tc_mail

tc_pw

tc_phone

5

kim

aaa@naver.com

*89C6B530AA78695E257E55D63C00A6EC9AD3E977

password(“1111”)

010-1111-1111

6

park

bbb@naver.com

*D142A988197D6E8B1D3D0945283450811637B73F

password(“2222”)

010-2222-2222

7

heo

ccc@naver.com

*0C794B34A1890E1AC777079465E38D1376F8FC24

password(“3333”)

010-3333-3333

11. src/main/webapp 위치에 tc 폴더를 생성한 후 test.html 파일 생성

그림12.png

 

<!DOCTYPE html>

<html lang="ko">

<head>

    <meta charset="UTF-8">

    <title>Test Form</title>

</head>

<body>

    <form action="/tc/postTest.do" method="post">

        Name : <input type="text" id="name" name="name"><br>

        Phone : <input type="text" name="phone"><br>

        ID : <input type="text" name="id"><br>

        PW : <input type="password" name="pw"><br>

        <button type="submit">Submit</button>

    </form>

 

</body>

</html>

 

12. src/main/webapp/tc 위치에 testDbList.html 파일 생성

그림13.png

 

<!DOCTYPE html>

<html>

<head>

<meta charset="UTF-8">

<title>Insert title here</title>

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>

</head>

<body>

<script>

// $.ajax 메소드를 사용하여 /tc/selectTC.do 경로에 GET 요청을 보낸다.

// 성공 처리: 요청이 성공하면, 응답(response) 배열인지 확인합니다. 배열이 존재하고 요소가 있을 경우, 요소(item) 대해 반복한다.

// 항목의 tc_id, tc_name, tc_phone, tc_pw, tc_mail 값을 <p> 태그로 출력한다.

// 데이터가 없을 경우: 배열이 비어있으면 "No data found"라는 메시지를 표시한다.

// 오류 처리: 요청 오류가 발생하면 경고창을 띄우고 오류 정보를 콘솔에 기록합니다.

    function codeList() {

        $.ajax({

            type: "GET",

            url: "/tc/selectTC.do",

            success: function(response) {

                $("#div1").empty();

                if (Array.isArray(response) && response.length > 0) {

                    response.forEach(function(item) {

                        $("#div1").append(

                            "<p>ID: " + item.tc_id +

                            ", Name: " + item.tc_name +

                            ", Phone: " + item.tc_phone +

                            ", PW: " + item.tc_pw +

                            ", Mail: " + item.tc_mail +

                            "</p>"

                        );

                    });

                } else {

                    $("#div1").text("No data found");

                }

            },

            error: function(xhr, status, error) {

                alert("error:: " + xhr.responseText);

                console.error("Error:: ", xhr, status, error);

            }

        });

    }

</script>

<!-- 'tc조회' 버튼 누를 codeList 실행 -->

<button onclick="codeList()">tc 조회</button> <br>

<div id="div1"></div>

<br><br>

<button onclick="location.href='/tc/testPage.do'">뒤로가기</button>

</body>

 

</html>

 

 

13. Boot Dashboard에서 프로젝트 우클릭하고 (Re)start 클릭하여 실행하기

그림14.png

 

그림15.png

 

그림16.png
→ 프로젝트에 에러가 없다면 위와 같이 console 창에 출력된다.

 

14. Web(= 크롬)에서 localhost:8080/tc/testPage.do 입력하고 엔터

그림17.png

 

그림18.png

 

→ 위에서 만든 /tc/testPage.do 경로에 의해 test.html 출력된다.

 

그림19.png

Name son, Phone 010-9999-9999, IDzzz@naver.com, PW 9999 입력 후 Submit 클릭

 

그림20.png

 

 tc 조회 클릭 시

 

그림21.png

→ 위와 같이 tc 테이블에 있는 정보를 모두 조회하는 기능을 한다. “뒤로가기는 아까 입력했던 페이지로 이동.

son ID값이 8이 아닌 이유는 tc_id primary key인데 heo 다음 오류 값이 있어 지우고 다시 son 값을 넣은 것이니 순서가 다를 수 있다. 다른 값도 마찬가지로 tc_id 순서가 다를 수 있다. 오류가 아니니 상관없다.