스프링 security 회원가입,로그인 로직

안석우(문과대학 철학)·2024년 11월 12일

스프링

목록 보기
13/15

프로젝트를 진행하면서 Spring security를 이용해서 정말 간단한 회원가입, 로그인 로직을 구현해서 기록으로 남긴다. 모든 스프링 Security가 이런 구조로 동작하는지는 모르겠다. 그건 나중에 공부하면서 채워가자

SecurityConfig

security에 관한 configuration file이다. 아래와 같이 구현했는데,
어느 페이지에서 로그인을 요구할지, 로그인이 필요할 때 로그인 페이지는 무엇으로 할지, 로그인 요청을 받는 경로는 어떻게 할지를 정의한다.

@Configuration
@EnableWebSecurity
public class SecurityConfig {


    @Bean  // 비밀번호 암호화를 진행하는 메서드
    public BCryptPasswordEncoder bCryptPasswordEncoder() {

        return new BCryptPasswordEncoder();
    }

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

        http
                .authorizeHttpRequests((auth) -> auth
                        .requestMatchers("/", "/login").permitAll()
                        .requestMatchers("/addRestaurant").hasRole("ADMIN") //addRestaurant 경로는 ADMIN role이 있어야 접근 가능
                        .requestMatchers("/my/**").hasAnyRole("ADMIN", "USER")
                        .anyRequest().permitAll()
                        // 이 로직은 상단부터 진행됨, 만약 anyRequest().permitAll()을 맨 위에 놓으면 모든 페이지에 접근 가능해져서 아래 로직은 의미가 없어져버림
                );

        http
                .formLogin((auth) -> auth.loginPage("/login") // 로그인 페이지 경로가 /login이라고 설정, 로그인이 필요할 때 spring security가 /login 경로로 리다이렋트
                        .loginProcessingUrl("/loginProc")   // loginProc이라는 경로로 post요청이 들어오면 spring security가 그 값을 받아서 로그인 처리를 진행함, 그래서 controller에 /loginProc이 없어도 되는거임, 
                        // loginProc에 post가 들어오면, CustomUserService에서 username가지고 DB에 조회해서 해당 유저를 찾음,
                        // 그거 가지고 CustomUserDetails 객체를 만들어서 이 유저의 인증정보를얻음.그래서 얘가 valid하면 로그인을 시켜줌
                        .permitAll()
                );

        http
                .csrf((auth) -> auth.disable());  // csrf는 spring security 위변조 방지설정인데 개발환경에서 잠시 이걸 disable시킴, 나중에 다시 enable 시킬거임


        return http.build();
    }
}

CustomUserDetails, CustomUserDetailsService

Spring에서 로그인 정보가 필요할 떄(ex,관리자 페이지에 접속) UserRepository를 걸쳐 DB에서 로그인 정보를 가져오고 이 로그인 정보가 유효한지 검증(ex, 관리자 페이지에 들어오는 유저가 ADMIN계정인지 검증)하는 데 쓰인다, CustomUserDetailsService가 CustomUserDetails를 이용해서 검증한다.

UserDetails, UserDetailsService 를 상속받아서 구현했다.

public class CustomUserDetails implements UserDetails {

    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() {

                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;
    }
}
@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){
            return new CustomUserDetails(userData);
        }
        return null;
    }
}

UserEntity, UserRepository

DB에 저장할 UserEntity, DB와 데이터를 주고받을 UserRepository,
UserRepository는 JpaRepository<UserEntity, Long> 을 상속해 구현했다. UserEntity는 내가 정의한 userEntity고 Long은 UserEntity의 Id타입을 내가 Long으로 정의했다.

@Repository
public interface UserRepository extends JpaRepository<UserEntity, Long> {
    boolean existsByUsername(String username);

    UserEntity findByUsername(String username);

}
@Entity
@RequiredArgsConstructor
@Getter
@Setter
public class UserEntity {
    @Id
    @GeneratedValue
    private Long restaurantId;

    @Column(unique = true)
    private String username;

    private String password;

    private String role;

}

JoinController, JoinService

회원가입을 담당하는 service와 controller다. 크게 특별한 점은 없고 JoinService에서 비밀번호를 암호화해서 DB에 쿼리를 날린다는 점 정도 기억하면 될 듯하다.

@Controller
@Slf4j
public class JoinController {

    @Autowired
    JoinService joinService;

    @GetMapping("/join")
    public String joinP() {

        return "join";
    }

    @PostMapping("/joinProc")
    public String joinProcess(
            @RequestParam("username") String name,
            @RequestParam("password") String password
    ) {

        System.out.println("joinProcess is called");
        System.out.println(name);

        joinService.joinProcess(new JoinDTO(name,password));


        return "redirect:/login";
    }
}
@Service
public class JoinService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;


    public void joinProcess(JoinDTO joinDTO) {


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


        UserEntity data = new UserEntity();
        data.setUsername(joinDTO.getUsername());
        data.setPassword(bCryptPasswordEncoder.encode(joinDTO.getPassword()));
        data.setRole("ROLE_ADMIN");
        // 여기 근데 ROLE을 SUPERADMIN이 승인해줘야 ADMIN이 될 수 있게 나중에 바꿔주자


        userRepository.save(data);
    }

}

LoginController

로그인 페이지로 get요청이 왔을 때 그 페이지로 보내주는 컨트롤러다. 기억할 점은 로그인하겠다고 아이디와 비밀번호를 Post요청으로 보내면, 이걸 처리하는 컨트롤러 로직이 따로 있는 게 아니라 SecurityConfig에 설정해놓은대로 Spring이 알아서 처리해준다 실제로 이 기능이 어떻게 구현됐는지는 모르겟지만 어쨌든 로그인 요청을 처리할 컨트롤러를 따로 만들지 않는다.

위의 코드에서 setter난 필드 주입 같은 건 별로 바람직한 구현이 아니다. 근데 급하게 만드느라 이렇게 만들었으니 나중에 리팩토링 해주자.

@Controller
@Slf4j
public class LoginController {
    @GetMapping("/login")
    public String loginP() {

        return "login";
    }
}

0개의 댓글