시작이 반

[Spring] 스프링 시큐리티 로그인 본문

Programming/Spring

[Spring] 스프링 시큐리티 로그인

G_Gi 2021. 2. 14. 21:05
SMALL

Spring Security란?

Spring Security는 Spring기반의 어플리케이션의 보안(인증과 권한(을 담당하는 프레임워크이다.

Spring Security는 Filter기반으로 동작한다.

Filter는 Dispatcher Servlet으로 가기전에 적용되므로 가장 먼저 URL의 요청을 받지만, Interceptor는 Dispatcher와 Controller사이에 위치한다.

 

  • 인증(Authentication): 해당 사용자가 본인이 맞는지를 확인하는 절차
  • 인가(Authorization): 인증된 사용자가 요청한 자원에 접근 가능한지를 결정하는 절차 
  • Principal(접근 주체): 보호받는 Resource에 접근하는 대상
  • Credential(비밀번호): Resource에 접근하는 대상의 비밀번호

Http Body에 username과 password를 가져오면

AuthenticationFilter를 거쳐 username과 password로 UsernamePasswordAuthenticationToken을 만든다.

AuthenticationManager가 토큰을 받아서 Authentication 객체를 만든다.

Authentication을 어떻게 만드냐면 UsernamePasswordAuthenticationToken를 UserDetailsService로 던지고 username을 가지고 DB에서 해당 유저가 있는지 확인후 있으면 Authentication을 만든다.

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(principalDetailService).passwordEncoder(encodePWD());
    }
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User principal = userRepository.findByUsername(username)
                .orElseThrow(() ->{
                    return new UsernameNotFoundException("해당 사용자를 찾을수 없습니다.:" + username);
                });
        return new PrincipalDetail(principal); //시큐리티의 세션에 유저정보가 저장이됨. 
    }

 

 

Spring Security는 기본적으로 인증 절차를 거친 후에 인가 절차를 진행하게 되며, 인가 과정에서 해당 리소스에 대한 접근 권한이 있는지 확인을 하게 된다. Spring Security에서는 이러한 인증과 인가를 위해 Principal을 아이디로, Credential을 비밀번호로 사용

 

........................아키텍쳐 더 알아보기.....................

 

간단한 로그인 구현방법

gradle

Spring Boot, JPA, thymeleaf

 

 

 

Dependency

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'

	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	compile 'mysql:mysql-connector-java'
	compile "org.springframework.boot:spring-boot-devtools"
	compile group: 'org.thymeleaf.extras', name: 'thymeleaf-extras-springsecurity5', version: '3.0.4.RELEASE'
	compile "org.springframework.boot:spring-boot-starter-security" //시큐리티 쓰면 모든 문이 막힘 커스컴해줘야함
	testImplementation('org.springframework.boot:spring-boot-starter-test') {
		exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
	}
}

html에서

Thymeleaf, 스프링 시큐리티 사용

<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">

 

스프링 시큐리티 관련 Dependency를 작성하면 시큐리티가 자체적으로 모든 url을 막는다.

스프링 시큐리티가 가로채서 모든 url에 들어가면 로그인창이 뜬다. (내가 만든 페이지가 아님)

기본 세팅이 

username : user

password : 콘솔창에 출력된 비밀번호

로그인이 완료되면 자체적으로 세션이 생기게된다.

일단 이것으로 로그인 후 html에서 세션을 확인해보자.

 

            <ul sec:authorize="!isAuthenticated()" 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>
            <ul sec:authorize="isAuthenticated()" class="navbar-nav">
                <li class="nav-item">
                    <span class="nav-link" sec:authentication="principal.username"></span>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="/board/saveForm">글쓰기</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="/logout">로그아웃</a>
                </li>
            </ul>

sec:ausec:authorize="!isAuthenticated()" 으로 

세션이 인증이 됐는지 확인이 가능하다.

타임리프의 if문법과 비슷하다.

 

로그인을 해보면

 

user로 바뀐것을 볼 수 있다.

 

우리는 아이디와 비밀번호를 DB에 있는 값으로 확인해야하며, 로그인 페이지 또한 우리가 만든 로그인 페이지로 바꿔야한다.

 

만들 java파일들

 

 

 

로그인 페이지 커스터마이징

package Social.Sociallogin.config;

import Social.Sociallogin.config.auth.PrincipalDetailService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
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.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;


