Spring Security (2)

ysh·2023년 8월 2일
0

Spring Boot

목록 보기
47/53

구현 시 구성 순서

1. config 설정

PasswordConfig

패스워드 암호화 방식 설정

package com.example.my.config.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class PasswordConfig {

    @Bean
    // 함수를 IoC 컨테이너로 넘겨줌
    // 클래스에는 @Component
    // 다른 곳에서
    // @Autowired
    // PasswordEncoder passwordEncoder시 자동으로 PasswordEncoder 타입의 클래스를 주입해줌

    // passwordEncoder 를 구현한 impl클래스들 중 어떤 클래스를 사용할 지 설정
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

SecurityConfig

로그인 / 로그아웃, 접근 세팅

package com.example.my.config.security;

import lombok.RequiredArgsConstructor;
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.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final AuthenticationSuccessHandler authenticationSuccessHandler;
    private final AuthenticationFailureHandler authenticationFailureHandler;
    private final LogoutSuccessHandler logoutSuccessHandler;

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

        httpSecurity.csrf(config -> config.disable());

//        httpSecurity.headers(config -> config
//                .frameOptions(frameOptionsConfig -> frameOptionsConfig
//                        .disable()
//                )
//        );
//
//        httpSecurity.authorizeHttpRequests(config -> config
//                .requestMatchers(PathRequest.toH2Console())
//                .permitAll()
//        );

        // 정적 파일들의 접근 세팅
        httpSecurity.authorizeHttpRequests(config -> config
                .requestMatchers("/css/**", "/js/**", "/assets/**", "/favicon.ico")
                .permitAll()
                // admin*.js 는 admin이 들어간 js파일
                .requestMatchers("/js/admin*.js", "/h2/**", "/temp/**")
                .hasRole("ADMIN")
        );


        httpSecurity.authorizeHttpRequests(config -> config
                // 이 요청들은
                .requestMatchers("/auth/**", "/api/*/auth/**")
                // 모두 허용
                .permitAll()
                // 이 요청들은
                .requestMatchers("/admin/**", "/api/*/admin/**")
                // "ADMIN" 이라는 Role을 가진 사용자만 허용
                .hasRole("ADMIN")
                // 다른 모든 요청들은
                .anyRequest()
                // 인증된 사람(로그인 된 사용자)만 접근 허용
                .authenticated()
        );

        httpSecurity.formLogin(config -> config
                // 로그인 페이지 매핑
                .loginPage("/auth/login")
                // 로그인 기능(API) 매핑
                .loginProcessingUrl("/api/v1/auth/login")
                // Security 기본 id 변수명 /  form안의 input태그들의 name속성으로 접근
                //<input type="text" id="id" ***이 속성*** name="id" class="form-control" aria-describedby="idAddOn" />
                .usernameParameter("id")
                // Security 기본 pw 변수명
                .passwordParameter("password")
                // 처리 성공 시 유저에게 보낼 내용
                .successHandler(authenticationSuccessHandler)
                // 처리 실패 시 유저에게 보낼 내용
                .failureHandler(authenticationFailureHandler)
                // 모든 유저 접근 허용
                .permitAll()
        );

        httpSecurity.logout(config -> config
                // 로그아웃 페이지 매핑
                .logoutUrl("/auth/logout")
                // 세션 초기화(?)
                .invalidateHttpSession(true)
                // 쿠키 삭제
                .deleteCookies("JSESSIONID")
                // 로그아웃 성공 시 유저에게 보낼 내용
                .logoutSuccessHandler(logoutSuccessHandler)
                // 모든 유저 접근 허용
                .permitAll()
        );

        return httpSecurity.getOrBuild();
    }
}

Handler

Config에서 로그인 성공 / 실패 시 유저에게 보낼 내용을 지정

CustomAuthenticationSuccessHandler

로그인 성공 시 핸들러

package com.example.my.config.security.auth;

import com.example.my.common.dto.ResponseDTO;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
@RequiredArgsConstructor
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    private final ObjectMapper objectMapper;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
        AuthenticationSuccessHandler.super.onAuthenticationSuccess(request, response, chain, authentication);

        response.setStatus(HttpServletResponse.SC_OK);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().print(objectMapper.writeValueAsString(
                ResponseDTO.builder()
                        .code(0)
                        .message("로그인에 성공하였습니다.")
                        .build()
        ));
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

        response.setStatus(HttpServletResponse.SC_OK);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().print(objectMapper.writeValueAsString(
                ResponseDTO.builder()
                        .code(0)
                        .message("로그인에 성공하였습니다.")
                        .build()
        ));
    }
}

