쌓고 쌓다

Spring Security 로그인 본문

프로그래밍/spring

Spring Security 로그인

승민아 2024. 1. 16. 13:08

SecurityConfig

package com.example.spotserver.config;

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.configurers.AbstractHttpConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {


    // 해당 메서드의 리턴되는 오브젝트를 IoC로 등록해준다.
    @Bean
    public BCryptPasswordEncoder encodePwd() {
        return new BCryptPasswordEncoder();
    }


    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable);

        http.authorizeHttpRequests(request ->
                        request
                                .requestMatchers("/user/**").authenticated() // 인증만 된다면 들어갈 수 있는 주소
                                .requestMatchers("/manager/**").hasAnyRole("ADMIN", "MANAGER")
                                .requestMatchers("/admin/**").hasAnyRole("ADMIN")
                                .anyRequest().permitAll())
                .formLogin(formLogin -> formLogin
                        .usernameParameter("name")
                        .loginPage("/loginForm")
                        .defaultSuccessUrl("/")
                        .loginProcessingUrl("/login") // "/login" 요청시 시큐리티가 낚아채 로그인을 진행해줌.

                        // loginForm에서 login을 왔다면 "/"로 보내고, 다른 페이지에서 로그인 왔다면 그 페이지로 리다이렉트 해줌.
                );

        return http.build();
    }

}

 

추가 및 변경된 사항은 다음과 같다.

  • usernameParameter("name") : 뒤의 내용들을 알아야 이해가 가는 부분이다.
    • UserDetailsSerivce의 loadUserByusername(String username)는 보면 파라미터가 username으로 되어 있다.
    • 로그인 폼에서 input 태그의 속성으로 name="username"를 해줘서 username으로 일치를 시켜야하지만
    • usernameParameter를 사용하여 로그인 폼에서 사용할 식별자를 변경할 수 있다.
  • loginProcessingUrl : 스프링 시큐리티에서 로그인 폼을 제출할때 사용할 URL을 설정한다. 해당 URL로 POST 요청을 보낸다.

 

loginForm.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>로그인 페이지</title>
</head>
<body>
<form action="/login" method="post">
  <input type="text" name="name" placeholder="name"> <br>
  <input type="password" name="password" placeholder="password"> <br>
  <button>로그인</button>
</form>

<a href="/joinForm">회원가입을 하지 않으셨나요?</a>

</body>
</html>

"/login"으로 POST 요청을 보낸다.

위에 SecurityConfig에서 loginProcessingUrl을 "/login"으로 했으므로 이 요청을 시큐리티가 가로챈다.

현재 name="name"으로 적어놨는데 시큐리티가 처리하는 기본값은 name="username"이다. 

usernameParameter("name")을 작성했기에 name="name"이 처리될 수 있는것이다.

 

다음의 규칙을 이해하자.

  1. "/login" 요청을 시큐리티가 낚아채어 로그인을 처리한다.
  2. 로그인 완료시 시큐리티 세션을 만들어준다.
  3. 이 세션에 들어갈 수 있는 오브젝트는 정해져 있으며 Authentication 객체여야한다.
  4. Authentication 안에는 유저 정보가 있어야한다.
  5. 유저 정보도 타입(오브젝트)가 정해져 있으며 UserDetails 객체여야한다.

=> 즉, 시큐리티 세션 ( Authentication ( UserDetails ) ) 구조이다.

 

 

로그인을 위해 다음의 UserDetails(PrincipalDetails.java) 클래스를 만들어 주자.

 

PrincipalDetails.java

package com.example.spotserver.config.auth;

import com.example.spotserver.securityStudy.TestUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

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

public class PrincipalDetails implements UserDetails {

    private TestUser testUser;


    public PrincipalDetails(TestUser testUser) {
        this.testUser = testUser;
    }



