Spring/spring security

스프링 시큐리티 DB를 이용한 로그인 구현 1 ( 회원 설정 및 일반 로그인 ) - 삽질중인 개발자

개발 N년차 2020. 2. 16. 17:31
반응형

- spring security 로그인 (mariaDB) -

 

스프링 시큐리티

스프링 기반의 보안 관련 프레임워크이다.

스프링 시큐리티를 사용하면 보안에 관련된 옵션을 자동적으로 구현해주기에 우리가 어느정도 신경을 안써도 된다.

 

개발 환경

- spring boot 2.2.5.RELEASE

- jpa ( 이거 무조건 배우자 )

- mariaDB  ( JPA 사용하면 oracle , h2 기타 등등 모든 DB 가능 )

- jsp

 

jpa를 사용해서 DB를 다룰건데 jpa가 아닌 mybatis를 이용해서 구현할때 중간중간에 어떤 코드가 들어가야 하는지 써놨다.

 

이 포스팅에서는 우선 로그인을 시켜볼 것이다. 

 

하기전에 application.properties 에 가서 미리 설정하자. ( 한글로 써둔 부분은 알아서 채우자 )

server.servlet.context-path=/
server.port: 8090

spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp

spring.datasource.driverClassName=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://디비 아이피/디비명?characterEncoding=UTF-8
spring.datasource.username=유저명
spring.datasource.password=비번

spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=create
spring.jpa.generate-ddl=true

 

 

1. pom.xml 에 spring secuirty starter 추가하기

	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-security</artifactId>
	</dependency>

 

추가하고 run을 하면 로그 중간에 이런 로그가 찍힌다.  그러면 적용된 것이다.

 

그 후 아무 페이지( ex) localhost:8090/ ) 로 이동을 하면 자동으로 /login 페이지로 이동한다.

 

 위에 보이는 페이지는 스프링 시큐리티에서 지원해주는 기본 로그인 화면이다.

 

 

2. SecurityConfig.java 생성

 

보통 웹사이트를 만들면 /resources/ 밑에 css, js, img, 기타 등등을 정적 리소스로 넣어두기에 리소스 파일은 스프링 시큐리티에서 인증 검사를 하지 않도록 미리 빼준다.

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.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter{


    @Override
	public void configure(WebSecurity web) throws Exception
	{
		// static 디렉터리의 하위 파일 목록은 인증 무시 ( = 항상통과 )
		web.ignoring().antMatchers("/css/**", "/js/**", "/img/**", "/lib/**");
	}
}

 

위의 설정을 적용하면 더이상 리소스파일들은 스프링 시큐리티에서 관리를 하지 않는다.

 

 

3. 유저 DB 설계

다른 사람들이 쓴 포스팅에는 2번 단계에서 메인페이지는 아무나 다 들어올 수 있고 유저페이지는 유저 권한이 있는 사람만 관리자는 관리자 권한이 있는 사람만 들어 올 수 있게 먼저 설정을 한다.

하지만 내 포스팅에서는 위의 과정은 후에 한다.

우선 로그인부터 시켜야지 위의 과정이 필요하기 때문이다.

 

DB 스키마는 다음과 같다

멤버 한명은 여러개의 권한을 가질 수 있으며 멤버 권한은 여러명의 멤버를 가질 수 있는 관계이다.

데이터 타입은 크게 신경쓰지 말자. 우리는 JPA를 사용해서 구현하기 때문에 크게 신경 안써도 된다.

 

 

4. JPA Entity 구현 

JPA를 모르면 구글링 해보자. 

Mybatis 를 사용해서 DB CURD를 하는 경우는 @Entity , @Table, @Id, @Column, @CreationTimestamp, @UpdateTimestamp 를 지우면 된다. ( 밑에 *.java 가 DTO라고 생각하면 됩니다. )

  • T_MEMBER_INFO 를 구현한 MemberInfo Entity
import java.time.LocalDateTime;
import java.util.List;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;

import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Entity
@Table(name="t_member_info")
@Getter
@Setter
@ToString
public class MemberInfo{

	
	@Id
	@Column
	@GeneratedValue
	private long memberInfoSeq;
	
	@Column(nullable = false)
	private String memberId;
	
	@Column(nullable = false)
	private String memberEmail;
	
