728x90

 스프링을 이용한 프로젝트들에 참여하면서 대부분을 JWT를 이용하여 회원인증을 진행했는데 그 외에도 시큐리티와 세션을 이용한 로그인 처리방식에 대해 간단하게 구현하면서 공부해보자는 목적으로 글을 작성한다.

 참고로 스프링부트 버전은 3.2.1 로 진행했다.


 

 

GitHub - N1ghtsky0/playground

Contribute to N1ghtsky0/playground development by creating an account on GitHub.

github.com


목차

  1. 구현할 페이지와 페이지별 특징
  2. 프로젝트 설정
  3. 유저 엔티티 생성
  4. 스프링 시큐리티 설정
  5. 회원가입
  6. 로그인
  7. 결과(이미지 포함)

1. 구현할 페이지와 페이지별 특징

페이지명 엔드포인트 로그인 필요 유무 접근 권한
메인 페이지 / X ALL
회원가입 페이지 /join X ALL
로그인 페이지 /login X ALL
내 정보 확인 페이지 /info O USER, ADMIN
유저 정보 확인 페이지 /users O ADMIN

2. 프로젝트 설정

 

build.gradle

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-mustache'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.springframework.boot:spring-boot-starter-validation'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	runtimeOnly 'com.mysql:mysql-connector-j'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.security:spring-security-test'
}

 

pom.xml

<dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-mustache</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
      <groupId>com.mysql</groupId>
      <artifactId>mysql-connector-j</artifactId>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

3. 유저 엔티티 생성

// UserRole
public enum UserRole {
    USER, ADMIN
}

// User
@Builder
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class User implements UserDetails {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long seq;

    private String loginId;
    private String password;
    private String nickName;

    @Enumerated(value = EnumType.STRING)
    private UserRole role;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collection = new ArrayList<>();
        collection.add(() -> role.name());
        return collection;
    }

    @Override
    public String getUsername() {
        return loginId;
    }

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

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

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

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

4. 스프링 시큐리티 설정

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    private final String[] USER_ALLOWED_URLS = {"/info"};
    private final String[] ADMIN_ALLOWED_URLS = {"/users"};

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.cors(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(request -> request
                        .requestMatchers(USER_ALLOWED_URLS).authenticated()
                        .requestMatchers(ADMIN_ALLOWED_URLS).hasAuthority(UserRole.ADMIN.name())
                        .anyRequest().permitAll()
                )
                .formLogin(form -> form
                        .usernameParameter("loginId")
                        .passwordParameter("password")
                        .loginPage("/login")
                        .defaultSuccessUrl("/"))
                .logout(logout -> logout
                        .logoutUrl("/logout")
                        .logoutSuccessUrl("/")
                        .invalidateHttpSession(true)
                        .deleteCookies("JSESSIONID"));
        return http.build();
    }
}

5. 회원 가입

5-1. passwordEncoder Config

@Configuration
public class BCryptConfig {
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

 

5-2. DTO 생성

@Setter
@Getter
public class RequestJoin {
    @NotBlank
    private String loginId;
    @NotBlank
    private String password;
}

 

5-3. 컨트롤러-서비스 생성

// Controller
@RequiredArgsConstructor
@Controller
public class UserController {
    private final UserService userService;
    
    @PostMapping("/join")
    public String joinProcess(@ModelAttribute RequestJoin requestDTO) {
        userService.joinUser(requestDTO);
        return "/index";
    }
    // ...
}

// Service
public interface UserService {
    void joinUser(RequestJoin requestDTO);
    // ...
}

// 구현체
@RequiredArgsConstructor
@Service
public class UserServiceImpl implements UserService, UserDetailsService {
    private final UserRepo userRepo;
    private final PasswordEncoder passwordEncoder;

    @Override
    public void joinUser(RequestJoin requestDTO) {
        userRepo.save(User.builder()
                .loginId(requestDTO.getLoginId())
                .password(passwordEncoder.encode(requestDTO.getPassword()))
                .nickName(requestDTO.getLoginId())
                .role(UserRole.USER)
                .build());
    }    
    // ...
}

6. 로그인

 로그인은 직접 구현할 부분이 페이지 밖에 없다. 왜냐하면 이 프로젝트에서는 스프링 시큐리티에서 제공하는 formLogin을 활성화 했기 때문에 로그인에 사용할 아이디와 비밀번호의 파라미터명만 주의하며 페이지를 구성하고 로그인 url로 form 데이터를 전송하면 시큐리티에서 인증과 인가 작업을 모두 처리해준다.

 기존에 알고 있던 JWT를 사용했을 때는 JWT로 인증 처리를 하고 JWT 내부 정보를 이용해서 인가처리를 진행했는데 훨씬 수고가 덜 드는 작업이었다. 물론 간단하게 구현했기에 손이 덜 갔을 뿐이지 보안이라는 난이도 높은 작업을 깊게 파고 들어가면 훨씬 복잡할 것이라 예상한다.


7. 결과

메인 페이지 (로그인 X)
메인 페이지 (로그인 O)
내 정보 페이지 (로그인 O, 로그인 하지 않았을 경우 로그인 페이지로 넘어감)
유저 정보 페이지 (User 계정으로 로그인)
유저 정보 페이지 (ADMIN 계정으로 로그인)

 

계획했던 대로 회원가입과 로그인을 스프링 시큐리티를 이용하여 구현했으며 페이지별로 접근 권한을 부여했고 정상적으로 동작했음을 확인할 수 있었습니다.

 

추가)

1. 전체 코드를 살펴보면 템플릿 엔진으로 mustache를 쓰며 CSRF 토큰을 사용하는 걸 확인할 수 있는데 application.yml 파일에 아래 속성을 추가해주지 않으면 오류가 발생한다.

spring:
  mustache:
    servlet:
      expose-request-attributes: true

 

2. 로그 아웃의 경우에 맨 처음에 a 태그를 사용하여 구현했지만 스프링 시큐리티에서 로그아웃 처리를 할 때 method=post 가 아니면 오류가 발생한다. 만약 로그아웃 url을 제대로 입력했음에도 오류가 발생한다면 a 태그로 연결되어있는지, form이어도 method="post" 가 맞는지 한 번 확인해보는 걸 추천드립니다.

+ Recent posts