    // 해당 유저의 권한을 리턴하는 곳.
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {

        // 현재 TestUser의 권한은 String이라 타입을 맞춰줘야함
        Collection<GrantedAuthority> collection = new ArrayList<>();
        collection.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return testUser.getRole();
            }
        });
        return collection;
    }

    @Override
    public String getPassword() {
        return testUser.getPassword();
    }

    @Override
    public String getUsername() {
        return testUser.getName();
    }

    @Override
    public boolean isAccountNonExpired() {
        // 계정이 만료되었는지 여부를 판단하는 로직을 구현
        // 만료되었으면 false, 그렇지 않으면 true 반환
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        // 계정이 안잠겼니?
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        //isCredentialsNonExpired() 메소드는 계정의 자격 증명이 만료되었으면 false를 반환하며, 만료되지 않았으면 true를 반환합니다.
        return true;
    }

    @Override
    public boolean isEnabled() {
        // 계정이 활성화 되어 있니?
        // 이런것들 언제 false 하느냐? User 필드의 TimeStamp를 통해 휴면 계정을 관리하여
        // 여기에 getLoginDate를 가져와서 현재시간 - 로그인 시간이 1년을 넘으면 false로 반환해버리면 된다!
        return true;
    }
}

오버라이딩한 메서드에 대한 설명은 주석으로 모두 설명해두었다. 읽어보자~

 

PrincipalDetailsService.java (UserDetailsService)

package com.example.spotserver.config.auth;

import com.example.spotserver.securityStudy.TestUser;
import com.example.spotserver.securityStudy.TestUserRepository;
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;

// 시큐리티 설정에서 .loginProcessingUrl("/login") 했기에
// "/login" 요청이 오면 자동으로 UserDetailsService 타입으로 IoC 되어 있는 loadUserByUsername 가 실행된다.
@Service
public class PrincipalDetailsService implements UserDetailsService {

    private TestUserRepository testUserRepository;

    @Autowired
    public PrincipalDetailsService(TestUserRepository testUserRepository) {
        this.testUserRepository = testUserRepository;
    }



    // 이 메서드 반환값이 Authentication 내부에 쏙 UserDeatils가 들어감
    // 시큐리티 세션 = Authentication(내부 UserDetails)
    // 시큐리티 세션 ( 내부 Authentication(내부 UserDetails) )
    // loadUser... 애가 알아서 세션까지 다 넣어
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 파라미터가 username으로 넘어오는데 loginForm에서도 username으로 맞춰서 넘겨줘야함.
        // username이 아닌 name으로 넘기도록 난 해놨으니 매칭되도록 SecurityConfig에서 .usernameParameter("name") 으로 해준거임.

        TestUser testUser = testUserRepository.findByName(username);
        if(testUser != null) {
            return new PrincipalDetails(testUser);
        }

        return null;
    }
}

loadUserByUsername의 파라미터명이 username이다. 그래서 로그인 폼에서 name="username"이 기본값이고 이 값으로 매칭을 시켜줘야하지만 usernameParameter("name")으로 변경해주었기에 name="name"으로 폼에서 넘겨줄 수 있었던 것이다.

 

username으로만 정보를 찾고 비밀번호는 어디서 대조하는것인가?

코드상 username만 비교하고 비밀번호를 대조하는 부분은 보이지 않아 비밀번호 처리는 어디서 이뤄지는지 궁금했다.

마침 비슷한 질문한 사람이 있어서 링크를 남겨놓는다.!

https://www.inflearn.com/questions/661129/loaduserbyusername-%EB%A9%94%EC%84%9C%EB%93%9C-%EC%A7%88%EB%AC%B8-%EC%9E%88%EC%8A%B5%EB%8B%88%EB%8B%A4

'프로그래밍 > spring' 카테고리의 다른 글

Spring Security 필터  (0) 2024.01.17
Spring Security 권한처리  (0) 2024.01.16
Spring Security 회원가입 및 BCrypt  (0) 2024.01.15
Spring Security 설정 및 로그인 페이지로 보내기  (1) 2024.01.15
Resource, UrlResource?  (1) 2024.01.02
Comments