[SpringSecurity] 회원가입, 로그인 기능

유알·2022년 12월 17일
0

[SpringSecurity]

목록 보기
1/15

유튜브 채널 "메타코딩"님의 스프링 부트 시큐리티를 보고 배운 내용을 정리하였습니다.

dependencies

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'com.mysql:mysql-connector-j'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.security:spring-security-test'
}

회원가입 기능

회원가입 기능은 일반적인 DB에 저장하는 회원가입과 크게 다르지 않다.
하지만 암호화 부분은 조금 다르다.

User.java

Jpa를 사용하여 유저 정보를 관리한다.

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;

import java.sql.Timestamp;

@Entity
@Data
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;
    private String username;
    private String password;
    private String email;
    private String role;
    @CreationTimestamp
    private Timestamp createDate;
}

UserRepository.java

이 부분은 내가 JPA를 공부하지 않아서 조금 헤메었다.
특이하게 interface를 @Repository를 붙여서 사용한다.
JpaRepository를 상속받아서 구현하는데, 기본적이 CRUD기능이 들어있다.
아래의 메서드는 로그인 때 사용할 메서드를 추가시켰다.

@Repository
public interface UserRepository extends JpaRepository<User,Integer> {

    //select * from user where username = ?
    public User findByUsername(String username); //Jpa Query Method

}

IndexController.java

다른 부분은 특이할 것이 없고, 특이한 점은 암호화 부분이다.
여기서는 BCryptPasswordEncoder를 주입 받아서 사용했다.
(Configuration에서 @Bean등록 예정)

@Controller
public class IndexController {
	@Autowired
    private UserRepository userRepository;

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;
    
	//...생략...
    
    @PostMapping("join")
    public String join(User user){
        System.out.println(user);
        user.setRole("ROLE_USER");
        String rawPassword = user.getPassword();
        String encPassword = bCryptPasswordEncoder.encode(rawPassword);
        user.setPassword(encPassword);
        userRepository.save(user);
        return "redirect:/loginForm";
    }


    @GetMapping("/join")
    public String join(){
        return "join";
    }
}

로그인 기능

오늘의 핵심이 이곳에 담겨있다.

loginForm.html

기본적으로 name에 username password로 되어있어야 spring security가 잘 불러와서 인증을 진행한다. 하지만 다른것으로 할경우 conriguration에서 .usernameParameter()과 같이 따로 설정해주어야한다.

    <form action="/login" method="POST">
        <input type="text" name="username" placeholder="Username"/><br/>
        <input type="password" name="password" placeholder="Password"/><br/>
        <button>로그인</button>        
    </form>

SecurityConfig.java

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

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public BCryptPasswordEncoder encoderPwd() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        http.csrf().disable();
        http.authorizeHttpRequests()
                .requestMatchers("/user/**").authenticated() //인증만 되면 들어갈 수 있는 주소
                .requestMatchers("/manager/**").hasAnyRole("ADMIN", "MANAGER")
                //hasAnyRole : 역할중 하나 가지고 있어야 허용
                .requestMatchers("/admin/**").hasRole("ADMIN")// 이 역할 필요
                .anyRequest().permitAll() //모두 허용
                .and() //return HttpSecurity
                .formLogin() //return FormLoginConfigurer <HttpSecurity>
                //.usernameParameter("username") 기본값은 username임
                .loginPage("/loginForm") //로그인 페이지 지정(Controller)
                .loginProcessingUrl("/login")
                //Url이 오면 가로채서 UserDetailsService의 loadUserByname 메서드로 연결
                .defaultSuccessUrl("/");
                //로그인 성공시 url
        return http.build();
    }
}

spring security를 설정하는 부분이다.

deprecated

강의에서는 아래와 같이 설정했다.

public class SecurityConfig extends WebSecurityConfigurerAdapter{
	@Override
    protected void configure(HttpSecurity http) throws Exception{
    	//...
    }
}

하지만 WebSecurityConfigurerAdapter는 @deprecate되었고 현재는 사라졌다.
그래서 위와 같이 @Bean으로서 SecurityFilterChain을 return하는 메서드를 등록하여서 사용하여야한다.
링크 참조 : https://devlog-wjdrbs96.tistory.com/434

HttpSecurity

핵심은 HttpSecurity이다. 전형적인 Builder이다.
.build를 하면 SecurityFilterChain을 뱉어낸다.

