Java/Spring Boot 게시판

spring boot 게시판 - 4 <spring security form login 구현>

요술공주밍키 2022. 11. 18. 10:25

https://magicmk.tistory.com/29

 

spring boot 게시판 - 3 <패스워드 암호화, 회원 목록 조회>

https://magicmk.tistory.com/28 spring boot 게시판 - 2 https://magicmk.tistory.com/25 Spring boot 게시판 - 1 < 간단 회원 가입 구현> 시스템 구성 Spring boot 2.7.5 Gradle Java 11 Intellij Ultimate 라이브러리 thymeleaf jpa web lombok h2

magicmk.tistory.com

이전 편에 패스워드 암호화를 진행한 뒤 바로 로그인 구현을 위해 삽질을 했는데 진짜 별거 아닌 것 같았지만

수많은 시행착오를 겪느라 구현이 늦어버렸다. spring security config에 WebSecurityConfigureAdapter가

deprecated 되면서 다른 방안을 모색하느라 시간을 많이 허비한 것 같다...

앞으로는 전체 코드를 같이 볼 수 있게 github도 같이 올리면 좋을 것 같다. 그래서 맨 하단에 github 주소도

넣도록 하겠다.


✅ security config 설정하기

이번에 로그인을 구현하면서 이전 시간에 했던 security config와는 모습이 많이 바뀌었다.

package com.practice.board.security;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;


@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

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

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        // 정적 리소스들이 보안필터를 거치지 않게끔
        return (web) -> web.ignoring().antMatchers("/css/**", "/js/**", "/img/**", "/font/**");
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.cors().disable()                               // cors 방지
                .csrf().disable()                           // csrf 방지
                .headers().frameOptions().disable();        // x frame 방어 해제

        http.authorizeRequests()
                // 페이지 권한 설정
                .antMatchers("/member/**").hasRole("USER")
                .antMatchers("/board/**").hasRole("USER")
                .anyRequest().permitAll();

        http.formLogin()
                    .loginPage("/loginForm")
                    .loginProcessingUrl("/login")
                    .defaultSuccessUrl("/")
                    .failureUrl("/login?error=true")
                    .usernameParameter("email")
                    .passwordParameter("password")
                .and()
                    .logout()
                    .logoutSuccessUrl("/?logout=true")
                    .invalidateHttpSession(true)
                    .deleteCookies("JSESSIONID");

        // status code 핸들링
        http.exceptionHandling().accessDeniedPage("/denied");

        return http.build();
    }
}

formLogin에 관한 내용들은 간단하게 살펴보자면 다음과 같다.

 

⏹️ login 관련

메소드(?) 내용
loginPage() 해당 메소드가 없다면 그림1 과 같은 페이지가 나오고
파라미터로 html 파일 경로를 적어주면
해당 파일이 출력된다.
loginProcessingUrl() 말 그대로 login프로세스가 실행될 url을 적어주면 된다.
필자는 /login이라고 적었지만
/helloJava 이렇게 적어도 된다.
다만 login form에서 action을 동일하게 적어줘야 한다.
defaultSuccessUrl() 로그인이 성공했을 때 돌아갈 주소를 적어준다.
failureUrl() 로그인이 실패했을 때 돌아가는 주소
필자처럼 param값을 넘겨줄 수 있다.
usernameParameter() 해당 메소드에 적은 name을 login form에 적힌 name과
일치 시켜준다.
passwordParameter() 해당 메소드에 적은 name을 login form에 적힌 name과
일치 시켜준다.

그림1

 

⏹️ logout 관련

메소드 내용
logoutSuccessUrl() 로그아웃을 성공했을 때 돌아가는 주소
이것 또한 param을 같이 넘길 수 있다.
invalidateHttpSession() 로그아웃 시 session을 전부 날린다.
deleteCookies() 인자로 적은 값의 cookies를 죽인다.

 

⏹️ 무쓸모 삽질...

WebSecurityConfigureAdapter가 *deprecated 되었는데 인터넷에 떠돌아 다니는 수많은 예제가 전부 해당 내용으로

작성되어 있다보니 config를 변경하는데 많은 삽질을 했다. 특히 이전 버전의 코드에서는

 @Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}

해당 내용이 있었는데 이것을 어떻게 변경해야 하는가 삽질을 많이 한 것 같다.

결론은 없어도 되는 것 같다.... 단 이후에 UserDetailsService를 override 해야 하는 과정은 필요하다.

Deprecated
사용해도 괜찮으나 언제 없어질 지 모르고 무슨 일이 벌어질지 모르니 권고하지 않는다.
라는 내용이다.

✅ UserDetailsService 구현

package com.practice.board.service.Impl;

