project/personal project

[CRUD] 기능구현 - 로그인 기능 구현 (스프링 시큐리티)

박허디 2024. 1. 10. 17:00

 

관련 화면

 

 

 

 

 

관련 코드

 

1. Spring Security dependency 추가하기

 

build.gradle에 security와 관련된 의존성 추가

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

 

의존성 추가 후 프로그램을 작동시키면 스프링 시큐리티에서 제공하는 로그인 페이지를 볼 수 있다.

 

콘솔창을 보고 비밀번호 입력할 수 있다

  • Username : user
  • Password : 콘솔창 

 

이게 너무 귀찮으면 application.properties에서 기본 스프링 시큐리티 username이랑 password를 설정할 수 있다.

 

 

2. Security Config 작성

 

package kr.co.crud.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.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

/* SecurityConfig.java -> Security Filter Chain
 * (1) HTTP 요청에 대한 보안 작업
 * - UsernamePasswordAuthenticationFilter : 사용자가 입력한 인증 정보로 Authentication 객체 생성
 * - http.formLogin() 등 메서드를 통해 설정
 * (2) 사용자 인증(로그인 설정), 인가(접근 권한 설정), 로그아웃 설정
 * */
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {

        /* 인가(접근권한) 설정 */
        http.authorizeHttpRequests().antMatchers("/", "/user/login", "/user/register").permitAll();      // 경로에 대해서 모든 사용자에게 접근을 허용한다는 의미
        http.authorizeHttpRequests().antMatchers("/board/**").hasAnyRole("1", "2");

        /* 사이트 위변조 요청 방지
         * CRSF(Cross-Site Request Forgery) 공격 방지 기능을 비활성화
         * disable -> CRSF 토큰을 사용하지 않도록 설정
         * */
        //http.csrf().disable();

        /* 로그인 설정 */
        http.formLogin()
                .loginPage("/user/login")                                       // 로그인 페이지 경로 설정 (해당 경로를 통해 로그인 진행)
                .defaultSuccessUrl("/")                                         // 로그인 성공 시 이동 경로
                .failureUrl("/user/login?success=100")       // 로그인 실패 시 이동 경로
                .usernameParameter("uid")                                       // 로그인 폼에서 사용자 아이디를 입력받는 Input 필드 이름 지정 (name="uid")
                .passwordParameter("pass");                                     // 로그인 폼에서 사용자 비밀번호를 입력받는 Input 필드 이름 지정 (name="pass")

        /* 로그아웃 설정 */
        http.logout()
                .invalidateHttpSession(true)                                                // 로그아웃 시 세션 무효화
                .logoutRequestMatcher(new AntPathRequestMatcher("/user/logout"))          // 로그아웃 요청 URL (POST 요청 URL)
                .logoutSuccessUrl("/user/login?success=200");                               // 로그아웃 성공 시 이동 경로

        /* 로그인 시 세션 유지 */
        http.sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);

        //return http.build(); // build: SecurityConfigurerAdapter의 메서드 중 하나로, HttpSecurity 객체를 반환

    }

    /* 비밀번호 암호화
     * BCrypt 해시 함수를 사용하여 비밀번호를 암호화
     *  */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

 

사용자가 폼을 통해 로그인 시도를 할 때 해당 폼에 입력한 정보를 http requset 로 서버에 전송을 하고 security config에서 security  filterchain을 통해 http 요청에 대한 보안 작업을 하고 또한 사용자가 입력한 입력한 인증정보로 authentication 객체를 생성한다.

 

3. MyUserDetails 작성
package kr.co.crud.security;

import kr.co.crud.entity.UserEntity;
import lombok.Builder;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

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

/*
 * AuthenticationManager => UserDetails
 * - 생성된 Authentication 객체를 검증 및 인증을 수행
 * */
@Data
@Builder
public class MyUserDetails implements UserDetails {

    /*
     * 직렬화(Serializable)
     * - 자바의 객체를 바이트의 배열로 변환하여 DB에 저장
     * */
    private static final long serialVersionUID = 1L;

    @Autowired
    private UserEntity user;

    /*
     * getAuthorities()
     * - 사용자가 가지고 있는 권한을 반환하는 메서드
     * - 반환타입: Collection<? extends GrantedAuthority>
     * */
    // <? extends GrantedAuthority>: 와일드카드('?')를 사용하여 GrantedAuthority가 확장한 타입이 될 수 있음을 명시
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // GrantedAuthority 객체들을 저장하기 위한 리스트 생성
        List<GrantedAuthority> authorities = new ArrayList<>();

        // 사용자의 권한을 나타내는 SimpleGrantedAuthority 객체를 생성하여 리스트에 추가
        // 일반적으로 Spring Security의 규약에 따라 "ROLE_" 접두어를 붙여 권한 생성
        authorities.add(new SimpleGrantedAuthority("ROLE_"+user.getGrade()));

        return authorities;
    }

    /*
     * getUsername()
     * - 사용자의 아이디를 반환하는 메서드
     * */
    @Override
    public String getUsername() {
        return user.getUid();
    }

    /*
     * getPassword()
     * - 사용자의 비밀번호를 반환하는 메서드
     * */
    @Override
    public String getPassword() {
        return user.getPass();
    }



    public String getNickname() {
        return user.getName();
    }



    /* 그외 */
    // 계정 만료 여부 (true: 만료X, false: 만료)
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    // 계정 잠김 여부 (true: 잠김X, false: 잠김)
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    // 계정 비밀번호 만료 여부 (true: 만료X, false: 만료)
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    // 계정 활성화 여부 (true: 활성화, false: 비활성화)
    @Override
    public boolean isEnabled() {
        return true;
    }

}

 

만들어진 authentication 객체를 검증 및 인증을 수행한다.

 

4. SecurityUserService
package kr.co.crud.security;

import kr.co.crud.entity.UserEntity;
import kr.co.crud.repository.UserRepo;
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;

/*
 * UserDetailsService
 * - UserDetails 객체를 사용해 인증 및 권한 부여
 * */
@Service
@Slf4j
public class SecurityUserService implements UserDetailsService {

    // 레포지토리를 통해 데이터베이스에서 사용자 정보를 조회
    @Autowired
    private UserRepo repo;

    // loadUserByUsername: 사용자의 아이디(username)을 기반으로 사용자 정보를 가져온다
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        log.warn("SecurityUserService loadUserByUsername");

        // 데이터베이스에서 사용자 정보를 조회
        UserEntity user = repo.findById(username).get();

        if(user == null) {
            throw  new UsernameNotFoundException(username);
        }

        // 조회한 사용자 정보로 UserDetails 객체 생성
        UserDetails myUser = MyUserDetails.builder()
                .user(user)
                .build();

        return myUser;
    }

}

 

3번에서 통과한 userdetails 객체를 사용해서 인증 및 권한을 검사한다.

5. 로그인 페이지 HTML
<form class="loginForm" th:action="@{/user/login}" method="post">
        <div class="content_main">
            <div class="input_id_row">
                <div class="img_id"><img src="/images/id.png"></div>
                <input type="text" class="input_id" name="uid" placeholder="아이디">
            </div>
            <div class="input_pw_row">
                <div class="img_pw"><img src="/images/pw.png"></div>
                <input type="password" class="input_pw" name="pass" placeholder="비밀번호">
            </div>
            <div class="login_btn_row">
                <input type="submit" class="login_btn" value="로그인">
            </div>
        </div>
    </form>

Security Configdptj 작성했던 설정으로 로그인 화면 구현한다.