23/05/15(스프링+시큐리티)

조영문·2023년 5월 15일
0

Spring

목록 보기
7/7
post-thumbnail

common 폴더

module 폴더

config 폴더

resources/ webapp 폴더

SecurityConfig.java

추가

package com.example.my.config.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {

        httpSecurity.csrf().disable();
        // h2를 볼려고
        httpSecurity.authorizeHttpRequests(config -> {
            try {
                config
                        .antMatchers("/h2/**")
                        .permitAll()
                        .and()
                        .headers().frameOptions().sameOrigin();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });

        // 시큐리티 기본 상태 - 인가 Authorization(인증 Authentication + 권한 Authority)
        httpSecurity.authorizeHttpRequests(config -> config
                // 패턴에 해당하는 주소는 허용
                .antMatchers("/auth/login","/auth/join","/api/*/auth/**")
                .permitAll()
                // 패턴에 해당하는 주소는 권한이 있어야만 들어갈 수 있음
                .antMatchers("/todoList")
                .hasRole("USER")
                // 모든 페이지를 인증하게 만듬
                .anyRequest()
                .authenticated());

        // formLogin과 관련된 내용
        httpSecurity.formLogin(config -> config
                // 우리가 직접 만든 로그인 페이지를 사용한다.
                .loginPage("/auth/login")
                // loginProc라고 생각하면 됨
                .loginProcessingUrl("/login-process")
                // 흔히 말하는 로그인 아이디를 시큐리티에서 username이라고 한다.
                // 우리가 해당하는 파라미터를 커스텀할 수 있다.
                .usernameParameter("id")
                // 비밀번호 파라미터도 커스텀가능하다.
                .passwordParameter("pw")
                // 로그인 실패 핸들러
                .failureHandler(new CustomAuthFailureHandler())
                // 로그인 성공 시 이동 페이지
                // 두번째 매개변수는 로그인 성공 시 항상 세팅 페이지로 이동하게 함
                .defaultSuccessUrl("/todoList", true));

        return httpSecurity.build();

    }

}

login.jsp

form 추가

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html lang="kr" id="loginPage">
<head>
    <meta charset="UTF-8"/>
    <meta
            name="viewport"
            content="width=device-width, initial-scale=1, shrink-to-fit=no"
    />
    <meta http-equiv="x-ua-compatible" content="ie=edge"/>

    <!-- 부트스트랩 링크 -->
    <link
            href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
            rel="stylesheet"
            integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"
            crossorigin="anonymous"
    />
    <script
            src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
            integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
            crossorigin="anonymous"
    ></script>

    <!-- Font Awesome -->
    <link
            rel="stylesheet"
            href="https://use.fontawesome.com/releases/v5.15.2/css/all.css"
    />

    <title>로그인</title>
</head>

<body>
<!-- Start your project here-->
<section style="background-color: #508bfc; min-height: 100vh">
    <div class="container py-5 h-100">
        <div class="row d-flex justify-content-center align-items-center h-100">
            <div class="col-12 col-md-8 col-lg-6 col-xl-5">
                <div class="card shadow-2-strong" style="border-radius: 1rem">
                    <div class="card-body p-5 text-center">
                        <h3 class="mb-3">
                            로그인
                        </h3>
                        <div class="input-group mb-3">
                  <span id="idAddOn" class="input-group-text">
                    &nbsp;아이디 &nbsp;
                  </span>
                            <input
                                    type="text"
                                    id="id"
                                    class="form-control"
                                    aria-describedby="idAddOn"
                            />
                        </div>

                        <div class="input-group mb-3">
                            <span id="pwAddOn" class="input-group-text">비밀번호</span>
                            <input
                                    type="password"
                                    id="pw"
                                    class="form-control"
                                    aria-describedby="pwAddOn"
                            />
                        </div>

                        <!-- Checkbox -->
                        <div class="form-check d-flex justify-content-start mb-4">
                            <input
                                    class="form-check-input"
                                    type="checkbox"
                                    id="rememberMe"
                            />
                            <label class="form-check-label" for="rememberMe">
                                아이디 기억하기
                            </label>
                        </div>

                        <button
                                class="btn btn-primary"
                                type="button"
                                style="width: 100%"
                                onclick="requestLogin()"
                        >
                            로그인
                        </button>

                        <hr class="my-4"/>

                        <a href="/auth/join">아이디가 없으신가요? 회원가입</a>
                    </div>
                </div>
            </div>
        </div>
    </div>