import com.practice.board.domain.Member;
import com.practice.board.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {

    private final MemberRepository memberRepository;

    @Override
    @Transactional(readOnly = true)
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        Member member = memberRepository.findByEmail(email).orElseThrow(() -> new UsernameNotFoundException("이메일이 존재하지 않습니다."));

        return toUserDetails(member);
    }

    private UserDetails toUserDetails(Member member) {
        return User.builder()
                .username(member.getEmail())
                .password(member.getPassword())
                .authorities(new SimpleGrantedAuthority(member.getRole().toString()))
                .build();
    }
}

위에서 얘기했다시피 UserDetailsService는 구현해줘야 한다. member 객체를 찾아서 User로 build 시켜주면

Spring security가 알아서 확인하는 것 같다. 자세한 사항은 더 알아봐야 할 것 같다.


✅ 테스트

이렇게만 해주면 일단 끝이다. 진짜 이렇게 간단한 내용을 며칠을 삽질을 했는지 모르겠다.

(혹시 모른다 끝이 아닐지도...)

 

⏹️ 로그인 실패 테스트

로그인 실패

잘못된 이메일을 입력하고 로그인을 클릭하면 위에 config에서 적용한 것처럼 param으로 error를 보내게 되고 thymeleaf에서 param을 잡아 해당 메세지를 출력해준다.

 

⏹️ 로그인 성공 테스트

로그인 성공

로그인을 성공하면 홈 화면으로 리다이렉트 되고 추후 설명할 내용이지만 thymeleaf에서 session에 name을 받아와

화면에 출력해준다.

 

⏹️ 로그아웃 테스트

로그아웃 성공

로그아웃 클릭 시 config에서 정의한 내용처럼 logout param을 넘겨주고 thymeleaf는 해당 param을 받아 메세지를 화면에 출력한다.


✅ Authorize 적용

해당 회원이 로그인 즉, 인증을 했는지 안했는지를 판단해야 한다. 위에서 보여준 security config에서

http.authorizeRequests()
                // 페이지 권한 설정
                .antMatchers("/member/**").hasRole("USER")
                .antMatchers("/board/**").hasRole("USER")
                .anyRequest().permitAll();

위와 같은 내용이 있는데 사실 필자처럼 role이 여러 개가 아니라 user 하나만 있다면 authenticated()를 사용해도 된다.

  • authenticated() - 인증된 사용자의 접근을 허용
  • permitAll() - 무조건 허용
  • denyAll() - 무조건 차단
  • anonymous() - 익명 사용자 허용
  • hasRole(String) - 사용자가 주어진 역할이 있다면 접근을 허용
  • hasAnyRole(String, ...) - 사용자가 주어진 역할 중 하나라도 있다면 허용

등등 많은 내용이 있다. 하지만 혹시 모를 admin 페이지를 생각해서 저렇게 적용해 놓았다.

 

⏹️ Role

package com.practice.board.domain;

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public enum Role {

    ROLE_USER("user");

    private String value;
}

role을 그냥 USER라고 해두면 security에서 인식을 하지 못하기 때문에 꼭 "ROLE_" 을 포함시켜줘야 한다.

ROLE_만 바라본다.

이것 때문에도 한참을 삽질했다...

role을 생성했다면 member entity에도 role을 추가해줘야 한다.

해당 내용은 생략.

 

⏹️ thymeleaf authorize

마지막으로 view page에서 권한에 따른 페이지 변화시킬 수 있다.

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" type="text/css" th:href="@{/css/style.css}">
    <title>Home</title>
</head>
<body>
<div class="container">
    <div sec:authorize="hasRole('USER')">
        <h2 sec:authentication="name">사용자</h2>
        <a href="/member/list">회원목록</a>
        <a href="/logout">로그아웃</a>
    </div>
    <div sec:authorize="!hasRole('USER')">
        <div th:if="${param.logout}">
            로그아웃 하셨습니다.
        </div>
        <a href="/login">로그인</a>
    </div>
</div>
</body>
</html>

간단하게 Home 화면을 예로 들자면 sec:authorize를 통해 인증된 사용자와 아닌 사용자를 구분하고

param으로 다른 메세지를 전달할 수도 있다. 자세한 내용은 thymeleaf 문법을 찾아보길 바란다.

 

https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html#introducing-thymeleaf

 

Tutorial: Using Thymeleaf

1 Introducing Thymeleaf 1.1 What is Thymeleaf? Thymeleaf is a modern server-side Java template engine for both web and standalone environments, capable of processing HTML, XML, JavaScript, CSS and even plain text. The main goal of Thymeleaf is to provide a

www.thymeleaf.org


✅ 끝

작성하고 보니 정리하면 20분도 안 걸리는 내용을 며칠 동안 팠다는 게.... 우울하다.

다른 사람들은 필자처럼 고생 안했으면 좋겠다 끝으로 Github.

 

https://github.com/Kimmingki/board

 

GitHub - Kimmingki/board: 강의만 듣다 때려치우지 말고 조금씩이라도 개발해보자...!!

강의만 듣다 때려치우지 말고 조금씩이라도 개발해보자...!! Contribute to Kimmingki/board development by creating an account on GitHub.

github.com