Spring Security

devkwon·2024년 1월 13일
post-thumbnail

Spring Security 추가

Spring Security Dependency를 추가하면 스프링 시큐리티를 사용할 수 있다.

추가하게 되면 어플리케이션 실행 시 콘솔 창에 초기 비밀번호가 나오고

이제 모든 페이지를 접속할 때 로그인이 필요해진다.

id : user, password: 초기 비밀번호를 입력하면 로그인이 된다.

SecurityConfig

스프링 시큐리티 6.1부터 많은 것들이 바뀌었다.

package com.cos.security1.config;

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.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity //스프링 시큐리티 필터(SecurityConfig)가 스프링 필터체인에 등록됨.
public class SecurityConfig {
    // 스프링 부트 2.7.0 이상부턴 WebSecurityConfigurerAdapter가 deprecated됨.
    // 아예 자체적으로 SecurityFilterChain Bean을 생성하는 방식으로 바뀌었다.
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                .csrf(AbstractHttpConfigurer::disable)

                // 특정 URL에 대한 권한 설정.
                .authorizeHttpRequests((authorizeRequests) -> {
                    authorizeRequests.requestMatchers("/user/**").authenticated(); //antMatcher -> requestMatchers로 변경

                    authorizeRequests.requestMatchers("/manager/**")
                            // ROLE_은 붙이면 안 된다. hasAnyRole()을 사용할 때 자동으로 ROLE_이 붙기 때문이다.
                            .hasAnyRole("ADMIN", "MANAGER");

                    authorizeRequests.requestMatchers("/admin/**")
                            // ROLE_은 붙이면 안 된다. hasRole()을 사용할 때 자동으로 ROLE_이 붙기 때문이다.
                            .hasRole("ADMIN");

                    authorizeRequests.anyRequest().permitAll();
                })

                .formLogin((formLogin) -> {
                    /* 권한이 필요한 요청은 해당 url로 리다이렉트 */
                    formLogin.loginPage("/loginForm");
                })

                .build();
    }
}

우선, 기존의 HttpSecurity 의 무분별한 메서드 체이닝을 지양하도록 바뀌었다.

    // 기존의 메서드 체이닝 방식
	.csrf().disable()

    .sessionManagement().sessionCreationPolicy(...)

    .and()
    .authorizeHttpRequests()
    // ...
    

코드를 보면 알겠지만, 기존에는 전혀 관계가 없는 요소들 끼리도 서로 직렬로 연결해서 사용했다.

    // 수정된 메서드 체이닝 방식
	.csrf(이곳에 CSRF 설정을 위한 함수)

    .sessionManagement(이곳에 세션 설정을 위한 함수)

    .authorizeHttpRequests(이곳에 인가 설정을 위한 함수);

지금은 함수형으로 하나의 요소에 대한 건 하나의 메서드 안에서 처리하도록 바뀌었다.

이런 식으로 코딩하지 않으면 IDE에서 에러를 뱉어내므로 주의.

회원가입

  • DB 구조

  • IndexController.java 회원가입 메소드 (평문)

@PostMapping("/join")
    public @ResponseBody String join(User user) {
        System.out.println(user);

        user.setRole("ROLE_USER"); //역할 지정
        userRepository.save(user); 

    }

다음과 같이 DB에 저장하여 회원가입을 할 수 있다.

허나 이는 문제가 생긴다. 스프링 시큐리티에선 암호화된 비밀번호만 인식할 수 있는데.
단순한 평문이기 때문에 DB 저장만 될뿐 로그인이 불가능하다.

이럴 때는 SecurityConfig에서 BCryptPasswordEncoder를 통해 암호화를 시켜 저장해야한다.

  • SecurityConfig.java
	//해당 메소드의 리턴되는 오브젝트를 Ioc로 등록
    @Bean
    public BCryptPasswordEncoder encodePwd(){
        return new BCryptPasswordEncoder();
    }
  • IndexController.java 회원가입 메소드 (암호화)
@PostMapping("/join")
    public @ResponseBody String join(User user) {
        user.setRole("ROLE_USER");
        String rawPassword = user.getPassword();
        String encPassword = bCryptPasswordEncoder.encode(rawPassword); //bCrypt로 암호화
        user.setPassword(encPassword);
        userRepository.save(user);

        return "redirect:/loginForm";
    }

로그인

로그인을 만들기 앞서 기존에 있던 SecurityConfig.java에 새로운 구문을 추가한다.

.formLogin((formLogin) -> {
                    /* 권한이 필요한 요청은 해당 url로 리다이렉트 */
                    formLogin.loginPage("/loginForm");
                    
                    //새로운 부분
                    formLogin.loginProcessingUrl("/login"); //login 주소가 호출되면 시큐리티가 낚아채서 로그인을 진행함
                    formLogin.defaultSuccessUrl("/"); //login 성공시 이동할 페이지, 만약 이전에 요청한 페이지가 있다면 해당 페이지로 감
                })

인증(authenticated)이 되면 시큐리티는 SecurityContextHolder에 사용자 정보를 저장한다. SecurityContextHolder 안에 있는 SecurityContext는 Authentication 객체를 가지고 있고, 그 안에는 유저 정보를 담고 있는 UserDetails타입의 Principal객체가 있다.

PrincipalDetails

package com.cos.security1.config.auth;

import com.cos.security1.model.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

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

public class PrincipalDetails implements UserDetails {

    private User user; // Composition

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

    // 해당 User의 권한을 return 함
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collect = new ArrayList<>();
        collect.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return user.getRole();
            }
        });
        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;
    }

    //휴면계정 로직 처리
    @Override
    public boolean isEnabled() {
        return true;
    }
}

PrincipalDetailsService

앞서 시큐리티 설정에서 loginProcessingUrl("/login")을 통해 /login으로 요청이 오면 자동으로 UserDetailsService 타입으로 등록되어 있는 loadUserByUsername 메소드가 실행된다.

package com.cos.security1.config.auth;

import com.cos.security1.model.User;
import com.cos.security1.repository.UserRepository;
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;

@Service
public class PrincipalDetailsService implements UserDetailsService {
    @Autowired
    private UserRepository userRepository;

    // 리턴이 되면  Authentication 내부에 들어가고 그 후에 SecurityContext에 들어감
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User userEntity = userRepository.findByUsername(username);
        if(userEntity != null){
            return new PrincipalDetails(userEntity);
        }
        return null;
    }
}

만약 해당하는 아이디가 존재해서 loadUserByUsername 함수에서 return 되는 UserDetails가 있을경우 이를 Authentication 내부에 넣고 해당 Authentication을 다시 세션에 넣음.

권한설정

SecurityConfig.java에 새로운 어노테이션을 추가한다.

@EnableMethodSecurity(securedEnabled = true, prePostEnabled = true)

해당 어노테이션의 securedEnable은 시큐어 어노테이션을 활성화하는 것으로, 이렇게 설정을 하게 되면

    @Secured("ROLE_ADMIN")
    @GetMapping("/info")
    public @ResponseBody String info(){
        return "개인정보";
    }

다음과 같이 Secured 어노테이션으로 접근이 가능한 권한을 설정할 수 있다.

다음으로 prePostenabled는 preAuthroize와 postAuthroize 어노테이션을 활성화한다.
이렇게 설정하게 되면

    @PreAuthorize("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')")
    @GetMapping("/data")
    public @ResponseBody String data(){
        return "데이터정보";
    }

다음과 같이 PreAuthorize 어노테이션을 쓸 수 있다.

Secured 어노테이션은 권한을 1개만 지정 가능하고, PreAuthroize는 여러개를 지정할 수 있다.

0개의 댓글