Spring Projcect/학습 관리 시스템 & 백오피스 구축

Chapter 07. 스프링 부트 프로젝트 - 로그인/로그아웃

계란💕 2022. 8. 18. 12:28

 

  Ex) SecurityConfiguration

 

    - demo4 프로젝트 만든다. 종속성 Spring security 선택, pom 파일에서 복사해서

    - fastlms에 붙여넣기

    -애플리케이션 실행하면 시큐리티 패스워드가 뜬다.

 

  - 로컬 접속 - 위의 패스워드 입력해야 로그인이 가능하다.

 

    - 시큐리티 Config 클래스 추가, 주소에 대한 권한 설정

      -> antMachers:  '/' => 루트 페이지, '/**' => 루트 및 모든 페이지

      -> 로그인 없이 접속 가능해진다.

<hide/>
package com.zerobase.fastlms;
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.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
public class SecurityConfiguration  extends  WebSecurityConfigurerAdapter{
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        
        // 주소에 대한 권한 설정
        http.authorizeRequests()
                .antMatchers("/", "/**")
                .permitAll();
        super.configure(http);
    }
}

 

===============================오류===================================

  - 오류: 로그아웃 다음에 다시 로컬 접속하면 바로 메인 페이지가 떠야하는데 로그인 페이지가 뜬다.

  - 원인:  http.authorizeHttpRequests()  가 아니라 http.authorizeRequests()로 고친다. 

 

   - 실행 구성 설정

 

    - 아래 처럼 실행 / 디버그 설정한다.

 

    - 메인 페이지 - 회원 정보 페이지 만든다.

 

    - 컨트롤러 내용 추가

<hide/>
    @GetMapping("/member/info")
    public String memberInfo(){
        return "member/member_info";
    }

 

    - 멤버인포 html 파일 추가

<hide/>
<!doctype html>
<html lang="ko"  xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>회원 정보</title>
</head>
<body>
   <h1>회원 정보</h1>
</body>
</html>

 

    - SeucurityConfig 에 루트 페이지, 회원 가입 페이지, 계정 활성화 페이지 세 개는 로그인 없이 접근 되도록 만든다.

<hide/>
package com.zerobase.fastlms;
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.WebSecurityConfigurerAdapter;

@EnableWebSecurity
@Configuration
public class SecurityConfiguration  extends  WebSecurityConfigurerAdapter{

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        // 주소에 대한 권한 설정
        http.authorizeRequests()
                .antMatchers("/",
                                        "/member/register",
                                        "/member/email-auth")
                .permitAll();
        super.configure(http);
    }
}

 

    - 로그인 페이지도 설정 가능하다.

      -> failureHandler(null): 

 

    - SimpleUrlAuthenticationFailureHandler를 상속받는 클래스도 만든다.

<hide/>
package com.zerobase.fastlms.configuration;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class UserAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {

        // 에러 발생한 경우
        setUseForward(true);
        setDefaultFailureUrl("/member/login?error=true");
        request.setAttribute("errorMessage", "로그인에 실패했습니다.");
        super.onAuthenticationFailure(request, response, exception);
    }
}

 

     - 시큐리티 컨피그 

      -> 메서드 get 추가

      -> 상속해야 userDetailsService()에 멤버서비스를 넣었을 때 오류가 안 난다.

<hide/>
package com.zerobase.fastlms.configuration;
import com.zerobase.fastlms.member.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@RequiredArgsConstructor
@EnableWebSecurity
@Configuration
public class SecurityConfiguration  extends  WebSecurityConfigurerAdapter{
    private  final MemberService memberService;

    @Bean
    UserAuthenticationFailureHandler getFailureHandler(){
        return  new UserAuthenticationFailureHandler();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        // 주소에 대한 권한 설정
        http.authorizeHttpRequests()
                .antMatchers("/",
                                        "/member/register",
                                        "/member/email-auth")
                .permitAll();

        http.formLogin()
                .loginPage("/member/login")
                .failureHandler(null)
                .permitAll();
        super.configure(http);
    }

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

 

    - Impl 클래스 

<hide/>
package com.zerobase.fastlms.member.service.impl;
import com.zerobase.fastlms.component.MailComponents;
import com.zerobase.fastlms.member.MemberRepository;
import com.zerobase.fastlms.member.entity.Member;
import com.zerobase.fastlms.member.model.MemberInput;
import com.zerobase.fastlms.member.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
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.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

@Service
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;
    private final MailComponents mailComponents;

