ID/PW 방식의 로그인처리

프로젝트 생성과 의존성 추가

제일 처음으로 Spring-Boot프로젝트에 Spring-Security, MariaDB 드라이버,Thymeleaf 확장 플러그인을 추가해줌

다음으로 application.properties파일에 데이터 베이스와 JPA관련 설정과 시큐리티와 관련된 부분의 로그 레벨을 설정해줌

application.properties

spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://localhost:3306/bootex
spring.datasource.username=bootuser
spring.datasource.password=bootuser

spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.show-sql=true

spring.thymeleaf.cache=false

logging.level.org.springframework.security.web=trace
logging.level.com.crow=debug

이후 제대로 설정된건지 알아보기 위해서 프로젝트를 실행해서 로그에 출력된 자동으로 생성된 임시 패스워드와 user라는 이름으로 로그인을 해봄

로그인을 성공해도 현재 프로젝트 내부에 컨트롤러가 없기 때문에 Whitelabel Error Page가 나옴
(비밀번호를 틀릴시 자격 증명에 실패했다고 나옴)

http://localhost:8080/login

시큐리티 설정 클래스 작성

프로젝트 내 src/main/java/내패키지이름경로에 config 패키지를 추가해주고 SecurityConfig 클래스를 작성해줌

여기서 5.7.x버전은 이전 버전들과 다르게
WebSecurityConfigurerAdapter를 extends 받아서 override를 통해서 구현했지만

5.7이후 버전부턴 @EnableWebSecurity을 클래스에 선언해주고 @Bean을 메서드에 사용해서 구현해야함

SecurityConfig.Class

package com.crow.oauthex.config;

import lombok.extern.log4j.Log4j2;
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.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@Log4j2
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/sample/all").permitAll()
                .antMatchers("/sample/member").hasRole("USER");

        http.formLogin();
        http.csrf().disable();
        http.logout();

        return http.build();
    }

}

패스워드를 인코딩
가장 많이 사용되는BCryptPasswordEncoder클래스를 사용함
(bcrypt해쉬 사용 원래대로 복호화 불가능 매번 동일 길이의 다른 값이 설정됨)
스프링부트 2.0부턴 반드시 PasswordEncoder를 지정해야 함

특정한 URL 접근을 제한
SecurityFilterChain구현
HttpSecurity객체는 .을 이용해서 연속적 처리하는 빌더 방식을 이용함

http.authorizeRequests()로 인증이 필요한 자원을 설정할 수 있고,
.antMatchers()와 같은 앤트 스타일 패턴으로 원하는 자원을 선택해주면됨
.permitAll()은 모든 사용자들에게 접근이 가능하게 해줌

해당 코드는 /sample/all은 모든사용자가 접근 가능하고 /sample/memberUSER라는 롤을 가진 사용자만 접근이 가능하게 만듬

http.formLogin()은 인가/인증에 실패하면 로그인 페이지를 보여줌

시큐리티의 CSRF(크로스 사이트 요청 위조)공격 방식을 막기위해 발행한 토큰은 disable해주고
logout도 처리해줌
(CSRF 토큰은 보안상이점을 위해서 사용하는게 좋지만
이건 테스트 이기 때문에 사용하지 않음)


또한 시큐리티가 작동하는지 확인하기 위한 컨트롤러를 controller 패키지를 구성하고 클래스를 추가해줌

SampleController

package com.crow.oauthex.controller;

import com.crow.oauthex.security.dto.OAuthMemberDTO;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@Log4j2
@RequestMapping("/sample/")
public class SampleController {

    @GetMapping("/all")
    public void exAll() {
        log.info("exAll...............");
    }

    @GetMapping("/member")
    public void exMember(@AuthenticationPrincipal OAuthMemberDTO oAuthMemberDTO) {

        log.info("exMember...............");

    }

    @GetMapping("/admin")
    public void exAdmin() {
        log.info("exAdmin..................");
    }
}

JPA 처리

프로젝트에 스프링 시큐리티를 적용하기 위해서는 이에 맞는 데이터베이스 관련 처리가 필요함
com.crow.oauthex.entity패키지를 추가하고
OAuthMember.java OAuthMemberRole.java BaseEntity.java구현
각각 회원 정보/회원 권한/수정일,생성일 관리용도임

BaseEntity.Class

package com.crow.oauthex.entity;

import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

@MappedSuperclass
@EntityListeners(value = { AuditingEntityListener.class })
@Getter
abstract class BaseEntity {

