Notice
Recent Posts
Recent Comments
Link
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

임도현의 성장

Spring-Boot Spring Security 스프링 시큐리티 본문

Spring Boot

Spring-Boot Spring Security 스프링 시큐리티

림도현 2024. 10. 28. 22:25
Spring Security 란?
Spring Security는 인증, 인가 그리고 데이터 보호 기능을 포함하여 웹 개발 과정에서 필수적인 사용자 관리 기능을 구현하는데 도움을 주는 스프링 하위 프레임워크입니다. 스프링 시큐리티는 필터 기반으로 동작한다. 

🤗 인증과 인가

  • 인증(Authentication)은 사용자의 신원을 입증하는 과정입니다. 예를 들어 사용자가 사이트에 로그인을 할 때 누구인지 확인하는 과정을 인증이라고 합니다.
  • 인가(Authorization)는 인증과는 다릅니다. 인가는 사이트의 특정 부분에 접근할 수 있는지 권한을 확인하는 작업입니다.

build.gradle  추가

https://mvnrepository.com/   <== 빌드 종합 세트

// Security
implementation 'org.springframework.boot:spring-boot-starter-security'

🚀 실행

  1. 처음에 바로 실행을 하게되면 밑에 이미지 처럼 로그인페이지가 나온다.
  2.  이유는 특정한 경로에 요청이 오면 Controller 클래스에 도달하기 전 필터에서 Spring Security가 검증을 함
  3. 스프링 시큐리티는 필터 기반이므로 요청이오면 바로 반응을 한다.
  4. id는 user이고 password는 터미널에 나왔있다.
  5. Spring Security에서 웹사이트 보안에 가장 기본적인 기능인 아이디/패스워드 인증을 화면까지 지원합니다.

 

🌇 SecurityConfig 인가 작업 

  1. @Configuration은 이 파일이 스프링의 환경 설정 파일임을 의미하는 애너테이션이다.
  2. @EnableWebSecurity는 모든 요청 URL이 스프링 시큐리티의 제어를 받도록 만드는 애너테이션이다.
  3. SecurityFilterChain 클래스가 동작하여 모든 요청 URL에 이 클래스가 필터로 적용되어 URL별로 특별한 설정을 할 수 있게 된다. 
  4. 밑에 설명을 보면 permitAll()을 사용하여 "/" 경로 는 접근을 허용시켜 이제 로그인 페이지가 안뜨게 설정
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{

        http
                .authorizeHttpRequests((auth) -> auth
                        .requestMatchers("/", "/login", "/join", "/joinProc", "/loginProc").permitAll()
                        .requestMatchers("/admin").hasRole("ADMIN")
                        .requestMatchers("/my/**").hasAnyRole("ADMIN", "USER")
                        .anyRequest().authenticated()
                );

        return http.build();
    }
}
  • authorizeHttpRequests :  각 URL 요청에 대한 접근 권한을 설정합니다.
  • requestMatchers :  통해 특정 URL에 대해 접근을 허용할지, 특정 역할이 필요할지 정의합니다.
  • permitAll() :  해당 URL은 모두 접근할 수 있게 허용합니다.
  • hasRole("ADMIN") :  /admin 경로에 접근하려면 ADMIN 역할이 필요하다는 의미입니다.
  • authenticated() : 나머지 모든 요청은 인증이 필요하다는 설정입니다.

🚫연결 거부

permitAll()을 사용하여 접근을 허용했지만 "/", "/login", "join" 등등 말고는 다른 경로로 들어가면 밑에 사진 처럼  연결을 거부해버린다.

requestMatchers("/", "/login", "/join", "/joinProc", "/loginProc").permitAll()

🥳 커스텀 로그인 설정

SecurityConfig 안에 filterChain매서드 안에 추가해주면 된다. 
  • formLogin : 커스텀 로그인 페이지를 설정할 때 사용합니다.
  • loginPage("/login") : 로그인 페이지로 사용할 URL을 지정합니다. 즉 접근 제한 된 주소로 들어가면 로그인 페이지로 리다이렉트 합니다.
  • loginProcessingUrl("/loginProc") :  실제 로그인 처리를 할 URL입니다.
  • permitAll() :  로그인 페이지와 로그인 처리는 모두 접근할 수 있게 허용합니다
        http
                .formLogin((auth) -> auth.loginPage("/login")
                        .loginProcessingUrl("/loginProc")
                        .permitAll()
                );

👹CSRF 설정

CSRF 방어 기능을 비활성화합니다. 개발 및 테스트 중에는 필요하지 않을 수 있지만, 운영 환경에서는 보안상 활성화하는 것이 좋습니다. 

        http
                .csrf((auth) -> auth.disable()
                );

🎅 회원가입 구현 및 비밀번호 암호화

SecurityConfig 안에 빈 등록 해주면 된다.
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
@Data
public class JoinDTO { // 로그인한 아이디와 비밀번호를 받아오는 JoinDTO
    private String username;
    private String password;
}

@Entity(name = "USERENTITY")
@Data
public class UserEntity { // 데이터 베이스에 넣어줄 Entity

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @Column(unique = true)
    private String username;
    private String password;

    private String role; // 권한 지정 값

}

👻Service 구현

bCryptPasswordEncoder.encode(joinDTO.getPassword()) 평문을 암호문으로 바꿔주는 코드

@Slf4j
@Service
public class JoinService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    public void joinProcess(JoinDTO joinDTO){

        //db에 이미 동일한 username을 가진 회원이 존재하는지?
        boolean isUser = userRepository.existsByUsername(joinDTO.getUsername());
        if (isUser) {
            return;
        }