    @Override
    public boolean register(MemberInput parameter) {

        Optional<Member>optionalMember =  memberRepository.findById(parameter.getUserId());

        if(optionalMember.isPresent()){ // 이미 아이디가 존재하면?
            return false;
        }

        // 회원가입입
        String uuid = UUID.randomUUID().toString();


        Member member = Member.builder()
                        .userId(parameter.getUserId())
                        .userName(parameter.getUserName())
                        .phone(parameter.getPhone())
                        .password(parameter.getPassword())
                        .regDt(LocalDateTime.now())
                        .emailAuthYn(false)
                        .emailAuthKey(uuid)
                        .build();
        memberRepository.save(member);

        // 메일 보내기
        String email = parameter.getUserId();
        String subject = "fastlms 사이트 가입을 축하드립니다.";
        String text = "<p>fastlms 사이트 가입을 축하드립니다.</p><p>아래 링크를 클릭하셔서 가입을 완료하세요.</p>" +
                      "<div><a target='_blank'   href='http://localhost:8080/member/email-auth?id=" + uuid +  "'> 가입완료 </a></div>";
        mailComponents.sendMail(email, subject, text);
        return true;
    }

    @Override
    public boolean emailAuth(String uuid) {

        Optional<Member> optionalMember = memberRepository.findByEmailAuthKey(uuid);    // 있으면 Optional<Member> 가 리턴된다.

        if(!optionalMember.isPresent()){
            return false;
        }
        Member member = optionalMember.get();   // 존재하는 경우 멤버
        member.setEmailAuthYn(true);
        member.setEmailAuthDt(LocalDateTime.now());
        memberRepository.save(member);
        return true;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {   // 실질적으로 이메일

        Optional<Member> optionalMember = memberRepository.findById(username);
        if(!optionalMember.isPresent()){
            throw new UsernameNotFoundException("회원 정보가 존재하지 않습니다.");
        }

        Member member = optionalMember.get();
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_USER"));

        return new User(member.getUserId(), member.getPassword(), grantedAuthorities);  // role
    }
}

 

     - 시큐리티 컨피그 클래스

      -> 패스워드 인코더를 받아서 auth에다가 주입해준다.

      -> 회원가입 할 때 패스워드를 지정해줘야한다.

<hide/>
package com.zerobase.fastlms.configuration;
import com.zerobase.fastlms.member.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@RequiredArgsConstructor
@EnableWebSecurity
@Configuration
public class SecurityConfiguration  extends  WebSecurityConfigurerAdapter{
    private  final MemberService memberService;

    @Bean
    PasswordEncoder getPasswordEncoder() {

        return new BCryptPasswordEncoder();
    }

    @Bean
    UserAuthenticationFailureHandler getFailureHandler(){
        return  new UserAuthenticationFailureHandler();
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        // 주소에 대한 권한 설정
        http.authorizeHttpRequests()
                .antMatchers("/",
                                        "/member/register",
                                        "/member/email-auth")
                .permitAll();

        http.formLogin()
                .loginPage("/member/login")
                .failureHandler(getFailureHandler())
                .permitAll();

        super.configure(http);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

        auth.userDetailsService(memberService)
                .passwordEncoder(getPasswordEncoder());
        super.configure(auth);
    }
}

 

    - 컨트롤러에서 로그인 페이지를 만든다. get 이랑 post 모두 받을 수 있도록 @RequestMapping

<hide/>
    @RequestMapping("/member/login")
    public String login(){
        return "member/login";
    }

 

  Ex)

   - 로그인 페이지

       -> username은 변수이름? 어디에서??

<hide/>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>회원 로그인</title>
</head>
<body>
  <h1>회원 로그인</h1>
    <form method="post">

        <div>
            <input type="text" name="username" placeholder="아이디(이메일)을 입력해주세요"/>
        </div>
        <div>
            <input type="password" name="password" placeholder="비밀번호 입력"/>
        </div>
        <div>
            <button type="submit" >로그인</button>
        </div>
    </form>
</body>
</html>

 

 

=================================== 오류 ====================================

    - 오류: 로컬 루트 페이지 접속하는데 로그인 페이지가 나온다.

    - 추측한 원인:  SecurityConfiguration에서 failureHandler(getFailureHandler()) <= 매개변수를 안 넣어줬다. => 이게 문제 가 아니었다.

      -> SecurityConfiguration에 메서드를 잘못 넣었다. (위에 씀)

 

    - SecurityConfig

     -> csrf()를 넣으면 보안적 이슈 있으나 작동하지 않도록 잠시 넣는다.

<hide/>
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.csrf().disable();
        // 주소에 대한 권한 설정
        http.authorizeHttpRequests()
                .antMatchers("/",
                                        "/member/register",
                                        "/member/email-auth")
                .permitAll();

        http.formLogin()
                .loginPage("/member/login")
                .failureHandler(null)
                .permitAll();

        super.configure(http);
    }

 