</section>
<!-- End your project here-->
</body>
<!-- Custom scripts -->
<script type="text/javascript">

    // 비밀번호 입력창에서 엔터키를 치면 로그인 요청하는 함수
    document.querySelector("#pw").addEventListener("keyup", (event) => {
        if (event.keyCode === 13) {
            requestLogin();
        }
    });

    // 로그인 요청하는 함수
    // 로그인 버튼을 누르면 실행됨
    const requestLogin = () => {
        // 서버와 통신하기 전에 입력값 검증
        if (!validateFields()) {
            return;
        }

        // id속성을 이용해서 태그를 가져옴
        const idElement = document.getElementById("id");
        const pwElement = document.getElementById("pw");
        const rememberMeElement = document.getElementById("rememberMe");

        if(rememberMeElement.checked){
            localStorage.setItem("rememberId", idElement.value);
        }

        // form 통신
        // const formData = new FormData();
        // formData.append("id",idElement.value);
        // formData.append("pw",pwElement.value);

        const formTag = document.createElement("form");
        formTag.action = "/login-process";
        formTag.method = "POST";

        // id
        const idInputTag = document.createElement("input");
        idInputTag.type = "hidden";
        idInputTag.name = "id";
        idInputTag.value = idElement.value;
        formTag.appendChild(idInputTag);
        // pw
        const pwInputTag = document.createElement("input");
        pwInputTag.type = "hidden";
        pwInputTag.name = "pw";
        pwInputTag.value = pwElement.value;
        formTag.appendChild(pwInputTag);

        document.body.appendChild(formTag);
        formTag.submit();

    };

    // 아이디와 비밀번호 입력창이 비어있는지 검사하는 함수
    const validateFields = () => {
        // id속성으로 요소를 가져옴
        const idElement = document.getElementById("id");
        const pwElement = document.getElementById("pw");

        if (idElement.value === "") {
            alert("아이디를 입력해주세요.");
            idElement.focus();
            return false;
        }

        if (pwElement.value === "") {
            alert("비밀번호를 입력해주세요.");
            pwElement.focus();
            return false;
        }

        return true;
    };

    // 페이지가 로드되면 실행되는 함수
    const setLoginPage = () => {
        // 창이 켜지면 아이디 입력창에 포커스가 가도록 설정
        const idElement = document.getElementById("id");
        idElement.focus();

        // 아이디 기억하기 체크박스가 체크되어있으면 아이디 입력창에 아이디를 넣어줌
        const rememberId = localStorage.getItem("rememberId");
        if (rememberId !== null) {
            const rememberMeElement = document.getElementById("rememberMe");

            idElement.value = rememberId;
            rememberMeElement.checked = true;
        }

        // 에러 메시지 확인하기
        const queryString = window.location.search;
        const urlParams = new URLSearchParams(queryString);
        const message = urlParams.get('message');
        if (message != null && message != ""){
            alert(message);
        }
    };
</script>
<script defer>
    setLoginPage();
</script>
</html>

CustomUserDetails.java

package com.example.my.config.security;

import com.example.my.module.user.entity.UserEntity;
import com.example.my.module.user.entity.UserRoleEntity;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

// 스프링 시큐리티에서 사용하는 유저 정보 저장소 => UserDetails
@RequiredArgsConstructor
@Getter
public class CustomUserDetails implements UserDetails {

    private final UserEntity userEntity;
    private final List<UserRoleEntity> userRoleEntityList;

