Digking's cave

My First Blog Project (14) : SpringSecurity 로그인 구현 본문

Spring/My First Blog Project

My First Blog Project (14) : SpringSecurity 로그인 구현

디깅 2022. 12. 21. 14:09
728x90

loginForm.java

<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>

<%@ include file="../layout/header.jsp" %>

<div class="container">
    <form action="/auth/loginProc" 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="pwd">Password:</label>
        <input type="password" name="password" class="form-control" placeholder="Enter password" id="pwd">
      </div>
    <button id="btn-login" class="btn btn-primary">로그인</button>
    </form>

</div>

<%@ include file="../layout/footer.jsp" %>

<script src="/js/user.js"></script>을 지움

user.js에서도 login부분 지움 (login ajax연결이 필요없음 이제)

 

SecurityConfig.java

package com.cos.blog.config;

import com.cos.blog.config.auth.PrincipalDetailService;
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.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration //빈등록 (IoC관리)
@EnableWebSecurity //security 필터 등록
@EnableGlobalMethodSecurity(prePostEnabled = true) //특정 주소를 접근을 하면 권한 및 인증을 미리 체크하겠다는 뜻
public class SecurityConfig {

    private PrincipalDetailService principalDetailService;
    @Bean
    public BCryptPasswordEncoder encodePWD(){
        return new BCryptPasswordEncoder();
    }

    //시큐리티가 대신 로그인해주는데 password를 가로채기 하는데,
    //해당 password가 뭘로 해쉬가 되어 회원가입이 되었는지 알아야 같은 해쉬로 암호화해서 DB에 있는 해쉬랑 비교할 수 있음

    protected void configure(AuthenticationManagerBuilder auth) throws Exception{
        auth.userDetailsService(principalDetailService).passwordEncoder(encodePWD());
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf().disable() //csrf 토큰 비활성화(테스트 시에는 걸어두는게 좋다)
                .authorizeRequests()
                   .antMatchers("/","/auth/**","/js/**","/css/**","/image/**")
                     .permitAll()
                    .anyRequest()
                  .authenticated()
                .and()
                    .formLogin()
                    .loginPage("/auth/loginForm")
                    .loginProcessingUrl("/auth/loginProc") //springSecurity 가 해당 주소로 요청오는 로그인을 가로채서 대신 로그인
                    .defaultSuccessUrl("/"); //정상적으로 로그인 되었을 때는 기본주소로 이동
        return http.build();
    }

}

 

.andMatchers() 에 설정된 주소를 제외하고 나머지 주소는 모두 loginPage로 이동한다.

loginPage에서 로그인을 시도하면 SrpingSecurity가 loginProceessingUrl로 요청오는 로그인을 가로채서 대신 로그인 한다.

정상적으로 로그인이 되었을 경우에는 defaultSuccessUrl에 설정된 기본주소로 이동한다.

 

+ 실패했을 때 이동할 페이지를 .failureUlr()로 설정할 수 있다.

 

config/auth 패키지 생성

auth/PrincipalDetail.java 

package com.cos.blog.config.auth;

import com.cos.blog.model.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;

// 스프링시큐리티가 로그인 요청을 가로채서 로그인을 진행하고 완료가 되면 
// UserDetails 타입의 오브젝트를 스프링 시큐리티의 고유한 세션저장소에 저장해준다. 
// (이때 PrincipalDetail 이 저장된다)
public class PrincipalDetail implements UserDetails {
    private User user; //객체를 품고 있는 것을 콤포지션 이라고 한다.

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

    @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;
    }

    //계정이 갖고있는 권한 목록을 리턴한다. (권한이 여러개 있을 수 있어서 루프를 돌아야 하는데 우리는 한개만)
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collectors = new ArrayList<>();

        //화살표 함수로 바로 함수를 넣을 수 있다.
        collectors.add(()->{return "ROLE_"+user.getRoles(); });
        return collectors;
    }
}

 

- 객체를 품고 있는 것을 콤포지션 이라고 한다.

- 스프링시큐리티가 로그인 요청을 가로채서 로그인을 진행하고 완료가 되면 UserDetails 타입의 오브젝트를 스프링 시큐리티의 고유한 세션저장소에 저장해준다. (이때 PrincipalDetail 이 저장된다)

    //계정의 권한을 리턴
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collectors = new ArrayList<>();

        //자바에서는 함수를 넣을 수 없기 때문에 object로 담아서 넣어준다.
        collectors.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return "ROLE_"+user.getRoles();
                // 앞에 "ROLE_" 붙이는게 스프링 role이다.
            }
        });


        //화살표 함수로 바로 함수를 넣을 수 있다.
        collectors.add(()->{return "ROLE_"+user.getRoles()});
        return collectors;
    }

- 자바에서는 함수를 넣을 수 없기 때문에 object로 담아서 넣어주는데, 더 편하게 화살표함수로 바로 함수를 넣어줄 수 있다.

 

auth/PrincipalDetailService.java

package com.cos.blog.config.auth;

import com.cos.blog.model.User;
import com.cos.blog.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
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;

@Service
public class PrincipalDetailService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    // 스프링이 로그인 요청을 가로챌 때, username과 password 변수 2개를 가로채는데 password는 알아서 처리함
    // 우리는 username이 DB에 있는지만 확인해주면 된다.
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User principal = userRepository.findByUsername(username)
                .orElseThrow(()->{
                    return new UsernameNotFoundException("해당 사용자를 찾을 수 없습니다.");
                });
        //바로 user타입의 principal을 return할 수 없고, PrincipalDetail을 새로 생성해서 return
        return new PrincipalDetail(principal); // 이 때 시큐리티의 세션에 유저 정보가 저장된다
    }
}

 

 

로그인요청이 오는 순간 SecurityConfig에 .loginProcessingUrl의 주소로 시큐리티가 가로채서

가로챈 username을 PrincipalDetailService에 loadUserByUsername으로 던짐

 

SecurityConfig에 configure에서 우리가 등록한 auth.userDetailsService을 통해서 principalDetailService 가 로그인 요청을 하고

loadUserByUsername에서 username을 비교해서 return할 때 passwordEncoder(encodePWD()) 를 통해서 password 암호화한 후 DB랑 비교한다.

비교랑 끝나서 name, password가 정상인걸 확인하면 

PrincipalDetail(principal) 로 감싸서 다시 return 

 

완료되고나면 "/" 기본주소로 이동

이 때는 이미 세션이 만들어진 후이다.

 

BoardController controller에서 세션을 어떻게 찾나요?

반응형