    @CreatedDate
    @Column(name = "regdate" , updatable = false)
    private LocalDateTime regDate;

    @LastModifiedDate
    @Column(name ="moddate" )
    private LocalDateTime modDate;

}

@MappedSuperclass
@EntityListeners(value = { AuditingEntityListener.class })
이 부분에서 사용한 어노테이션들은 JPA 설계 공통 칼럼을 적용하여 다른 Entity에 상속해줌
(주로 생성일 수정일등 Date)

또한 BaseEntity.class를 이용하기 위해선 프로젝트 생성시 만들어진 OAuthExApplication.class
@EnableJpaAuditing 어노테이션을 추가해야함

OAuthExApplication

package com.crow.oauthex;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
@EnableJpaAuditing
public class OAuthExApplication {

    public static void main(String[] args) {
        SpringApplication.run(OAuthExApplication.class, args);
    }

}

OAuthMember.class

package com.crow.oauthex.entity;

import lombok.*;

import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import java.util.HashSet;
import java.util.Set;


@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString
public class OAuthMember extends BaseEntity {

    @Id
    private String email;

    private String password;

    private String name;

    private boolean fromSocial;

    @ElementCollection(fetch = FetchType.LAZY)
    @Builder.Default
    private Set<OAuthMemberRole> roleSet = new HashSet<>();

    public void addMemberRole(OAuthMemberRole OAuthMemberRole) {
        roleSet.add(OAuthMemberRole);
    }

}

BaseEntity를 상속 받아서 생성일 수정일적용
PK키는 소셜 로그인을 감안해서 이메일로 적용함
(요즘 많이 쓰는 트렌드임)
OAuthMemberRole타입 값을 처리하기 위한Set<OAuthMemberRole>타입을 추가하고
지연 로딩방식(fetch = FetchType.LAZY)사용

OAuthMemberRole

package com.crow.oauthex.entity;

public enum OAuthMemberRole {

    USER, MANAGER, ADMIN
}

유저 권한은 USER, MANAGER, ADMIN으로 설계함

이후 프로젝트가 실행되면
2개의 테이블이 생성됨

Repository 구현

OAuthMemberRepository.interface

package com.crow.oauthex.repository;

import com.crow.oauthex.entity.OAuthMember;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.Optional;

public interface OAuthMemberRepository extends JpaRepository<OAuthMember, String> {

    @EntityGraph(attributePaths = {"roleSet"}, type = EntityGraph.EntityGraphType.LOAD)
    @Query("select m from OAuthMember m where m.fromSocial = :social and m.email =:email")
    Optional<OAuthMember> findByEmail(String email, boolean social);
}

findByEmail()은 사용자의 이메일과 소셜로 추가된 회원 여부를 선택해서 동작하도록 설계

@EntityGraph을 이용해서 left outer join으로 OAuthMemberRole이 처리될 수 있도록함

OAuthMemberTests.java

package com.crow.oauthex.security;

import com.crow.oauthex.entity.OAuthMember;
import com.crow.oauthex.entity.OAuthMemberRole;
import com.crow.oauthex.repository.OAuthMemberRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.util.HashSet;
import java.util.Optional;
import java.util.stream.IntStream;

@SpringBootTest
public class OAuthMemberTests {

    @Autowired
    private OAuthMemberRepository repository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Test
    public void insertDummies() {

        //1 - 80까지는 USER만 지정
        //81- 90까지는 USER,MANAGER
        //91- 100까지는 USER,MANAGER,ADMIN

        IntStream.rangeClosed(1,100).forEach(i -> {
            OAuthMember oAuthMember = OAuthMember.builder()
                    .email("user"+i+"@test.com")
                    .name("사용자"+i)
                    .fromSocial(false)
                    .roleSet(new HashSet<OAuthMemberRole>())
                    .password(  passwordEncoder.encode("1111") )
                    .build();

            //default role
            oAuthMember.addMemberRole(OAuthMemberRole.USER);

            if(i > 80){
                oAuthMember.addMemberRole(OAuthMemberRole.MANAGER);
            }

            if(i > 90){
                oAuthMember.addMemberRole(OAuthMemberRole.ADMIN);
            }

            repository.save(oAuthMember);

        });

    }



    @Test
    public void testRead(){

        Optional<OAuthMember> result = repository.findByEmail("user95@test.com",false);

        OAuthMember OAuthMember = result.get();

        System.out.println(OAuthMember);
        OAuthMember.addMemberRole(OAuthMemberRole.USER);
    }
}