        UserEntity data = new UserEntity();
        data.setUsername(joinDTO.getUsername());
        // 암호화
        data.setPassword(bCryptPasswordEncoder.encode(joinDTO.getPassword()));
        data.setRole("ROLE_ADMIN");

        log.info("data = {}", data);

        userRepository.save(data);
    }

👨‍💻Repository JPA로 구현

public interface UserRepository extends JpaRepository<UserEntity, Integer> {
	// 아이디 증복 검즘
    boolean existsByUsername(String username);
	// 유저 찾기
    UserEntity findByUsername(String username);
}

🐧 JPA 자주 사용하는 쿼리 메서드 명 규칙

Distinct findDistinctByLastnameAndFirstname select distinct …​ where x.lastname = ?1 and x.firstname = ?2
And findByLastnameAndFirstname … where x.lastname = ?1 and x.firstname = ?2
Or findByLastnameOrFirstname … where x.lastname = ?1 or x.firstname = ?2
Is, Equals findByFirstname,findByFirstnameIs,findByFirstnameEquals … where x.firstname = ?1
Between findByStartDateBetween … where x.startDate between ?1 and ?2
LessThan findByAgeLessThan … where x.age < ?1
LessThanEqual findByAgeLessThanEqual … where x.age <= ?1
GreaterThan findByAgeGreaterThan … where x.age > ?1
GreaterThanEqual findByAgeGreaterThanEqual … where x.age >= ?1
After findByStartDateAfter … where x.startDate > ?1
Before findByStartDateBefore … where x.startDate < ?1
IsNull, Null findByAge(Is)Null … where x.age is null
IsNotNull, NotNull findByAge(Is)NotNull … where x.age not null
Like findByFirstnameLike … where x.firstname like ?1
NotLike findByFirstnameNotLike … where x.firstname not like ?1
StartingWith findByFirstnameStartingWith … where x.firstname like ?1 (parameter bound with appended %)
EndingWith findByFirstnameEndingWith … where x.firstname like ?1 (parameter bound with prepended %)
Containing findByFirstnameContaining … where x.firstname like ?1 (parameter bound wrapped in %)
OrderBy findByAgeOrderByLastnameDesc … where x.age = ?1 order by x.lastname desc
Not findByLastnameNot … where x.lastname <> ?1
In findByAgeIn(Collection<Age> ages) … where x.age in ?1
NotIn findByAgeNotIn(Collection<Age> ages) … where x.age not in ?1
True findByActiveTrue() … where x.active = true
False findByActiveFalse() … where x.active = false
IgnoreCase findByFirstnameIgnoreCase … where UPPER(x.firstname) = UPPER(?1)

🐦 사용자의 정보를 가져오는 UserDetailsService인터페이스 구현

  • 스프링 시큐리티에서 로그인을 진행할 때 사용자 정보를 가져와 findByUsername으로 확인을 한다.
@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity userData = userRepository.findByUsername(username);

        if (userData != null) { // userData 정보를 보내 줌
            return new CustomUserDetails(userData);
        }

        return null;
    }
}

🍟사용자 정보 설정

 

  • 사용자 정보 설정
    • CustomUserDetails 생성자를 통해 UserEntity 객체를 받아오고, 이 정보를 기반으로 사용자 인증에 필요한 데이터를 반환합니다.
  • 권한 설정 (getAuthorities)
    • 이 메서드는 사용자의 권한 정보를 제공합니다. Spring Security는 인증 후 권한에 따라 사용자가 접근할 수 있는 URL을 제한하거나 허용합니다.
    • 여기서는 userEntity.getRole()에서 가져온 권한 문자열을 GrantedAuthority로 변환해 권한 리스트로 반환합니다.
  • 기본 사용자 정보 제공
    • getPassword()와 getUsername() 메서드를 통해 사용자의 비밀번호사용자 이름 정보를 Spring Security에 제공합니다.
    • 이 정보는 로그인 시 PasswordEncoder를 통해 입력된 평문 비밀번호와 암호화된 DB의 비밀번호를 비교하는 데 사용됩니다.

 

public class CustomUserDetails implements UserDetails {

    //UserEntity 객체를 받아오기
    private UserEntity userEntity;

    public CustomUserDetails(UserEntity userEntity){
        this.userEntity = userEntity;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collection = new ArrayList<>();

        collection.add(new GrantedAuthority() {

            @Override  // 권한
            public String getAuthority() {
                // userEntity.getRole()에서 가져온 권한 문자열을 GrantedAuthority로 변환해 리턴
                return userEntity.getRole();
            }
        });

        return collection;
    }

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

    @Override
    public String getUsername() {
        return userEntity.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;
    }
}

🤮 내 순서 정리

  1. 회원가입 요청 
    • 클라이언트는 회원가입 폼에 정보를 입력하고, @PostMapping으로 서버로 전달
    • Service에서 증복 회원인지 DB에서 찾아봄  없는 회원이면 DB에 저장 이때 비밀번호는 암호문으로 저장 됨
    • 회원가입이 완료되면 클라이언트는 로그인 페이지로 리다이렉트됩니다.
  2. 로그인 요청
    • 클라이언트가 username과 password를 입력하고 제출 이 요청은 @PostMapping /loginProc으로 보내지만, 실제 처리는 Spring Security가 자동으로 로그인 요청을 가로채 진행합니다.
    • Spring Security는 /loginProc에 대한 POST 요청을 받으면 CustomUserDetailsService에서 DB를 통해 해당 username을 가진 사용자를 조회해서 UserEntity객체를 CustomUserDetails로 보내줍니다.
  3. 비밀번호 검증 
    • CustomUserDetails에 서 가져온 UserEntity객체를 꺼내 암호화된 비밀번호와 getPassword() 비밀번호를 비교하여 일치하면 인증에 성공