FreeHand

[Spring Boot] 게시판 프로젝트 - 02. 회원가입 및 로그인 본문

Web/Spring

[Spring Boot] 게시판 프로젝트 - 02. 회원가입 및 로그인

Jinn 2023. 9. 25. 21:19

 

이전 글

 

[Spring Boot] 게시판 프로젝트 - 01. 개발환경 및 엔티티 작성

목차1. 개발환경2. 프로젝트 생성 1. 개발환경개발환경은 다음과 같다. Java 17Spring Boot 3Spring Security 5OAuthJPAMySQLJSPBootstrapJQuery2. 프로젝트 생성1. 의존성 설정 먼저 사용할 의존성을 설정한다.depende

pressky99.tistory.com

목차

1. 회원가입 작성
2. 로그인 작성
3. 트러블슈팅

 

1. 회원가입 작성

회원가입 페이지

 

[joinForm.jsp]
<%@ page language="java" contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>

<!-- Navbar start -->
<%@ include file="../layout/header.jsp" %>
<!-- Navbar end -->

<!-- Form start -->
<div class="container">
    <form>
      <div class="form-group">
        <label for="username">Username:</label>
        <input type="text" class="form-control" placeholder="Enter username" id="username">
      </div>

      <div class="form-group">
        <label for="password">Password:</label>
        <input type="password" class="form-control" placeholder="Enter password" id="password">
      </div>

      <div class="form-group">
        <label for="email">Email address:</label>
        <input type="email" class="form-control" placeholder="Enter email" id="email">
      </div>
    </form>
    <button id="btn-save" class="btn btn-primary">회원가입</button>
</div>

<script src="/js/user.js"></script>
<!-- Form end -->

<!-- Footer start -->
<%@ include file="../layout/footer.jsp" %>
<!-- Footer end -->

</body>
</html>
[user.js]
let index = {
    init: function() {
        $("#btn-save").on("click", () => {
            this.save();
        });
    },
    save: function() {
        let data = {
            username: $("#username").val(),
            password: $("#password").val(),
            email: $("#email").val()
        }

        $.ajax({
            type: "POST",
            url: "/auth/api/user",
            data: JSON.stringify(data),
            contentType: "application/json; charset=utf-8",
            dataType: "json"
        }).done(function() {
            alert("가입 완료");
            location.href = "/";
        }).fail(function(error) {
            alert(JSON.stringify(error));
        });
    }
}
index.init();

joinForm.jsp에서 버튼을 누르면 user.js의 save 함수가 실행되도록 작성했다.

save 함수는 joinForm에서 작성한 form의 값을 읽어서 ajax로 post 요청을 보낸다.

 

@RequiredArgsConstructor
@RestController
public class UserApiController {

    private final UserService userService;

    @PostMapping("/auth/api/user")
    public ResponseEntity<Integer> save(@RequestBody User user) {
        userService.join(user);
        return new ResponseEntity<>(1, HttpStatus.CREATED);
    }
}
@RequiredArgsConstructor
@Service
public class UserService {

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder passwordEncoder;

    @Transactional
    public void join(User user) {
        String encodedPassword = passwordEncoder.encode(user.getPassword());
        user.setPassword(encodedPassword);
        userRepository.save(user);
    }
}

ajax로 보낸 요청은 UserApiController에서 받아서 userService의 join메서드를 호출하여 user 객체를 저장한다.

비밀번호는 주입받은 BCryptPasswordEncoder로 인코딩 되어 저장된다.

 

BCryptPasswordEncoder를 주입받으려면 빈으로 등록해야하는데, 스프링 시큐리티 설정은 아래 로그인 작성 파트에 있다. Config 클래스에 시큐리티 관련 여러 설정들을 하고 빈으로 등록하면 된다.


2. 로그인 작성

로그인 페이지

 

[loginForm.jsp]

... 생략 ...

<!-- Form start -->
<div class="container">
    <form action="/auth/login" method="POST">
      <div class="form-group">
        <label for="username">Username:</label>
        <input type="text" name="username" class="form-control" placeholder="Enter username" id="username">
      </div>

      <div class="form-group">
        <label for="password">Password:</label>
        <input type="password" name="password" class="form-control" placeholder="Enter password" id="password">
      </div>

      <div class="form-group form-check">
        <label class="form-check-label">
          <input name="remember" class="form-check-input" type="checkbox"> Remember me
        </label>
      </div>
      <button id="btn-login" class="btn btn-primary">로그인</button>
    </form>
</div>
<!-- Form end -->
/*
* 시큐리티가 로그인 요청을 가로채서 진행하고 완료되면
* UserDetails 타입 객체를 시큐리티 세션저장소에 저장함
* */
@Getter
public class PrincipalDetail implements UserDetails {

    private User user;

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

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collection = new ArrayList<>();
        collection.add(() -> "ROLE_" + user.getRole()); // "ROLE_"로 시작하는 것이 시큐리티 규칙

        return collection;
    }

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

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

    // 계정 만료 여부(true -> 정상)
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    // 계정 잠김 여부(true -> 정상)
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    // 비밀번호 만료 여부(true -> 정상)
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    // 계정 활성화 여부(true -> 정상)
    @Override
    public boolean isEnabled() {
        return true;
    }
}
/*
* 스프링이 로그인 요청을 가로챌 때 username, password를 가로챔
* password는 알아서 처리함
* username이 DB에 있는지 확인하면 끝
* */
@RequiredArgsConstructor
@Service
public class PrincipalDetailService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User principal = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("해당 사용자 찾을 수 없음: " + username));

        return new PrincipalDetail(principal); // 시큐리티 세션에 유저 정보가 저장됨
    }
}
@Configuration
public class SecurityConfig {