CustomAuthenticationFailureHandler

로그인 실패 시 핸들러

package com.example.my.config.security.auth;

import com.example.my.common.dto.ResponseDTO;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
@RequiredArgsConstructor
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {

    private final ObjectMapper objectMapper;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {

        response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().print(objectMapper.writeValueAsString(
                ResponseDTO.builder()
                        .code(1)
                        .message("아이디와 비밀번호를 정확히 입력해주세요.")
                        .build()
        ));
    }
}

CustomLogoutSuccessHandler

로그아웃 성공 시

package com.example.my.config.security.auth;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
@RequiredArgsConstructor
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.sendRedirect("/auth/login");
    }
}

2. UserDetails

유저의 정보를 저장할 UserDeatils 객체 생성

package com.example.my.config.security.auth;

import com.example.my.common.dto.LoginUserDTO;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

@AllArgsConstructor
@Getter
public class CustomUserDetails implements UserDetails {

    private LoginUserDTO loginUserDTO;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return loginUserDTO.getUser().getRoleList()
                .stream()
                .map(role -> (GrantedAuthority) () -> "ROLE_" + role)
                .toList();
    }

    @Override
    public String getPassword() {
        return loginUserDTO.getUser().getPassword();
    }

    @Override
    public String getUsername() {
        return loginUserDTO.getUser().getId();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

3. UserDetailsService

Repository에서 유저 정보를 받아와 UserDetails 유형으로 변환 후 반환

package com.example.my.config.security.auth;

import com.example.my.common.dto.LoginUserDTO;
import com.example.my.model.user.entity.UserEntity;
import com.example.my.model.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
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.Optional;

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        Optional<UserEntity> userEntityOptional = userRepository.findByIdAndDeleteDateIsNull(username);
        if (userEntityOptional.isEmpty()) {
            throw new UsernameNotFoundException("아이디를 정확히 입력해주세요.");
        }
        return new CustomUserDetails(LoginUserDTO.of(userEntityOptional.get()));
    }
}

4. Controller

설정을 토대로, Controller의 매개변수로 UserDetails를 받아와 Security에서 처리

package com.example.my.domain.todo.controller;

import com.example.my.common.dto.LoginUserDTO;
import com.example.my.common.exception.InvalidSessionException;
import com.example.my.config.security.auth.CustomUserDetails;
import com.example.my.domain.todo.dto.ReqTodoTableInsertDTO;
import com.example.my.domain.todo.dto.ReqTodoTableUpdateDoneYnDTO;
import com.example.my.domain.todo.service.TodoServiceApiV1;
import jakarta.servlet.http.HttpSession;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/todo")
public class TodoControllerApiV1 {

    private final TodoServiceApiV1 todoServiceApiV1;

    @GetMapping
    public ResponseEntity<?> getTodoTableData(@AuthenticationPrincipal CustomUserDetails customUserDetails) {
        return todoServiceApiV1.getTodoTableData(customUserDetails.getLoginUserDTO());
    }

    @PostMapping
    public ResponseEntity<?> insertTodoTableData(
            @Valid @RequestBody ReqTodoTableInsertDTO dto,
            // 유저 정보 받아옴
            @AuthenticationPrincipal CustomUserDetails customUserDetails
    ) {
        return todoServiceApiV1.insertTodoTableData(dto, customUserDetails.getLoginUserDTO());
    }

    @PutMapping("/{todoIdx}")
    public ResponseEntity<?> updateTodoTableData(
            @PathVariable Long todoIdx,
            @Valid @RequestBody ReqTodoTableUpdateDoneYnDTO dto,
            @AuthenticationPrincipal CustomUserDetails customUserDetails
    ) {
        return todoServiceApiV1.updateTodoTableData(todoIdx, dto, customUserDetails.getLoginUserDTO());
    }

    @DeleteMapping("/{todoIdx}")
    public ResponseEntity<?> deleteTodoTableData(
            @PathVariable Long todoIdx,
            @AuthenticationPrincipal CustomUserDetails customUserDetails
    ) {
        return todoServiceApiV1.deleteTodoTableData(todoIdx, customUserDetails.getLoginUserDTO());
    }
}
profile
유승한

0개의 댓글