	@Column(nullable = false)
	private String memberPassword;
	
	@Column
	@CreationTimestamp
	private LocalDateTime registerDate;
	
	@Column
	@UpdateTimestamp
	private LocalDateTime updateDate;
	
	@Column
	private LocalDateTime deleteDate;
	
	@OneToMany(mappedBy = "memberInfo" ,fetch = FetchType.LAZY)
	private List<MemberAuthoritiesMapping> memberAuthoritiesMappingList;

}

 

  • T_MEMBER_AUTHORITIES_CODE 구현한 MemberAuthoritiesCode
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;

import org.hibernate.annotations.CreationTimestamp;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Entity
@Table(name = "T_MEMBER_AUTHORITIES_CODE")
@Getter
@Setter
@ToString
public class MemberAuthoritiesCode {
	
	@Id
	@Column
	@GeneratedValue
	private long memberAuthoritiesCodeSeq;
	
	@Column(nullable = false)
	private String authority;
	
	@Column
	@CreationTimestamp
	private LocalDateTime registerDate;
	
	@OneToMany(mappedBy = "memberAuthoritiesCode" ,fetch = FetchType.LAZY)
	private List<MemberAuthoritiesMapping> memberAuthoritiesMappingList = new ArrayList<>();
	  
}

 

  • T_MEMBER_AUTHORITIES_MAPPING 구현한 MemberAuthoritiesMapping
import java.time.LocalDate;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

import org.hibernate.annotations.CreationTimestamp;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Entity
@Table(name = "t_member_authorities_mapping")
@Getter
@Setter
@ToString
public class MemberAuthoritiesMapping {
	
	@Id
	@Column
	@GeneratedValue
	private long memberAuthoritiesMappingSeq;
	
	
	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name="member_info_seq", referencedColumnName = "memberInfoSeq")
	private MemberInfo memberInfo;
	
	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name="member_authorities_code_seq", referencedColumnName="memberAuthoritiesCodeSeq")
	private MemberAuthoritiesCode memberAuthoritiesCode;
	
	@Column
	@CreationTimestamp
	private LocalDate registerDate;
	
}

 

위의 3개의 클래스를 추가해주면 DB ERD 처럼 자동으로 jpa에서 만들어준다.

 

5. 테스트용 데이터 넣기

src/main/resources 밑에 import.sql 이라는 파일을 만든다.

 

 

그 후 다음 쿼리를 복붙한다.

 

INSERT INTO t_member_authorities_code(member_authorities_code_seq,authority, register_date ) VALUES(1,'ROLE_ADMIN','2020-01-01');
INSERT INTO t_member_authorities_code(member_authorities_code_seq,authority, register_date ) VALUES(2,'ROLE_MEMBER','2020-01-01');


INSERT INTO t_member_info(member_info_seq,delete_date , member_email, member_id, member_password,register_date, update_date  ) VALUES(1,null,'admin@naver.com','admin' ,'$2a$10$hKDVYxLefVHV/vtuPhWD3OigtRyOykRLDdUAp80Z1crSoS1lFqaFS','2020-01-01',null );


INSERT INTO t_member_authorities_mapping(member_authorities_mapping_seq ,member_info_seq, member_authorities_code_seq) VALUES(1,1,1);
INSERT INTO t_member_authorities_mapping(member_authorities_mapping_seq ,member_info_seq, member_authorities_code_seq) VALUES(2,1,2);

 

그럼 실행되면 자동으로 들어간다.

ID 는 admin 이고 비밀번호도 admin 이다. 이 계정은 ROLE_MEMBER권한과 ROLE_ADMIN 권한을 둘 다 가지고 있다.

 

* 스프링 시큐리티 5 에서는 권한의 관련된 체크를 ROLE_ 시작하는지로 확인을 한다. 따라서 다른 권한을 만들때도 ROLE_로 시작하는 권한을 만드는걸 권장한다. (이 부분도 시큐리티 버젼마다 약간 설정이 다른듯 하다.)

 

 

 

 

6. MemberService.java 클래스와 MemberRepository.java 인터페이스를 생성

JPA를 활용하기 위해 위의 두개의 java 파일을 만든다.

mybatis인 경우는 *Service, *DAO, *Mapper.xml를 만들면 된다.

 

MemberService.java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.project.demo.member.entity.MemberInfo;
import com.project.demo.member.repository.MemberRepository;