@EnableWebSecurity //시큐리티 필터가 등록
@EnableGlobalMethodSecurity(prePostEnabled = true) //특정 주소로 접근을 하면 권한 및 인증을 미리 체크
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private PrincipalDetailService principalDetailService;

    @Autowired
    public SecurityConfig(PrincipalDetailService principalDetailService) {
        this.principalDetailService = principalDetailService;
    }

    @Bean
    public BCryptPasswordEncoder encodePWD(){ //비밀번호 암호화를 위해 사용 시큐리티는 비밀번호가 암호화 되있어야 사용가능하다
        return new BCryptPasswordEncoder();   //회원가입할때 쓰면된다.
    }

    // 시큐리티가 대신 로그인해주는데 password를 가로채는데
    // 해당 password가 뭘로 해쉬화해서 회원가입이 되었는지 알아야
    // 같은 해쉬로 암호화해서 DB에 있는 해쉬랑 비교가능
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(principalDetailService).passwordEncoder(encodePWD());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()//csrf토큰 비활성화(테스트시 걸어두는게 좋음) 시큐리티는 csrf토큰이 있어야 접근가능함
                    .authorizeRequests() //인가 요청이 오면
					.antMatchers("/","/auth/**","/js/**","/css/**","/image/**") //해당 경로들은
					.permitAll() //접근을 허용한다.
					.anyRequest() //다른 모든 요청은
					.authenticated() //인증이 되야 들어갈 수 있다.
				.and() // 그리고
					.formLogin() //로그인 폼은
					.loginPage("/auth/loginForm") //로그인 페이지를 우리가 만든 페이지로 등록한다.
					.loginProcessingUrl("/auth/loginProc")//스프링 시큐리티가 해당 주소로 요청오는 로그인을 가로채서 대신 로그인해줌(서비스의 loadUserByName로 알아서)
					.defaultSuccessUrl("/"); //정상일떄

		//중복 로그인
        http.sessionManagement()
                .maximumSessions(1) //세션 최대 허용 수 
                .maxSessionsPreventsLogin(false); // false이면 중복 로그인하면 이전 로그인이 풀린다.

    }
}

 

 

 

로그인 페이지 .html

<div class="container">
    <form action="/auth/loginProc" method="post"> <!--컨트롤러에 해당 주소 맵핑 안만들고 시큐리티가 가로채게 할것-->
        <div class="form-group">
            <label for="username">Username:</label>
            <input type="text" class="form-control" name="username" placeholder="Enter username" id="username">
        </div>
        <div class="form-group">
            <label for="password">Password:</label>
            <input type="password" class="form-control" name="password" placeholder="Enter password" id="password">
        </div>
        <button id="btn-login" class="btn btn-primary">로그인</button>
    </form>
</div>

form태그 안의 input태그의 name을 보고 스프링 시큐리티가 가로챌 것이다.

ID : username 

PW : password

 

스프링 시큐리티는 로그인을 성공하면 세션이 만들어질텐데.

 

entity User 정보

package Social.Sociallogin.domain;


import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;

import javax.persistence.*;
import java.sql.Timestamp;

@Entity
@Data
@NoArgsConstructor
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @Column(nullable = false, length = 30, unique = true)
    private String username;

    @Column(nullable = false, length = 100)
    private String password;

    @CreationTimestamp
    private Timestamp createDate;
}

 

이 유저 정보를 세션에 등록해야한다. 

 

근데 세션에 등록되는 type은 UserDetails 이다.

 

UserDetails을 extends해서 새로운 class를 만든다.

package Social.Sociallogin.config.auth;

import Social.Sociallogin.domain.User;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

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

//로그인했을때 정보들
//스프링 시큐리티가 로그인 요청을 가로채서 로그인을 진행하고 완료가 되면 UserDetails타입의 오브젝트를
//스프링 시큐리티의 고유한 세션저장소에 저장을 해준다. PrincipalDetail
@Getter
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";
        });
        return collectors;
    }
}

 

세션에 등록될 UserDetails를 만들었다.

 

이후에는

로그인을 진행하는 Service를 만들어야함.

UserDetailsService를 implements한다.

package Social.Sociallogin.config.auth;

import Social.Sociallogin.domain.User;
import Social.Sociallogin.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 {

    private UserRepository userRepository;

    @Autowired
    public PrincipalDetailService(UserRepository userRepository) {
        this.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("해당 사용자를 찾을수 없습니다.:" + username);
                });
        return new PrincipalDetail(principal); //시큐리티의 세션에 유저정보가 저장이됨. (원래는 콘솔창에 뜨는 user, pw가 있었음)
    }
}

loadUserByUsername함수를 Override하여 DB에 접근해 username이 DB에 있는지 확인한다. password는 알아서 처리된다. 유저정보가 있으면 UserDetails를 구현한 class를 해당 user의 정보로 초기화해주고 리턴해준다. 이후에 시큐리티의 세션의 정보가 저장이된다.

 

 

  • 로그인 요청 ->
  • loginPage()로 이동 ->
  • loginProcessingUrl이 가로챔 (username, password정보) ->
  • UserDetailsService의 loadUserByUsername로 던진다 ->
  • DB와 username을 비교 후 UserDetails를 반환함 ->
  • ( UserDetails를 반환하기전에 WebSecurityConfigurerAdapter를 상속받은 클래스에서 configure() 함수에서 패스워드 비교가 이뤄짐) ->
  • 정상인것을 확인 됐으면 스프링 시큐리티 영역에 등록된다.
  • 로그인되면 defaultSuccessUrl로 이동함 
  • 이제 세션이 만들어졌음

확인

Main페이지

 

로그인 페이지
로그인 완료후 /로 이동

 

 

참고

 

LIST