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

Chapter 09. 관리자 로그인 구현

계란💕 2022. 8. 20. 15:40

  Ex) 초기 설정

    - 먼저, 관리자 페이지를 구성한다. ex)admin,backoffice, cms가 들어간다. http://admin.fastlms.co.kr

    - 서브 도메인을 따로 둘 수도 있다.  http://www.fastlms.co.kr/admin

    - adminService를 만든다. (member쪽을 만들었듯이)

      -> AdminMainController

<hide/>
package com.zerobase.fastlms.admin;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class AdminMainController {

    @GetMapping("/admin/main.do")
    public String main(){
        return "admin/main";
    }
}
<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:replace="/fragments/layout.html :: fragment-body-menu" ></div>-->

</body>
</html>

    - Security 클래스에 permitAll()로 등록되어 있지 않아서 로그인한 다음에야 admin/main 페이지에 접속이 가능하다.

 

 

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

    - 오류: 로그인 안해도 바로 관리자 화면이 뜬다.

    - 원인: ?

<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:replace="/fragments/layout.html :: fragment-body-menu" ></div>-->
   <div>
       <a href="/admin/member/list.do">회원 관리</a>
       |
       <a href="#">카테고리 관리</a>
       |
       <a href="#">강의 관리</a>
       <br>
   </div>
</body>
</html>

 

    - AdminMemberController

<hide/>
package com.zerobase.fastlms.admin;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class AdminMemberController {

    @GetMapping("/admin/member/list.do")
    public String list(){
        return "admin/member/list";
    }
}

 

    - list 파일 만든다.

<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:replace="/fragments/layout.html :: fragment-body-menu" ></div>-->
   <div>
       <a href="/admin/main.do">관리자 메인</a>
       |
       <a href="/admin/member/list.do">회원 관리</a>
       |
       <a href="#">카테고리 관리</a>
       |
       <a href="#">강의 관리</a>
       |
       <a href="/member/logout">로그 아웃</a>

       <br>
   </div>
</body>
</html>

 

    - 위 화면에서 로그아웃 누르면?

 

 

    Ex) 관리자 권한과 사용자 권한을 구분하려면?

     - Entity에 필드 추가 - 관리자인지 회원인지 여부를 나타낸다.

<hide/>
package com.zerobase.fastlms.member.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Entity;
import javax.persistence.Id;
import java.time.LocalDateTime;
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data   // getter, setter
@Entity
public class Member {

    @Id
    private String userId;

    private String userName;
    private String phone;
    private String password;
    private LocalDateTime regDt;

    private boolean emailAuthYn;
    private LocalDateTime emailAuthDt;
    private String emailAuthKey;

    private String resetPasswordKey;
    private LocalDateTime resetPasswordLimitDt; //초기화 가능한 날짜

    // 관리자 여부를 지정할 것인지 여부
    // 회원에 따른 ROLE을 지정할 것인가?
    // 준회원 / 정회원 / 특별회원/ 관리자
    // ROLE_SEMI_USER, ROLE_USER, ROLE_SPECIAL_USER, ROLE_ADMIN
    //
    private boolean adminYn;    // 관리자 여부
}

 

    cf) yml 파일에 jpa 설정이 있기 때문에 엔티티에 데이터를 추가하면 DDL을 자동 생성하고 데이터베이스까지 저장한다.

      - 데이터베이스에 저장된 계정 중 하나를 관리자 user(true)로 만든다. 

 

 

 

 

  Ex) 회원 가입 후, 이메일이 활성화 되는 것은 한 번만 가능하도록 수정

    - Impl 클래스 

      -> 회원 가입했을 때 받은 회원 가입 메일의 링크를 들어가면 활성화 페이지가 보인다. 이는 한 번만 가능해야한다.

       -> 따라서 impl클래스를 수정한다. (여러 번 활성화되지 않도록한다.)

 

    - impl - emailAuth()메서드를 수정한다.