@Service
public class MemberService{

	@Autowired
	MemberRepository memberRepository;

	public MemberInfo findByMemberId(String memberId) {
		return memberRepository.findByMemberId(memberId);
	}

}

 

JpaRepositiory를 상속받은  MemberRepositiory.java 

 

 

권한을 함께 가지고 오기 위하여 EntityGraph 방식과 fetch 조인(주석친 부분 )을 중 EntityGraph 방식을 사용하였다.

mybatis는 그냥 유저 정보 + 권한  ID로 해서 가져오는 쿼리 호출하면 된다.

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

import com.project.demo.member.entity.MemberInfo;

@Repository
public interface MemberRepository extends JpaRepository<MemberInfo,Long>{

	@EntityGraph(type = EntityGraphType.LOAD, attributePaths = {"memberAuthoritiesMappingList","memberAuthoritiesMappingList.memberAuthoritiesCode"})
//	@Query(value = 
//			"select m from MemberInfo m "
//			+ 	"join fetch m.memberAuthoritiesMappingList a "
//			+ 		"join fetch a.memberAuthoritiesCode "
//			+ "where m.memberId = :memberId" )
	MemberInfo findByMemberId(String memberId);
	
}

 

7. 스프링 시큐리티 인증을 위한 CustomUserDetailsService.java 클래스 생성

 

mybatis의 경우에는 memberService.findByMemberId() 부분을 user 정보 가져오는 걸로 변경.

import java.util.Collection;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
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 com.project.demo.member.entity.MemberAuthoritiesMapping;
import com.project.demo.member.entity.MemberInfo;


@Service
public class CustomUserDetailsService implements UserDetailsService {

	@Autowired
	private MemberService memberService;
	
	/**
	 * 인증 하는 부분
	 */
	@Override
    public UserDetails loadUserByUsername(String memberId) throws UsernameNotFoundException {
		MemberInfo memberInfo = memberService.findByMemberId(memberId);
        return new User(memberInfo.getMemberId(), memberInfo.getMemberPassword(),
        getAuthorities(memberInfo));
    }

	/**
	 * 권한 받아오는 부분
	 * @param memberInfo
	 * @return
	 */
	private Collection<? extends GrantedAuthority> getAuthorities(MemberInfo memberInfo) {
		String[] userRoles =  convert(memberInfo.getMemberAuthoritiesMappingList());
		Collection<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList(userRoles);
		return authorities;
	}
	
	/**
	 * 실제 권한 매핑 함수
	 * @param list
	 * @return
	 */
    public String[] convert(List<MemberAuthoritiesMapping> list) 
    { 
        String[] arrayOfString = new String[list.size()]; 
  
        int index = 0; 
        for (MemberAuthoritiesMapping memberAuthoritiesMapping : list) {
            arrayOfString[index++] = memberAuthoritiesMapping.getMemberAuthoritiesCode().getAuthority(); 
        }
        return arrayOfString; 
    } 
    
}

 

8. 2번에서 만든 SecurityConfig.java 에서 아무곳에 추가


@Autowired
private CustomUserDetailsService customUserDetailsService;
 
/**
* 비밀번호 암호화 관련 설정
*/
@Bean
	public PasswordEncoder passwordEncoder() {
	return new BCryptPasswordEncoder();
}


/**
* 권한 인증 받기(로그인)
*/
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
	auth
		.userDetailsService(customUserDetailsService)
		.passwordEncoder(passwordEncoder());
}
    

 

9. 아무페이지로 가서 로그인 화면을 띄운다.

admin / admin 을 치고 로그인해본다.

로그인이 성공하면 잘 복붙한 거다.

 

 

안되는 부분이 있으면 댓글 남겨주세요.

 


스프링 시큐리티 커스텀 로그인 -로그인 페이지 제작 및 ajax 로그인-

 

 

Spring security 커스텀 로그인 2 - 개발자 삽질 일기

- AJAX 를 이용한 커스텀 로그인 - 이 포스팅에서는 jQuery Ajax를 이용한 커스텀 로그인 페이지 구현을 할 것이다. 스프링 시큐리티에 기본 로그인 페이지는 로그인 실패시 페이지를 새로고침하고 쿼리스트링(loc..

programmer93.tistory.com

 

반응형