각진 세상에 둥근 춤을 추자

[CRUD] 3. 로그인 기능 구현 - 스프링 시큐리티 본문

프로젝트/CRUD

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

circle.j 2024. 1. 10. 17:25

스프링 시큐리티와 <Form> 전송을 통한 로그인을 진행한다. 


1. Spring Security dependency 추가

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

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

 

프로그램을 구동시키면 스프링 시큐리티에서 제공하는 로그인 페이지가 뜬다.

 

 

아래와 같이 콘솔창을 통해 비밀번호를 입력한다.

  • Username: user
  • Password: 콘솔창 확인 

 

또는 application.properties에 기본 스프링 시큐리티 Username과 Password를 지정한다. 

#############################################
#스프링 시큐리티 임시 아이디/패스워드 설정
#############################################
spring.security.user.name=user
spring.security.user.password=1234

 


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();
    }
}

 


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;
    }

}

 


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;
    }

}

 


5. 로그인 HTML 

앞서 SecurityConfig에서 작성했던 설정을 토대로 로그인 화면을 구현한다. 

<form class="loginForm" th:action="@{/user/login}" method="post">
    <div>
        <input class="idForm" type="text" name="uid" placeholder="아이디">
    </div>
    <div>
        <input class="passForm" type="password" name="pass" placeholder="비밀번호">
    </div>
    <button class="btn btnLogin" type="submit">로그인</button>
</form>