    //유저가 가진 모든 권한 가져오기
    //유저는 여러가지 권한을 가질 수 있다.
    //카페 개설자 ADMIN
    //이용자 USER
    //댓글을 10번 적어서 새싹회원->열심회원
    //이용자 중에 스태프로 선정됨
    //해당 이용자는 USER, 열심회원, STAFF 셋의 권한을 모두 가짐
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        //1. userRoleEntityList에서 role(String)만 가져오기
        //2. role로 GrantedAuthority 객체 만들기
        //3. 리스트에 집어넣기
        Collection<GrantedAuthority> grantedAuthoritiesList = new ArrayList<>();
        for (UserRoleEntity userRoleEntity: userRoleEntityList) {
            grantedAuthoritiesList.add(new GrantedAuthority() {
                //익명 클래스
                //인터페이스가 구현해 달라고 한 것들만 구현하면 익명 클래스를 만들 수 있다.
                @Override
                public String getAuthority() {
                    //스프링 시큐리티에서는 권한 앞에 ROLE_ 를 붙인다.
                    return "ROLE_" + userRoleEntity.getRole();
                }
            });
        }
        //결과값 리턴
        return grantedAuthoritiesList;
    }

    // 비밀번호 가져오기
    @Override
    public String getPassword() {
        return userEntity.getPw();
    }
    // 아이디 가져오기
    @Override
    public String getUsername() {
        return userEntity.getId();
    }
    // 계정이 만료되지 않았는지 체크
    @Override
    public boolean isAccountNonExpired() {
        // 만료될 서비스가 아니라서 true
        return true;
    }
    // 계정이
    @Override
    public boolean isAccountNonLocked() {
            return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        //  세션 방식이라 true
        return true;
    }
    // 일시정지
    @Override
    public boolean isEnabled() {
        // 일시정지, 신고
        return true;
    }
}

CustomUserDetailsService.java

package com.example.my.config.security;

import com.example.my.module.user.entity.UserEntity;
import com.example.my.module.user.entity.UserRoleEntity;
import com.example.my.module.user.repository.UserRepository;
import com.example.my.module.user.repository.UserRoleRepository;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Primary;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.List;

// UserDatailsService가 기존에 있음
//  시큐리티 디펜던시 추가했을 때 로그인 돼었음 user라는 아이디로
// Service라고 붙이면 UserDetailsService 중에서 하나만 뜬다.
// Bean또는 Component는 interface 기준으로 하나만 떠야한다.
@Service
@Primary // 중복되는 component 중에서 1순위로 IOC컨테이너에 등록된다.
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    // DI 방법
    // 1. @Autowired
    // 2. setter
    // 3. 생성자
    private final UserRepository userRepository;
    private final UserRoleRepository userRoleRepository;

    // 로그인 단계
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // username으로 DB에 해당하는 userEntity가 있는지 확인
        // userEntity가 null이면 아이디를 잘못 친것이니 에러를 터트린다
        // userEntity가 null이 아니면
        // UserEntity와 UserRoleEntity로 UserDetails를 만들어서 리턴한다.

        UserEntity userEntity = userRepository.findById(username);
        if(userEntity == null) {
            throw new UsernameNotFoundException("아이디를 정확히 입력해주세요.");
        }
        List<UserRoleEntity> userRoleEntitiesList = userRoleRepository.findByUserIdx(userEntity.getIdx());

        //        if(userRoleEntityList.size() < 1){
//            throw new AuthenticationCredentialsNotFoundException("권한이 없습니다,");
//        }

        return new CustomUserDetails(userEntity,userRoleEntitiesList);
    }
}

CustomAuthFailureHandler.java

package com.example.my.config.security;


import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

// form로그인을 했기 때문에
// 페이지를 리턴해줘야한다.
// 그래서 SimpleUrlAuthenticationFailureHandler를 상속받았다.
public class CustomAuthFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    // 로그인 실패 시 처리하는 용도
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException exception) throws IOException, ServletException {
        String errorMessage;
        if (exception instanceof UsernameNotFoundException) {
            errorMessage = exception.getMessage();
        } else {
            errorMessage = "알 수 없는 에러가 발생했습니다. 관리자에게 문의하세요.";
        }

        String encodedErrorMessage = URLEncoder.encode(errorMessage, StandardCharsets.UTF_8);
        setDefaultFailureUrl("/auth/login?error=true&message=" + encodedErrorMessage);
        super.onAuthenticationFailure(request, response, exception);
    }
}

요점 정리(스프링 시큐리티)

  • 수많은 필터가 있다.
  • 그 중에 우리가 주로 쓰는 것은 Authentication
  • Authentication에서 주로 커스텀하는 것은 UserDetails / UserDetailsService
  • 컨트롤러에서 @AuthenticationPrinspal를 사용하면 UserDetails를 가져올 수 있다. (JSP의 Session 대용)
  • antMatchers는 경로를 지정한다.
  • permitAll은 필터 없이 해당 경로로 이동할 수 있게 한다.
  • hasRole은 해당 권한이 있는 사람만 이동할 수 있게 한다.

git

https://github.com/youngmoon97/Spring

0개의 댓글