인생 디벨로퍼

Spring security + my batis 본문

JAVA Spring

Spring security + my batis

뫄뫙뫄 2024. 3. 11. 10:41
728x90

시큐리티, 스프링 업그레이드로 변경된 부분이 있어 정리해두려 함.

 

그래들 버전 디펜던시 dependencies

implementation "org.springframework.boot:spring-boot-starter-security"

지금 과정에서 테스트는 진행하지 않을 예정이라 추가하지 않음.

SecurityConfig 

package com.jycoding.login.config;

import jakarta.servlet.DispatcherType;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;


@EnableWebSecurity
@Configuration // Bean 을 정의
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) //secured 활성화(실행 후), preAutorize 활성화(실행 전)
public class SecurityConfig {



    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        // h2 콘솔 사용을 위한 설정
        http.csrf((csrf) -> csrf
                                .ignoringRequestMatchers(new AntPathRequestMatcher("/h2-console/**")))
                                .headers((headers) -> headers.addHeaderWriter(new XFrameOptionsHeaderWriter(
                                            XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN)));

        // disable 하지 않으면 post, delete 요청을 막아버림
        http.csrf((csrf-> csrf.disable()));


        http.authorizeHttpRequests((authorize) -> {
            authorize
                    .dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.ERROR).permitAll()
                    .requestMatchers("/user").hasAnyAuthority("USER")
                    .requestMatchers("/admin").hasAnyAuthority("ADMIN")
                    .requestMatchers("/**").permitAll()
                    .anyRequest().permitAll();
        });

        // 로그인 페이지는 loginForm 으로 설정, 기존 로그인 페이지가 아닌 직접 생성한 로그인 페이지로 보낸다.
        http.formLogin(form -> form.loginPage("/loginForm")
                .loginProcessingUrl("/login")
                .defaultSuccessUrl("/"));


        return http.build();
    }

    // 권한 계층 설정 (어드민 > 유저 -> 어드민이 유저의 모든 권한을 상속받는다)
    @Bean
    static RoleHierarchy roleHierarchy() {
        RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
        hierarchy.setHierarchy("ADMIN > USER\n");
        return hierarchy;
    }

    // 메소드 수준의 보안 실행 -> 메소드가 호출될때 보안 검사를 수행
    @Bean
    static MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) {
        DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
        expressionHandler.setRoleHierarchy(roleHierarchy);
        return expressionHandler;
    }

    // 비밀번호 인코딩
    @Bean
    BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

 

auth/PrincipalDetails

package com.jycoding.login.config.auth;

import com.jycoding.login.model.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

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

// Security Session => Authentication => UserDetails(PrincipalDetails)
@Slf4j
public class PrincipalDetails implements UserDetails {

    private User user;

    public PrincipalDetails(User user) {
        this.user = user;
    }

    // 해당 유저의 권한을 리턴하는 곳
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collect = new ArrayList<>();
        collect.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return user.getRole();
            }

            @Override
            public String toString() {
                return getAuthority();
            }

        });
        // [ROLE_ADMIN]
        log.info(collect.toString());
        return collect;
    }

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

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    //계정 만료 여부
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    // 계정 잠겼는지
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    // 자격증면이 만료 되었는지
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    // 계정 활성화 (비활시 false 반환)
    @Override
    public boolean isEnabled() {
        return true;
    }
}

 

auth/PrincipalDetailsService

package com.jycoding.login.config.auth;

import com.jycoding.login.model.User;
import com.jycoding.login.model.UserRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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;

// "/login" 요청이 오면 자동으로 UserDetailsService 타입을 IoC 되어 있는 loadUserByUsername 함수가 실행
@Service
@Slf4j
public class PrincipalDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    // 시큐리티 session 내부(Authentication 내부 (UserDetails))
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User userEntity = userRepository.findByUsername(username);

        //User(id=1, username=ssar, password=$2a$10$pszt9sAg3yXLmy1XxZEjkeXOwJY0hMqhifao3hgwnqZ9XVb/7p.cm, role=ROLE_ADMIN, email=쌀, createdAt=2024-03-08 15:28:20.104208)
        log.info(userEntity.toString());
        if (userEntity != null){
            return  new PrincipalDetails(userEntity);
        }
        return null;
    }
}

 

회원가입 컨트롤러에서 추가해야하는 코드

        joinReqDto.setRole("USER");

        String encPassword = bCryptPasswordEncoder.encode(joinReqDto.getPassword());
        joinReqDto.setPassword(encPassword);

        userService.회원가입(joinReqDto);

이때, role 에 "ROLE_" 접두어를 붙어야 한다, 아니다로 갈리는데,

직접 해본 결과 필요 없었다. 두가지 다 작동 잘 됨.

 

 이후 JWT 부분도 보완해서 업로드 예정.

728x90