<hide/>
@Override
public boolean emailAuth(String uuid) { // 이메일 인증

    Optional<Member> optionalMember = memberRepository.findByEmailAuthKey(uuid);    // 있으면 Optional<Member> 가 리턴된다.
    if(!optionalMember.isPresent()){
        return false;
    }
    Member member = optionalMember.get();   // 존재하는 경우 멤버

    // 이미 활성화됐기 때문에 또 활성화할 필요없다.
    if(member.isEmailAuthYn()){
        return false;
    }
    member.setEmailAuthYn(true);
    member.setEmailAuthDt(LocalDateTime.now());
    memberRepository.save(member);
    return true;
}

  Note) 실행 결과

    - 아래처럼 실패라고 나와야 정상이다.

 

 

   Ex)

    - loadUserByUserName() 메서드에서 user가 관리자인 경우는?

      -> "ROLE_ADMIN"을 추가한다.

      -> SimpleGrantedAuthority("ROLE_USER"): GrantedAuthority 인터페이스를 구현한 클래스이다.

<hide/>
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {   // 실질적으로 이메일
    Optional<Member> optionalMember = memberRepository.findById(username);
    if(!optionalMember.isPresent()){
        throw new UsernameNotFoundException("회원 정보가 존재하지 않습니다.");
    }
    Member member = optionalMember.get();
    if(!member.isEmailAuthYn()){
        throw new MemberNotEmailAuthException("이메일 활성화 이후에 로그인 해주세요");
    }
    List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
    grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_USER"));

    // 추가 - 관리자 ROLE
    if(member.isAdminYn()){
        grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
    }
    return new User(member.getUserId(), member.getPassword(), grantedAuthorities);  // role
}

 

 

    - SecurityConfig에 관리자 권한 관련해서 내용을 추가한다.

      -> AntPathRequestMatcher(String pattern): 대소문자를 구분하지 않고 모든 http 메서드와 일치하는 특정 패턴으로 Matcher을 만든다.

<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.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;
@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"
                        , "/member/find-password"
//                        , "/member/reset/password"
//                        , "/admin"
                )
                .permitAll();
        
        // 추가 - 관리자 권한
        http.authorizeRequests()
                        .antMatchers("/admin/**")
                                .hasAnyAuthority("ROLE_ADMIN");
        
        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);
    }
}

    - 관리자가 아닌 일반 계정으로 로그인

 

 

    - 다음과 같이 관리자 페이지를 들어가려고 시도하면 오류가 난다. => 추가 작업을 한다.

 

    - 관리자 계정 접속: 관리자 메인 화면 접속 가능

 

    - 일반계정 처리 작업

      -> SecutiryConfig 클래스 수정 

      -> logoutSuccessUrl("/"): 로그아웃을 성공하면 메인 페이지로 이동한다.

       -> invalidateHttpSession(true): 로그아웃 했으니까 세션을 초기화한다. 

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

    http.csrf().disable();      // csrf: 토큰
    http.authorizeRequests()
            .antMatchers(
                    "/"
                    , "/member/register"
                    , "/member/email-auth"
                    , "/member/find-password"
//                        , "/member/reset/password"
//                        , "/admin"
            )
            .permitAll();
    http.authorizeRequests()
                    .antMatchers("/admin/**")
                            .hasAnyAuthority("ROLE_ADMIN");
    http.formLogin()
            .loginPage("/member/login")
            .failureHandler(getFailureHandler())
            .permitAll();
    http.logout()
            .logoutRequestMatcher(new AntPathRequestMatcher("/member/logout"))
            .logoutSuccessUrl("/")          // 로그아웃 성공하면 메인페이지 이동
            .invalidateHttpSession(true);   // 로그아웃했으니 새션 초기화

    // 추가 - 접근 제한
    http.exceptionHandling()
            .accessDeniedPage("/error/denied");
    super.configure(http);
}

 

    - 메인 컨트롤러에 추가

<hide/>
@RequestMapping("/error/denied")
public String errorDenied() {
    return "error/denied";    
}

 

    - denied 파일 추가한다.

<hide/>
<!doctype html>
<html lang="ko"  xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>에러 페이지</title>
</head>
<body>
   <h1>에러 페이지</h1>
   <p>접근 권한이 없습니다.</p>
    <div>
        <a href="/" >홈으로 이동</a>
    </div>
</body>
</html>

  Note) 회원이 관리자 페이지에 접근하는 경우 - 실행 결과