    @Bean
    BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable())
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers( "/WEB-INF/**", "/auth/**", "/", "/js/**").permitAll()
                        .anyRequest().authenticated())
                .formLogin(form -> form
                        .loginPage("/auth/loginForm").permitAll()
                        .loginProcessingUrl("/auth/login") // 해당 url로 오는 로그인 요청을 시큐리티가 가로채서 로그인 수행
                        .defaultSuccessUrl("/")); // 성공하면 홈으로 이동

        return http.build();
    }
}

loginForm.jsp에서 form으로 post 요청을 보내면 Spring Security가 해당 요청을 가로채 로그인하도록 작성했다.

홈, 회원가입, 로그인 페이지는 로그인을 하지 않아도 접근 가능해야 하므로 permitAll()을 해줬다.

 

로그인 페이지를 허용하지 않으면 브라우저에 리다이렉트가 많다는 에러가 발생할 것이다.

스프링 시큐리티는 허용되지 않은 페이지에 접근하면 로그인 페이지로 리다이렉트 하는데, 로그인 페이지도 접근이 허용된 페이지가 아니면 또 로그인 페이지로 보낸다. 그렇게 무한 리다이렉트 현상이 발생한다.

따라서 로그인 페이지를 꼭 permitAll 해줘야 한다.

 

로그인에 성공하면 사용자 정보(UserDetails)를 세션에 저장한다.

해당 정보에는 계정 정보와 사용자 정보(User객체)가 있다.

 

@RequiredArgsConstructor
@Service
public class UserService {

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder passwordEncoder; // 패스워드 인코더 주입

    @Transactional
    public void join(User user) {
        String encPassword = passwordEncoder.encode(user.getPassword()); // 패스워드 인코딩
        user.setPassword(encPassword);
        userRepository.save(user);
    }
}

서비스 로직에서 BCrypt 패스워드 인코딩을 적용해서 비밀번호를 저장하도록 수정한다.

 

[header.jsp]
<%@ page language="java" contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>

<sec:authorize access="isAuthenticated()">
    <sec:authentication property="principal" var="principal" />
</sec:authorize>

<!DOCTYPE html>
<html lang="en">
<head>
... 생략 ...
</head>
<body>

<nav class="navbar navbar-expand-md bg-dark navbar-dark">
  <a class="navbar-brand" href="/">홈</a>
  <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#collapsibleNavbar">
    <span class="navbar-toggler-icon"></span>
  </button>
  <div class="collapse navbar-collapse" id="collapsibleNavbar">

  <c:choose>
    <c:when test="${empty principal}">
      <ul class="navbar-nav">
        <li class="nav-item">
          <a class="nav-link" href="/auth/loginForm">로그인</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" href="/auth/joinForm">회원가입</a>
        </li>
      </ul>
    </c:when>
    <c:otherwise>
      <ul class="navbar-nav">
        <li class="nav-item">
          <a class="nav-link" href="/board/saveForm">글쓰기</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" href="/user/updateForm">회원정보</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" href="/logout">로그아웃</a>
        </li>
      </ul>
    </c:otherwise>
  </c:choose>
  </div>
</nav>
<br />
</body>

로그인을 하면 세션 정보를 저장하고 그 정보를 사용해서 로그인 유무에 따라 메뉴바의 내용이 바뀌게 작성했다.

<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>

<sec:authorize access="isAuthenticated()">
    <sec:authentication property="principal" var="principal" />
</sec:authorize>

이때 이렇게 jsp에서 spring security를 사용하려면 taglib을 추가해줘야 한다.

헤더에 있는 principal은 메뉴바를 바꾸는 것 외에도 이후에 중요한 역할을 한다.


3. 트러블슈팅

로그인 페이지를 요청하는 경로인 "/auth/loginForm"을 접근 허용했는데 계속 리다이렉트 되는 현상이 발생했다.

그런데 생각해보니 로그인 페이지 자체는 허용을 하지 않았던 것이다.

@Controller
public class UserController {

    @GetMapping("/auth/joinForm")
    public String joinForm() {
        return "user/joinForm";
    }

    @GetMapping("/auth/loginForm")
    public String loginForm() {
        return "user/loginForm";
    }
}

즉, 여기서 "/auth/loginForm"만 허용하고 실제 로그인 페이지인 "user/loginForm"은 허용하지 않아서 계속 리다이렉트가 발생했던 것이다.

 

@Configuration
public class SecurityConfig {

    @Bean
    BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable())
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers( "/WEB-INF/**", "/auth/**", "/", "/js/**").permitAll()
                        .anyRequest().authenticated())
                .formLogin(form -> form
                        .loginPage("/auth/loginForm").permitAll()
                        .loginProcessingUrl("/auth/login")
                        .defaultSuccessUrl("/"));

        return http.build();
    }
}

접근 가능 경로에 "/WEB-INF/**"를 추가했고 자바스크립트도 접근할 수 있도록 "/js/**"도 추가했다.

이렇게 하니 로그인 페이지에서 무한 리다이렉트 현상이 해결되었다.

 

 

다음 글

 

[Spring Boot] 게시판 프로젝트 - 03. 게시판 글 CRUD

이전 글 { this.save(); }); }, save: function() { let d" data-og-host="pressky99.tistory.com" data-og-source-url="https://pressky99.tistory.com/39" data-og-url="https://pressky99.tistory.com/39" data-og-image="https://scrap.kakaocdn.net/dn/be1dFf/hyWY8MN

pressky99.tistory.com