insertDummies()는 더미 데이터를 추가해서 제대로 롤이 구현됬는지 테스트하는 코드임

testRead()OAuthMemberRepository.interface
구현된 findByEmail()를 테스트하는 코드임
(일반 로그인 사용자와 소셜 로그인 사용자 구분)

Service 구현

JPA로 회원정보를 처리하는데 문제가 없다는 것을 확인했다면
다음으론 OAuthMemberRepository를 이용해서 회원 정보를 처리하는 부분을 구현해야함

일반 로그인 처리와 다르게 스프링 시큐리티는UserDetailsService를 상속받아
loadUserByUsername()를 오버라이딩 해서 사용함

여기서 크게 두가지 방식으로 나뉨
기존 DTO 클래스에 UserDetails 인터페이스를 구현하거나, DTO와 같은 개념으로 별도의 클래스를 구성하는 방식

이중 별도의 클래스를 구성하는 방식을 사용하겠음
프로젝트 내 security 패키지를 구성하고 내부에 dto, service 패키지를 구성

OAuthMemberDTO.class

package com.crow.oauthex.security.dto;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

import java.util.Collection;

@Log4j2
@Getter
@Setter
@ToString
public class OAuthMemberDTO extends User {

    private String email;

    private String name;

    private boolean fromSocial;

    public OAuthMemberDTO(String username,
                          String password,
                          boolean fromSocial,
                          Collection<? extends GrantedAuthority> authorities) {

        super(username, password, authorities);
        this.email = username;
        this.fromSocial = fromSocial;

    }

}

DTO를 구성하는 첫번째 단계는 User 클래스를 상속하고
부모 클래스인 User 클래스의 생성자를 호출할 수 있는 코드를 만드는것임
(부모 클래스인 User 클래스에 사용자 정의 생성자가 있으므로 반드시 호출 해야함)

AuthUserDetailsService

package com.crow.oauthex.security.service;

import com.crow.oauthex.entity.OAuthMember;
import com.crow.oauthex.repository.OAuthMemberRepository;
import com.crow.oauthex.security.dto.OAuthMemberDTO;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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;

import java.util.Optional;
import java.util.stream.Collectors;

@Log4j2
@Service
@RequiredArgsConstructor
public class AuthUserDetailsService implements UserDetailsService {

    private final OAuthMemberRepository OAuthMemberRepository;


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("-------------------");
        log.info("ClubUserDetailsService loadUserByUsername " + username);

        Optional<OAuthMember> result = OAuthMemberRepository.findByEmail(username, false);

        if (result.isEmpty()) {
            throw new UsernameNotFoundException("Check Email or social");
        }

        OAuthMember OAuthMember = result.get();

        log.info("-------------------");
        log.info(OAuthMember);
        log.info("-------------------");

        OAuthMemberDTO clubAuthMember = new OAuthMemberDTO(
                OAuthMember.getEmail(),
                OAuthMember.getPassword(),
                OAuthMember.isFromSocial(),
                OAuthMember.getRoleSet().stream()
                        .map(role -> new SimpleGrantedAuthority("ROLE_" + role.name()))
                        .collect(Collectors.toSet())
        );

        clubAuthMember.setName(OAuthMember.getName());
        clubAuthMember.setFromSocial(OAuthMember.isFromSocial());



        return clubAuthMember;
    }
}

loadUserByUsername()를 오버라이드 받아서
AuthUserDetailsServiceOAuthMemberRepository를 연동

username이 실제로는 Email을 의미하므로 이를 이용해서
findByEmail()호출 소셜 여부는 false임

사용자가 존재 하지 않는다면 UsernameNotFoundException처리

OAuthMemberUserDetails타입으로 처리 하기 위해서
OAuthMemberDTO타입으로 변환함

OAuthMemberRole은 시큐리티에서 사용하는 SimpleGrantedAuthority로 변환 이때 "ROLE_"이라는 접두어 추가해줌

이후 /sample/member에 접근해서 로그인 한다면 For Member라는 내용을 확인가능
(로그인은 아까 테스트에서 추가한 계정들로 해주면됨)





참조
코드로 배우는 스프링부트 웹프로젝트
스타트 스프링부트

후순위할일

페이지 다듬어서 h1태그별로 포스트하고 시리즈로 정리

profile
어제보다 개발 더 잘하기 / 많이 듣고 핵심만 정리해서 말하기 / 도망가지 말기 / 깃허브 위키 내용 가져오기

0개의 댓글