[BEInternship] 회원가입, 로그인 간단 구현

junghan·2023년 8월 21일
0

BE 인턴십

목록 보기
6/9
post-thumbnail

비밀번호 암호화

회원 가입 기능을 만들경우 절대 입력한 문자열을 그대로 DB에 저장하면 안된다. 보안에 매우 취약하기 때문이다. 그렇기때문에 패스워드를 해싱 하여 저장해야하는데 BCrypt가 가장 많이쓰이는 해싱 방법이다. (참고로 해싱된 패스워드를 다시 encode할 수 있으면 안됨 그렇기 때문에 요즘 웹사이트에서 비밀번호 찾기를 할 경우 비밀번호를 알려주는 게 아니라 재 설정을 하게 하는 것 입니다.)

예시

@Bean
UserDetailsManager users(DataSource dataSource) {
	UserDetails user = User.builder()
		.username("user")
		.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
		.roles("USER")
		.build();
	UserDetails admin = User.builder()
		.username("admin")
		.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
		.roles("USER", "ADMIN")
		.build();
	JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
	users.createUser(user);
	users.createUser(admin);
	return users;
}

https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/jdbc.html#servlet-authentication-jdbc

ScurityConfig 설정

@EnableWebSecurity
@Configuration
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfig {

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain configure(HttpSecurity http) throws Exception {
        http
                .httpBasic(httpBasic -> httpBasic.disable())
                .csrf(AbstractHttpConfigurer::disable) // post 방식으로 값을 전송할 때 token을 사용해야하는 보안 설정을 해제
                .authorizeRequests(authorizeRequests ->
                                .requestMatchers("/board/**").hasRole(Role.USER.name())
                                .requestMatchers("/adminpage").hasRole(Role.ADMIN.name())
                                .requestMatchers("/swagger-ui/**", "/v3/apiXdocs/**", "/swagger-ui-jung.html", "/webjars/**").permitAll()
                                .requestMatchers("/users/signup").permitAll()
                                .requestMatchers("/signupform").permitAll()
                                .requestMatchers("/signinform").permitAll()
                                .requestMatchers("/").permitAll()
                                .anyRequest().authenticated()
                )
                .formLogin(
                        login ->
                                login.loginPage("/signinform")
                                        .loginProcessingUrl("/login")
                                        .defaultSuccessUrl("/board/mainpage")
                );

        return http.build();
    }

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring().requestMatchers("/v3/api-docs/**");
    }

}

비밀번호 암호화를 하기 위해 위와 같이 BCryptPasswordEncoder를 빈에 등록해줍니다.


    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

암호화 비즈니스 로직 구현


/*service 구현*/
  public Long signUp(UserSignUpRequest userSignUpRequest) throws Exception {
        if(this.isEmailExist(userSignUpRequest.getEmail())) {
            throw new Exception("Your Mail already Exist.");
        }
        Users users = userSignUpRequest.toEntity();
        users.hashPassword(bCryptPasswordEncoder);
        return usersRepository.save(users).getId();
    }
  
/*USERS 메소드*/
  public Users hashPassword(PasswordEncoder passwordEncoder) {
        this.password = passwordEncoder.encode(this.password);
        return this;
    }

데이터 저장 시, 아래와 같이 비밀번호가 암호화가 된 것을 확인할 수 있습니다.


로그인

  1. 시큐리티가 /login주소 요청이 요면 낚아채서 로그인을 진행시킵니다.
  2. 로그인을 진행이 완료가 되면 시큐리티 session을 만들어 줍니다. (security contectHolder)
    2-1. 같은 세션 공간인데 시큐리티가 자신만의 시큐리티 공간에 만듦
  3. 이 세션에 들어가는 오브젝트는 정해져있음 (Authentication 타입객체)
  4. Authentication 안에 User 정보가 있어야함.
  5. User 오브젝트의 타입은 UserDetails타입객체로 정해져있음.

즉, Security Session => Authentication => UserDetails

Userdetails 설정


package com.wanted.jungproject.config.userDetail.domain;

import com.wanted.jungproject.domain.user.domain.Users;
import lombok.AllArgsConstructor;
import lombok.Builder;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

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

@Builder
public class CustomUserDetails implements UserDetails {

    private Users users; //콤포지션
    
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collect = new ArrayList<>();
        collect.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return users.getRole().getKey();
            }
        });
        return collect;
    }

    public CustomUserDetails(Users users) {
        this.users = users;
    }

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

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

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

메소드들을 오버라이딩해줘야합니다. (로그인 유효기간, 휴면계정 설정 등 다양한 조건을 추가할 수 있습니다.) 그 중 특히 해당 Users의 권한을 리턴하는 getAuthorities를 신경써서 권한값을 반환할 수 있도록 수정해줍니다.

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collect = new ArrayList<>();
        collect.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return users.getRole().getKey();
            }
        });
        return collect;
    }

유저 역할

package com.wanted.jungproject.domain.user.domain;


import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum Role {
    GUEST("ROLE_GUEST", "손님"),
    USER("ROLE_USER", "일반사용자"),
    ADMIN("ADMIN_USER", "관리자");

    private final String key;
    private final String title;
}

UserDetailsService 설정

package com.wanted.jungproject.config.userDetail.application;

import com.wanted.jungproject.config.userDetail.domain.CustomUserDetails;
import com.wanted.jungproject.domain.user.domain.Users;
import com.wanted.jungproject.domain.user.domain.UsersRepository;
import lombok.RequiredArgsConstructor;
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;

import java.util.Optional;

@Service
@RequiredArgsConstructor
public class CustomUserDetailService implements UserDetailsService {

    @Autowired
    private final UsersRepository usersRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        Optional<Users> users = usersRepository.findByEmail(username);
        if (users.isPresent()) {
            return new CustomUserDetails(users.get());
        }
        return null;
    }
}

시큐리티 설정에서 loginProcessingUrl(“/login”)을 걸어놨기 때문에 /login 요청이 오면 자동으로 UserDetailsService타입으로 Ioc 되어 있는 loadUserByUsername함수가 실행

클라이언트로 전달받은 username을 기반으로 유저를 검색하고, UserDetails => Authentication => 시큐리티 Session으로 전달하여 로그인 확인 절차 진행

profile
42seoul, blockchain, web 3.0

0개의 댓글