[Spring boot] Security 인증

이경영·2022년 11월 5일
0

스프링부트

목록 보기
12/12
post-custom-banner

Spring Security는 무엇인가요?

  • 자바 어플리케이션에서 인증과 권한 부여, 일반적인 공격에 대한 보호기능을 제공하는 프레임워크
  • 인터셉터와 AOP를 활용하는 방식이 아닌 Filter를 통해서 인증 구현가능.

Filter와 Security Filter의 차이

  • Filter와 Security Filter 모두 Servlet에 요청이 맵핑되기 전에 실행되는 필터
  • Filter : 서블릿 컨테이너에 직접 등록해서 사용하는 필터
  • Security Filter는 DelegatingFilterProxy가 서블릿 컨테이너에 Filter로 등록되어 Filter작업을 Security FilterChain으로 위임해 실행되는 필터

-다시 말해서 Filter와 Security Filter 모두 서블릿에 요청이 맵핑되기 전에 실행되는 필터인데 이를 서블릿 컨테이너에 직접 등록하느냐 아니면 DelegatingFilterProxy가 Filter작업을 Security Filter Chain으로 위임해서 사용하느냐의 차이

dependency 설정

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

Security 설정
프로젝트에 필요한 구성으로 Security 설정이 자유롭게 가능함.

package com.example.configuration;

import org.springframework.context.annotation.Configuration;
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
@EnableWebSecurity
public class WebSecurityConfigruation {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http.authorizeRequests()
			// 해당 url 패턴은 로그인 권한없어도 접근되게
			.antMatchers("/public/**", "/member/form", "/member/join**")
			.permitAll()
			// 나머지 요청은 로그인을 해야 접근되게
			.anyRequest().hasRole("USER").and()
			//.csrf().disable()
			.formLogin()
			.permitAll();
		return http.build();
	}
	
	/**
	 * 비밀번호 인코더 등록
	 * 등록안하면 There is no PasswordEncoder mapped for the id "null" 에러가 발생
	 * @return
	 */
	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}

}

MemberService.java 전체부분

package com.example.service;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.UUID;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.multipart.MultipartFile;

import com.example.controller.form.MemberJoinForm;
import com.example.domain.Member;
import com.example.mapper.MemberMapper;
import com.example.security.SecurityUserDetails;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Service
@RequiredArgsConstructor
@Slf4j
public class MemberService implements UserDetailsService{
	
	private final MemberMapper memberMapper;
	final Logger logger = LoggerFactory.getLogger(getClass());
	
	private final PasswordEncoder passwordEncoder;
	
	@Value("${file.root-path}")
	private String rootPath;
	
	public int selectMemberAccount(String account) {
		return memberMapper.selectMemberAccountCount(account);
	}
	
//	public void insertMember(MemberJoinForm form) {
//		memberMapper.insertMember(form);
//	}
	
	public void insertMember(MemberJoinForm form) {
		MultipartFile profileImage = form.getProfileImage();
		String originalFilename=profileImage.getOriginalFilename();
		String ext=originalFilename.substring(originalFilename.lastIndexOf(".")+1,
				originalFilename.length()); 
		// 탐색하는 문자열이 마지막으로 등장하는 위치에 대한 index를 반환  ex).jpg, .png의 .
		// ext : .jpg 출력
		
		String randomFilename=UUID.randomUUID().toString()+"."+ext; 
		//UUID 클래스를 사용해 유일한 식별자 생성가능. 
		// 1. 업로드된 파일명의 중복을 방지하기 위해 파일명을 변경할 때 사용.
		// 2. 첨부파일 파일다운로드시 다른 파일을 예측하여 다운로드하는것을 방지하는데 사용.
		// 3. 일련번호 대신 유추하기 힘든 식별자를 사용하여 다른 컨텐츠의 임의 접근을 방지하는데 사용.
		
		String addPath="/"+LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
		// 현재날짜 포매팅 /2021-09-02 표시
		// 출처 : https://suyou.tistory.com/287 
		
		// 저장경로
		String savePath = new StringBuilder(rootPath).append(addPath).toString();
		String imagePath = addPath + "/" + randomFilename; // imagePath:/2021-09-02/파일명.jpg 
		File saveDir = new File(savePath); // pathname가 생성자의 파라미터 
		logger.info("originalFilename : {}", originalFilename);
		logger.info("ext : {}", ext);
		logger.info("randomFilename : {}", randomFilename);
		
		// 폴더가 없는 경우
		// 폴더가 없는경우
		if (!saveDir.isDirectory()) {
			// 폴더 생성
			saveDir.mkdirs();
		}
		File out = new File(saveDir, randomFilename);
		
		try {
			FileCopyUtils.copy(profileImage.getInputStream(), new FileOutputStream(out));
			//in의 내용을 out에 복사하고 스트림 닫는다. byte수 return
		} catch (IOException e) {
			log.error("fileCopy", e);
			throw new RuntimeException("파일을 저장하는 과정에 오류가 발생하였습니다.");
		}
		
		
		Member member = new Member();
		member.setAccount(form.getAccount());
		//패스워드 암호화 
		String encodePassword=passwordEncoder.encode(form.getPassword());
		log.info("encodePassword : {}" , encodePassword);
		member.setPassword(encodePassword);
		
		member.setNickname(form.getNickname());
		member.setProfileImagePath(imagePath);
		member.setProfileImageName(originalFilename);

		memberMapper.insertMember(member);

	}

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		log.info("loadUserByUsername : {}", username);
		//화면에서 받은 username
		Member member = memberMapper.selectMemberAccount(username);
		if (member == null) {
			throw new UsernameNotFoundException("회원이 존재하지 않습니다.");
		}
		
