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