이것은 특정한 Http요청에 대한 설정을 제공한다. 기본적으로는 모든 요청에 적용되나, requestMatcher(RequestMatcher)로 대상을 제한 할 수 있다.
만약 허용되지 않은 유저가 접근시 로그인 페이지로 보낸다.

주요 기능

requestMatcher : 특정 요청에 대해 설정
authenticated : 인증된 사용자는 접속허용
hasAnyRole, hasRole : 특정 역할이 있어야 접속허용 (여러개 한개 차이)
anyRequest : 나머지 모든 요청에 대한 설정
permitAll : 모두 접속 허용
and : 다시 HttpSecurity를 반환
formLogin : 로그인 설정
loginPage : 로그인 페이지 지정 (controller 매핑한 주소)
loginProcessionUrl : 이 Url이 오면 가로채서 UserDetailsService를 구현한 @Bean의 loadUserByname 메서드로 보낸다.
defaltSuccessUrl : 로그인 성공시 갈 주소
usernameParameter : form 태그에서 전송시 input 태그 이름. 기본값은 "username"

작동 방식

즉 이 설정은 다음과 같은 기능이 있다.
1. 로그인 url이 오면 가로채서 설정한 로그인 페이지로 보냄
2. 로그인 요청이 전송되면(loginProcessionUrl 에 설정한 주소로) UserDetailServiceloadUserByname 메서드로 보낸다.
3. 요청별로 권한을 확인하고, 권한이 없으면 로그인 페이지로 보낸다.

자 그러면 로그인 권한을 체크하는 UserDetailService를 살펴보자.

PrincipalDetailService.java

UserDetailsService 인터페이스를 구현한 클래스 이다.
로그인 요청이 들어오면 이 인터페이스를 구현한 클래스로 연결된다.
아래를 보면 인터페이스를 구현하면서 loadUserByname 메서드를 구현하였다.

import com.cos.security1.model.User;
import com.cos.security1.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;

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

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        System.out.println("username : " + username);
        User userEntity = userRepository.findByUsername(username);
        if(userEntity != null){
            return new PrincipalDetail(userEntity);
        }
        return null;
    }
}

loadUserByname 메서드

이 메서드는 반환 타입이 UserDetails이다. (인터페이스)
이는 바로 다음 나온다. Db에서 User 객체를 뽑아서 구현체인 PrincipalDetails의 생성자에 넣어준다.

PrincipalDetails.java

이 클래스는 위에서 말했다싶이 UserDetails의 구현체이다.

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

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

// 시큐리티가 /login 요청이 오면 낚아채서 로그인 진행
// session <= Authentication 객체 <= UserDetails(interface) <implement= 이 객체
// PrincipalDetailsService 는 Au
public class PrincipalDetail implements UserDetails {

    private User user;

    public PrincipalDetail(User user) {
        this.user = user;
    }
	
    //사용자에서 부여된 권한을 반환한다.
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collect = new ArrayList<>();
        collect.add(new GrantedAuthority() { //GrantedAuthority를 Collection안에 담기
            @Override
            public String getAuthority() {
                return user.getRole(); //여기서 역할 뽑기
            }
        });
        return collect;
    }
	
    //비밀번호 가져오기
    @Override
    public String getPassword() {
        return user.getPassword();
    }
	
    //유저 이름 가져오기
    @Override
    public String getUsername() {
        return user.getUsername();
    }
	
    //계정만료 안됨?
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    
	//계정 안 잠김?
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    
	//계정 Credential 안 만료됨?
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
	
    //비활성화 되지 않음? (오랫동안 사용하지 않은 경우 등)
    @Override
    public boolean isEnabled() {
        return true;
    }
}

정리

security에서 관리하는 session에는 유저 정보가 담겨있어야 되는데 다음과 같이 층층이 감싸져있다.
session - Authentication - UserDetails(PrincipalDetail)
이거를 구현한 것이다.

호출되는 순서를 쉽게 생각해보면
1. 잘못된 접근
2. .loginPage("/loginForm") -> /loginForm
3. 작성후 /login POST
4. .loginProcessingUrl("/login") 이 가로채서 UserDetailsService 구현체의 loadUserByusername 메서드로 보냄
5. 이곳에서 new PrincipalDetailService() (UserDetailsService 구현체)을 리턴
6. PrincipalDetialsService가 PrincipalDetail 호출

profile
더 좋은 구조를 고민하는 개발자 입니다

0개의 댓글