		log.info("member : {}", member);
		return SecurityUserDetails.builder()
			.memberSeq(member.getMemberSeq())
			.nickname(member.getNickname())
			.username(username)
			.password(member.getPassword())
			.build();

	}
	

}

MemberService.java

  • insertMember에서 Database에 저장시 비밀번호는 단방향으로, 암호화된 값으로 저장해야함
  • passwordEncoder를 사용하여 쉽게 가능함.
  • 앞서 설정에서 Encoder를 Bean으로 등록해서 자동 주입으로 사용가능함.
//상단에 final설정
		Member member = new Member();
		member.setAccount(form.getAccount());
		//패스워드 암호화 
		String encodePassword=passwordEncoder.encode(form.getPassword()); //passwordEncoder
		log.info("encodePassword : {}" , encodePassword);
		member.setPassword(encodePassword);
		
		member.setNickname(form.getNickname());
		member.setProfileImagePath(imagePath);
		member.setProfileImageName(originalFilename);

		memberMapper.insertMember(member);

com.example.security
UserDetails.java 생성
해당 메소드들은 Security에서 로그인 사용자의
만료기간, 계정잠금, 자격증명, 사용여부(활성화) 등에 대한 부가적인 정보도 boolean으로 알려주면,
만약 false인 경우 아이디/비번이 맞아도 로그인이 실패하게 되어있다. (Security가 제공하는 기능)
프로젝트에서 필요한 경우 회원 테이블에 컬럼을 추가해서 사용하면 된다.

package com.example.security;

import java.util.Arrays;
import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import lombok.Builder;
import lombok.Data;


@Builder
@Data
public class SecurityUserDetails implements UserDetails {

	private static final long serialVersionUID = -5122915267753025191L;
	private final int memberSeq;
	private final String username;
	private final String password;
	private final String nickname;

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		// 권한을 추가해줘야 로그인 이후 오류가 발생안함.
		return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
	}

	@Override
	public String getUsername() {
		return username;
	}

	@Override
	public boolean isAccountNonExpired() {
		return true;
	}

	@Override
	public boolean isAccountNonLocked() {
		return true;
	}

	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}

	@Override
	public boolean isEnabled() {
		return true;
	}

}

MemberService.java UserDetailsService 구현
MemberService에 UserDetailsService 인터페이스를 구현함.
이렇게하면 Security 내부 로그인 과정에서 해당 클래스가 호출되도록 자동으로 연결된다.

오버라이드
loadUserByUsername 메소드를 오버라이드하여 실제 회원을 조회하고 UserDetails를 구현한 클래스에
회원정보를 담아서 리턴함.

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		log.info("loadUserByUsername : {}", username);
		//화면에서 받은 username
		Member member = memberMapper.selectMemberAccount(username);
		if (member == null) {
			throw new UsernameNotFoundException("회원이 존재하지 않습니다.");
		}
		
		log.info("member : {}", member);
		return SecurityUserDetails.builder()
			.memberSeq(member.getMemberSeq())
			.nickname(member.getNickname())
			.username(username)
			.password(member.getPassword())
			.build();

	}

MemberMapper 메소드 추가

package com.example.mapper;

import com.example.domain.Member;

public interface MemberMapper {
	
	int selectMemberAccountCount(String account);
	
	void insertMember(Member form);
	
	Member selectMemberAccount(String username); //추가함.
	
	

}

Member.xml에
selectMemberAccount 쿼리 추가
username(계정)으로 회원정보를 조회해서 리턴하는 쿼리를 추가. 비밀번호는 암호화되어 있으므로 추후 Security 내부에서 자동으로 비밀번호 검사까지 진행해준다.

  	<!-- 회원 계정 조회 -->
	<select id="selectMemberAccount" parameterType="String" resultType="com.example.domain.Member">
		SELECT MEMBER_SEQ, ACCOUNT, PASSWORD, NICKNAME
		FROM T_MEMBER
		WHERE ACCOUNT = #{account}
	</select>

HomeController.java

security 인증 성공시 매개변수로 Authentication 인터페이스를 선언 한다면 인증된 객체 정보를 받을 수 있다.
MemberService에서 구현한 내부회원 상세정보를 가진 객체를 가져올 수 있다.

Home.html

<html xmlns:th="http://www.thymeleaf.org"
	xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
	layout:decorate="~{layouts/default-layout}">
<body>
	<th:block layout:fragment="content">
	<p>Spring Boot.</p>
		<th:block th:if="${details != null}">
			<p>로그인 회원 아이디 : [[${details.username}]]</p>
			<p>로그인 회원 닉네임 : [[${details.nickname}]]</p>
		</th:block>
	</th:block>
</body>
</html>

References

https://imbf.github.io/interview/2021/03/06/NAVER-Practical-Interview-Preparation-8.html

profile
꾸준히
post-custom-banner

0개의 댓글