Back-end/Spring

[SpringBoot] 7주차 스터디 ① (스프링 시큐리티)

poppy 2021. 10. 2. 23:01
반응형
스프링 시큐리티란?
- '스프링 시큐리티' 프레임워크는 스프링 서버에 필요한 인증 및 인가를 위해 많은 기능을 제공해 줌으로써 개발의 수고를 덜어준다.
- 인증: 사용자 신원을 확인하는 행위 ex) 로그인 통해 본인 확인
- 인가: 사용자의 권한을 사용하는 행위 ex) 역할에 따른 사용 권한 관리 (회원과 관리자)

 

이전에 만들어두었던 나만의 셀렉샵에 회원 가입 기능과 관리자 권한을 추가해보겠습니다.

 

1. 회원 가입 구현

사용자의 정보를 저장해야 하므로 User 테이블을 다음과 같이 생성한다. 사용자 역할을 구분할 수 있는 UserRole 도 생성한다.

@Setter
@Getter // get 함수를 일괄적으로 만들어줍니다.
@NoArgsConstructor // 기본 생성자를 만들어줍니다.
@Entity // DB 테이블 역할을 합니다.
public class User extends Timestamped {

    public User(String username, String password, String email, UserRole role) {
        this.username = username;
        this.password = password;
        this.email = email;
        this.role = role;
    }
    // ID가 자동으로 생성 및 증가합니다.
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Id
    private Long id;

    // 반드시 값을 가지도록 합니다.
    @Column(nullable = false)
    private String username;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    private String email;

    // 사용자의 역할(회원, 관리자)
    @Column(nullable = false)
    @Enumerated(value = EnumType.STRING)
    private UserRole role;
}
public enum UserRole {
    USER,  // 사용자 권한
    ADMIN  // 관리자 권한
}

 

회원 가입 관련 요청을 처리하기 위한 UserController 와 회원 정보를 입력받을 SignupRequesDto 를 생성한다.

@Controller
public class UserController {

    private final UserService userService;

    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }

    // 회원 로그인 페이지
    @GetMapping("/user/login")
    public String login() {
        return "login";
    }

	// 로그인 에러 시 처리
    @GetMapping("/user/login/error")
    public String loginError(Model model) {
        model.addAttribute("loginError", true);
        return "login";
    }

    // 회원 가입 페이지
    @GetMapping("/user/signup")
    public String signup() {
        return "signup";
    }

    // 회원 가입 요청 처리
    @PostMapping("/user/signup")
    public String registerUser(SignupRequestDto requestDto) {
        userService.registerUser(requestDto);
        return "redirect:/";
    }
}
@Setter
@Getter
public class SignupRequestDto {
    private String username;
    private String password;
    private String email;
    private boolean admin = false;
    private String adminToken = "";
}

 

회원 가입을 처리하기 위한 UserRepository 와 UserService 를 생성한다.

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsername(String username); // 이름으로 사용자를 찾는다
}
@Service
public class UserService {
    private final UserRepository userRepository;
    private static final String ADMIN_TOKEN = "AAABnv/xRVklrnYxKZ0aHgTBcXukeZygoC"; // 관리자 권한을 확인하기 위한 토큰

    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    // 회원가입 처리
    public void registerUser(SignupRequestDto requestDto) {
        String username = requestDto.getUsername();
        String password = requestDto.getPassword();
        // 회원 ID 중복 확인
        Optional<User> found = userRepository.findByUsername(username);
        if (found.isPresent()) {
            throw new IllegalArgumentException("중복된 사용자 ID 가 존재합니다.");
        }

        String email = requestDto.getEmail();
        // 사용자 ROLE 확인
        UserRole role = UserRole.USER;
        if (requestDto.isAdmin()) { // 관리자라면 관리자 토큰 확인 후 관리자 권한을 준다
            if (!requestDto.getAdminToken().equals(ADMIN_TOKEN)) {
                throw new IllegalArgumentException("관리자 암호가 틀려 등록이 불가능합니다.");
            }
            role = UserRole.ADMIN;
        }

        User user = new User(username, password, email, role);
        userRepository.save(user); // 회원 가입
    }
}

 

2. 로그인, 로그아웃 구현

스프링 시큐리티를 통해 회원 가입을 처리하기 위해 WebSecurityConfig 를 생성하고 로그인, 로그아웃 등 여러 권한을 설정한다.

@Configuration
@EnableWebSecurity // 스프링 Security 지원을 가능하게 함
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public BCryptPasswordEncoder encodePassword() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.headers().frameOptions().disable();
        http.authorizeRequests()
                // image 폴더를 login 없이 허용
                .antMatchers("/images/**").permitAll()
                // css 폴더를 login 없이 허용
                .antMatchers("/css/**").permitAll()
                // 회원 관리 URL 전부를 login 없이 허용
                .antMatchers("/user/**").permitAll()
                // h2-console URL을 login 없이 허용
                .antMatchers("/h2-console/**").permitAll()
                // 그 외 모든 요청은 인증과정 필요
                .anyRequest().authenticated() // 어떤 요청이든 로그인 과정이 없으면 로그인을 하게 한다
                .and()
                .formLogin() // 로그인 페이지에 대해서는 허용
                .loginPage("/user/login") // 로그인 위치
                .loginProcessingUrl("/user/login") // 로그인 에러 위치
                .defaultSuccessUrl("/") // 로그인한 후 이동할 페이지
                .permitAll()
                .and()
                .logout() // 로그아웃 기능 허용
                .logoutUrl("/user/logout")
                .permitAll()
                .and()
                .exceptionHandling()
                .accessDeniedPage("/user/forbidden"); // 인가되지 않았을 때 이동할 페이지
    }
}

 

스프링 시큐리리를 통해 로그인을 처리할 수 있도록 사용자의 정보를 담고 있는 UserDetailsImpl 를 생성한다.

public class UserDetailsImpl implements UserDetails {

    private final User user;

    public UserDetailsImpl(User user) {
        this.user = user;
    }

    public User getUser() {
        return user;
    }

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

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

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Collections.emptyList();
    }
}

 

로그인한 후 첫(메인) 페이지 요청을 처리하기 위한 HomeController 를 생성한다. @AuthenticationPrincipal 는 스프링 시큐리티가 전달해주는 부분이다.  UserDetailsServiceImpl 는 사용자 정보 가져오는 것을 처리한다.

@Controller
public class HomeController {
	// 첫(메인) 페이지 요청 처리
    @GetMapping("/")
    public String home(Model model, @AuthenticationPrincipal UserDetailsImpl userDetails) {
        model.addAttribute("username", userDetails.getUsername());
        return "index";
    }
}
@Service
public class UserDetailsServiceImpl implements UserDetailsService{

	@Autowired
	private UserRepository userRepository;

        // username으로 사용자 정보 가져오기
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("Can't find " + username));

        return new UserDetailsImpl(user);
    }
}

 

반응형