-다시 말해서 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>
https://imbf.github.io/interview/2021/03/06/NAVER-Practical-Interview-Preparation-8.html