Spring/Spring Boot

[Spring] 스프링 시큐리티 사용하기 - spring security (로그인, 로그아웃, 회원가입)

민돌v 2021. 12. 4. 02:47
레거시(?) 블로그 글 입니다. 
JWT 토큰과 Sping Boot 3.0 + Spring Security6 를 이용한 Rest API 방식의 인증-인가 방식을 이용한 더 자세한 내용을 원하시는 분은 아래 글을 참고해주세요 !

🤗 https://thalals.tistory.com/436

 

스프링 시큐리티 프레임워크

  • 스피링 시큐리티(Spring Security) - '스프링 시큐리티' 프레임워크는 스프링 서버에서 필요한 인증 및 인가를 위해 스프링에서 제공해주는 프레임워크입니다.

 

스프링 시큐리티 프레임워크 추가(빌드 추가)

  • build.gradle
	// 스프링 시큐리티
    implementation 'org.springframework.boot:spring-boot-starter-security'
    // Thymeleaf (뷰 템플릿 엔진)
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

스프링 시큐리티 활성화

  • security 패키지 > WebSecurityConfig.Java
@Configuration
@EnableWebSecurity // 스프링 Security 지원을 가능하게 함
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.headers().frameOptions().disable();

        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .defaultSuccessUrl("/")
                .permitAll()
                .and()
                .logout()
                .permitAll();
    }
}
  • @Configuration    - 이 Class, .java 파일이 설정파일임을 명시하기위한 어노테이션 입니다.(Bean에 등록합니다.)
  • @EnableWebSecurity   - 스프링 시큐리티를 사용하겠다는 어노테이션입니다.
    • WebSecurityConfigyreAdapter를 상속받아 시큐리티 기능을 사용할 수 있습니다.
  • @Override  -  Configure 함수를 @Override 하여 해당 설정을들 바꾼다는 의미 입니다.
  • .anyRequest().authenticated()  - 어떤 요청이 들어오든 로그인을 하도록하겠다는 의미(인증)
  • defaultSuccessUrl('/')    - 로그인 성공시 돌아갈 페이지
  • PermitAll()  - 인증을 예외로 허가하겠다는 의미입니다.  ➡ 위의 설정에서는 로그인과 로그아웃은 허가하겠다는 의미입니다.

 

 

스피링 시큐리티 Default 로그인 기능

이렇게 설정을 해놓으면, 아무것도 만들지 않아도 실행하자마자 로그인 페이지가 로딩 됩니다.

회원가입을 하지 않았기 때문에, 인텔리제이 터미널에 패스워드로 로그인 할 수 있습니다.

 

  • Username: user
  • Password: spring 로그 확인 (서버 시작 시마다 변경됨)

 

스프링 시큐리티 로그인 페이지 커스텀

이번엔 로그인 과 회원가입 페이지를 구현해보자

1) 로그인 페이지를 Static > login.html 을 만든다.

2) 회원가입 페이지 요청처리를 위해 타임리프 구문을 추가한다.

main > resources > application.properties

spring.thymeleaf.prefix=classpath:/static/

 

3) 시큐리티 설정에서 로그인 페이지 url을 변경해준다.

.security > WebSecurityConfig

.formLogin()
.loginPage("/user/login")
.failureUrl("/user/login/error")

- login 은 "/user/login" url로 렌더링

- 로그인 실패시 "error" 페이지 렌더링

 

UserController

@Controller
public class UserController {

    // 회원 로그인 페이지
    @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";
    }
}

 

이렇게 실행하면, 스프링 시큐리티에서 CSS, JS가 적용되지 않는다.

css, js 적용 안됨

'개발자 도구' 에러 확인 302code 

스프링 시큐리티에서 허가를 해주지 않았기 때문이다.

security > WebSecurityConfig

http.authorizeRequests()
                // image 폴더를 login 없이 허용
                .antMatchers("/images/**").permitAll()
                // css 폴더를 login 없이 허용
                .antMatchers("/css/**").permitAll()
                // 그 외 모든 요청은 인증과정 필요
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/user/login")
                .loginProcessingUrl("/user/login")
                .defaultSuccessUrl("/")
                .permitAll()
                .and()
                .logout()
                .permitAll();

.antMatchers().permitAll()로 인증을 허가해준다.

 

 

회원가입 기능 구현

User 테이블을 먼저 설계한다.

- role 컬럼에는 User 아니면 Admin (관리자) 를 값으로 받는 enum 클래스 타입을 만들어서 사용해준다.

@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;
}
package com.sparta.springcore.model;

public enum UserRole {
    USER,  // 사용자 권한
    ADMIN  // 관리자 권한
}

 

회원가입 API

Controller

// 회원 가입 요청 처리
    @PostMapping("/user/signup")
    public String registerUser(SignupRequestDto requestDto) {
        userService.registerUser(requestDto);
        return "redirect:/";
    }
}

DTO

@Setter
@Getter
public class SignupRequestDto {
    private String username;
    private String password;
    private String email;
    private boolean admin = false;
    private String adminToken = "";
}

Service

Optional : 자바8에 추가된 타입으로 별도의 널포인트 에러처리를 하지 않아도 되는 타입이다. 

@RequiredArgsConstructor
@Service
public class UserService {
    private final UserRepository userRepository;
    private static final String ADMIN_TOKEN = "AAABnv/xRVklrnYxKZ0aHgTBcXukeZygoC";


    public User 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);

				return user;
    }
}

Repository

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsername(String username);
}

 

 

그리고 다시 접근 제한 해제하기

// 회원 관리 URL 전부를 login 없이 허용
.antMatchers("/user/**").permitAll()
.antMatchers("/h2-console/**").permitAll()

 

 

로그인, 로그아웃 기능 구현하기

로그인은 스프링 시큐리티를 이용한다.

스프링 시큐리티를 사용하면, 클라이언트가 request요청을 보내기 전에 spring security에서 인증 및 인가 절차를 걸친다.

 

스프링 시큐리티 로그인 처리 과정

회원 정보를 UserDetailsService에서 디비정보를 조회해 호가인한다.

인증 및 인가가 완료되면 UserDetails에 사용자 정보를 담아서 넘겨준다.

  • 인증/인가 성공 시에만, Controller 에게 회원 정보 전달 (UserDetails)
  • 우리가 구현해 줘야 할 클래스
    1. UserDetailsService 인터페이스 → UserDetailsServiceImpl 클래스
    2. UserDetails 인터페이스 → UserDetailsImpl 클래스

 

 

security > UserDetailsServiceImpl.java

UserDetailsServicelmpl

회원 정보의 서비스 로직처리를 담당하는 클래스이다.

@RequiredArgsConstructor
@Service
public class UserDetailsServiceImpl implements UserDetailsService{

	private final UserRepository userRepository;

	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("Can't find " + username));

        return new UserDetailsImpl(user);
    }
}

 

security > UserDetailsImpl

 

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

 

Controller

스프링 시큐리티를 거쳐서 controller로 갈때 @AuthenticationPrincipal 어노테이션을 이용해 로그인 된 사용자 정보를 받아올 수 있다.

@Controller
public class HomeController {
    @GetMapping("/")
    public String home(Model model, @AuthenticationPrincipal UserDetailsImpl userDetails) {
        model.addAttribute("username", userDetails.getUsername());
        return "index";
    }
}

 

지금 해 본 방법은 제일 간단한 스프링 시큐리티를 사용하는 기초적인 방법이다