<재부팅>

=========================오류========================

    - 오류:  로그인 정보를 입력하면 강사님처럼 request method 'POST' not supported가 나와야되는데 안 나온다.

    - 추측한 원인:  Sec Config에서 failureHandler(getFailureHandler()) <= 매개변수를 안 넣어줬다.

 

  - UserAuth ,.,,,  추가

<hide/>
 @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {

        // 에러 발생한 경우
        setUseForward(true);
        setDefaultFailureUrl("/member/login?error=true");
        request.setAttribute("errorMessage", "로그인에 실패했습니다.");
        System.out.println("로그인에 실패했습니다.");
        super.onAuthenticationFailure(request, response, exception);
    }

 

    - 로그인 페이지

      -> 여디서 "errorMessage"라는 이름의 변수는 로그인 페이지가 변수를 넘겨받는건가? 어디서?

<hide/>
<!DOCTYPE html>
<html lang="ko"  xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>회원 로그인</title>
</head>
<body>
  <h1>회원 로그인</h1>

    <div th:text="${errorMessage}">
    </div>

    <form method="post">
        <div>
            <input type="text" name="username" placeholder="아이디(이메일)을 입력해주세요"/>
        </div>
        <div>
            <input type="password" name="password" placeholder="비밀번호 입력"/>
        </div>
        <div>
            <button type="submit" >로그인</button>
        </div>
<!--    </form>-->
</body>
</html>

 

    - 시큐리티 컨피그에 로그아웃 내용 추가

    -> 따로 html 파일을 넣지 않아도 세션을 초기화하고 루트 페이지로 돌아가게끔 만든다. ("/member/logout")

      -> index 페이지에서 logout 이 걸려있기 때문이다. <a = href>

<hide/>
package com.zerobase.fastlms.configuration;
import com.zerobase.fastlms.member.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import static org.springframework.security.authorization.AuthenticatedAuthorizationManager.authenticated;

@RequiredArgsConstructor
@EnableWebSecurity
@Configuration
public class SecurityConfiguration  extends  WebSecurityConfigurerAdapter{
    private  final MemberService memberService;

    @Bean
    PasswordEncoder getPasswordEncoder() {

        return new BCryptPasswordEncoder();
    }

    @Bean
    UserAuthenticationFailureHandler getFailureHandler(){
        return  new UserAuthenticationFailureHandler();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {


        http.csrf().disable();      // csrf: 토큰

        // 주소에 대한 권한 설정
        http.authorizeRequests()
                .antMatchers("/",
                                        "/member/register",
                                        "/member/email-auth")
                .permitAll();

        http.formLogin()
                .loginPage("/member/login")
                .failureHandler(getFailureHandler())
                .permitAll();

        http.logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/member/logout"))
                .logoutSuccessUrl("/")          // 로그아웃 성공하면 메인페이지 이동
                .invalidateHttpSession(true);   // 로그아웃했으니 새션 초기화

        super.configure(http);
    }

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

 

    - 로그아웃 눌렀을 때 화면  - 확인하기

 

 

    Ex) 이메일 인증을 완료한 사람만 로그인 가능하도록 하려면??

 

    - 다음과 같이 인증받지 않은 계정도 있다.

 

    - 예외도 새롭게 추가한다.

     -> 런타임 을 상속받아야 문제 없다. 

<hide/>
package com.zerobase.fastlms.member.exception;
public class MemberNotEmailAuthException extends RuntimeException {
    public MemberNotEmailAuthException(String error) {
        super(error);
   }
}

 

    - 그럼 failHandler 쪽으로 예외가 떨어진다.

 

    - 회원 로그인 창에서 이메일 인증 받지 않은 정보를 입력하면?

 

    - 디버깅 모드

      -> InternalAuthenticationServiceException 발생한다.

 

   - UserAuth

    -> 예외 이름을 알았으니 넣어준다.

<hide/>
package com.zerobase.fastlms.configuration;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class UserAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {

        String msg = "로그인에 실패했습니다.";

        if(exception instanceof InternalAuthenticationServiceException){
            msg = exception.getMessage();
        }

        // 에러 발생한 경우
        setUseForward(true);
        setDefaultFailureUrl("/member/login?error=true");
        request.setAttribute("errorMessage", msg);   // html 파일에다가? 변수로?

        System.out.println("로그인에 실패했습니다.");
        super.onAuthenticationFailure(request, response, exception);
    }
}

 

    - 인증 받지 않은 이메일로 로그인을 시도하면?

    - 이메일 로그인해서 이메일 인증을 해준다.

 

    -활성화 되면 활성화 되었다는 알림창이 뜬다.