회원가입 / 로그인 기능 구현
게시판에 게시글을 작성하기 위해 작성자가 될 회원(Member)에 대한 '회원가입/로그인' 기능을 구현한다.
1. 회원(Member) 엔티티, 리포지토리, 서비스, 컨트롤러 생성하기
[Member - Entity]
//Member - Entity
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
private Long id;
@Column(unique = true, nullable = false)
private String name; //회원명 == 회원 ID
private String password;
@Column(unique = true, nullable = false)
private String email;
@Enumerated(EnumType.STRING)
private Role role; //회원 권한
private LocalDateTime createMemberDateTime; //회원가입 일자
}
'회원명', '비밀번호', 'E-mail', '회원 권한', '회원 가입 일자' 를 속성 엔티티를 구성하였다.
[MemberRepository - Repository]
//MemberRepository - Repository
@Repository
@RequiredArgsConstructor
public class MemberRepository {
private final EntityManager em;
//저장 로직
public void save(Member member) {
em.persist(member);
}
//검색 로직 (id 기반)
public Optional<Member> findById(Long id) {
return Optional.ofNullable(em.find(Member.class, id));
}
//검색 로직 (name 기반)
public Optional<Member> findByName(String name) {
List findMembers = em.createQuery("select m from Member m where m.name = :name")
.setParameter("name", name)
.getResultList();
return findMembers.stream().findFirst();
}
}
EntityManager 를 통하여 '저장 로직' , '검색 로직 (id, name 기반)' 로직을 구성하였다.
[MemberService - Service]
//MemberService - Service
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
// 회원가입 기능
@Transactional
public Member createMember(String name, String password, String email) {
Member member = new Member();
member.setName(name);
//password 암호화 저장
member.setPassword(passwordEncoder.encode(password));
member.setEmail(email);
member.setCreateMemberDateTime(LocalDateTime.now());
member.setRole(Role.USER); //USER 권한 부여
memberRepository.save(member);
return member;
}
// 인증 (로그인시 인증)
public boolean authenticate(String name, String password) {
Optional<Member> findMember = memberRepository.findByName(name);
if (findMember.isPresent()) {
return passwordEncoder.matches(password, findMember.get().getPassword());
}
return false;
}
// 회원 검색 기능 (username 기반)
public Member findMemberByName(String name) {
Optional<Member> findMember = memberRepository.findByName(name);
if (findMember.isPresent()) {
return findMember.get();
} else {
throw new DataNotFoundException("user not found");
}
}
// 회원 검색 기능 (id 기반)
public Member findMemberById(Long id) {
Optional<Member> findMember = memberRepository.findById(id);
if (findMember.isPresent()) {
return findMember.get();
} else {
throw new DataNotFoundException("user not found");
}
}
}
회원가입 기능, 회원 검색 기능(id 기반, name 기반) 으로 구성된 MemberService 이다.
회원가입 기능(createMember) 중 회원의 비밀번호를 저장하는 곳에서
passwordEncoder를 통해 비밀번호를 암호화 하여 저장한다.
passwordEncoder 는 별도로 Bean 등록을 해야 한다. 이는 '3. Spring Security 구성하기' 에 자세히 설명한다.
[URL 경로 구성]
회원가입, 로그인, 회원정보 의 기능을 구현하기 위해 위와 같이 경로를 구성하였다.
회원 정보는 현재 로그인 되어 있는 회원을 식별하기 위해서 추가하였다.
회원 정보는 로그인을 통해 인증을 통과한 후 접근이 가능 하도록 한다.
[MemberController - Controller]
//MemberController - Controller
@Controller
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
// User Login
@GetMapping("/member/login")
public String getLoginForm(Model model) {
model.addAttribute("loginDto", new LoginDto());
return "member/login";
}
//로그인 관련 로직은 "/login" spring security 에게 이관한다.
// Create New User
@GetMapping("/member/new")
public String createMemberForm(Model model) {
model.addAttribute("CreateMemberDto", new CreateMemberDto());
return "member/createMember";
}
@PostMapping("/member/new")
public String createUser(@ModelAttribute CreateMemberDto createMemberDto, Model model) {
if (createMemberDto.getName().isEmpty() || createMemberDto.getEmail().isEmpty()) {
model.addAttribute("createMemberError", "회원가입 양식을 모두 입력해 주십시오.");
System.out.println("회원가입 양식을 모두 입력해 주십시오.");
return "member/createMember";
}
if (!createMemberDto.getPassword().equals(createMemberDto.getPasswordEqual())) {
model.addAttribute("createMemberError", "비밀번호가 일치 하지 않습니다.");
System.out.println("비밀번호가 일치 하지 않습니다.");
return "member/createMember";
}
try {
memberService.createMember(createMemberDto.getName(), createMemberDto.getPassword(), createMemberDto.getEmail());
} catch (DataNotFoundException e) {
model.addAttribute("createMemberError", "이미 등록된 사용자 입니다.");
System.out.println("이미 등록된 사용자 입니다.");
return "member/createMember";
} catch (Exception e) {
e.printStackTrace();
return "member/createMember";
}
return "redirect:/";
}
// User Information
@GetMapping("/member/private/info")
public String userInfo(Principal principal, Model model) {
String memberName = principal.getName();
model.addAttribute("memberName", memberName);
return "member/memberInfo";
}
}
- 로그인
@GetMapping 으로 로그인 페이지를 매핑해준다.
별도의 @PostMapping 로그인 과정을 직접 구현하지 않고, SpringSecurity 에게 이 과정을 이관한다.
- 회원가입
@GetMapping 으로 회원가입 페이지를 매핑해준다.
@PostMapping 으로 회원가입 규칙을 설정한다.
- 1. name 과 email 은 필수적으로 입력해야 한다.
- 2. 초기 비밀번호 설정은 2번 중복하여 입력하고, 두개의 비밀번호는 일치해야 한다.
- 회원정보
@GetMapping 으로 현재 로그인한 회원 이름을 model에 싣어서 보낸다.
2. VIEW 단 구성하기 (resource)
[resources 트리 구조]
'templates/fragments' : 공통적으로 반복되는 header/footer 구성
'templates/members' : 회원 관련 view 구성
3. Spring Security 구성하기 (Config 파일, 별도의 userDetailsService 설정)
[WebSecurityConfig]
//WebSecurityConfig - SpringSecurityConfig File
@EnableWebSecurity
@Configuration
@RequiredArgsConstructor
public class WebSecurityConfig {
@Autowired
private CustomUserDetailsService customUserDetailsService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 요청되는 모든 URL 요청을 허용
http
.authorizeHttpRequests((authorizeRequests) ->
authorizeRequests
//메인, 로그인, 회원가입 페이지는 인증 없이 접근가능
.requestMatchers("/", "/member/login", "/member/new").permitAll()
//"/member/private" 이하 URL은 인증 필요
.requestMatchers("/member/private/**").authenticated()
.anyRequest().authenticated()
)
.formLogin((form) ->
form
.usernameParameter("username")
.passwordParameter("password")
.loginPage("/member/login") //사용자가 직접 지정한 로그인 페이지
.loginProcessingUrl("/login") //"/member/login" 으로 부터 받은 로그인 정보를 "/login" 으로 POST 요청을 보냄
.defaultSuccessUrl("/", true) // 로그인 성공시 이동
.permitAll()
)
.userDetailsService(customUserDetailsService) //DB의 Member 테이블의 데이터를 통해 로그인하기 위함.
.logout(logout ->
logout
.logoutUrl("/logout") //로그아웃 처리 URL
.logoutSuccessUrl("/login?logout") //로그아웃 성공 후 리다이렉트 할 URL
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.permitAll()
);
return http.build();
}
// WebSecurityCustomizer Bean 등록 - 정적 resources 접근을 위함
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
// 정적 리소스가 위치한 파일의 보안 처리를 무시 (누구든 접근 가능)
return (web -> web.ignoring()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()));
}
//일부러 아무런 암호화도 사용하지 않음.
@Bean
public static PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
/*
// PasswordEncoder Bean 등록 - password 암호화 (방식 - BCryptPasswordEncoder)
@Bean
public static PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
*/
}
앞서 설명했던, PasswordEncoder 를 Bean 등록하였다.
현재 개발단계이기 때문에 별도로 암호화를 하지 않는 구현 클래스인 "NoOpPasswordEncoder" 을 사용하였다.
[CustomUserDetailsService]
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private MemberRepository memberRepository;
@Override
public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
Member member = memberRepository.findByName(name)
.orElseThrow(() -> new UsernameNotFoundException("User not found with name: " + name));
return new User(member.getName(), member.getPassword(), getAuthorities(member));
}
private Collection<? extends GrantedAuthority> getAuthorities(Member member) {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_" + member.getRole().name()));
return authorities;
}
}
4. 동작 확인
["/"] - 메인
["/member/new"] - 회원가입
["/member/login"] - 로그인
["/member/private/info"] - 회원정보
'PROJECT > [SpringBoot] 게시판 서비스' 카테고리의 다른 글
[게시판 서비스] 회원 탈퇴시, 게시글 / 댓글 처리 (0) | 2024.06.02 |
---|---|
[게시판 서비스] 게시글 키워드 검색 + 정렬 + 페이징 기능 구현 (0) | 2024.06.02 |
[게시판 서비스] 게시글 페이징 처리 구현 (0) | 2024.06.02 |
[ERROR] Refused to apply style from 'http://localhost:8080/member/login' because its MIME type ('text/html') is not a supported stylesheet MIME type, and strict MIME checking is enabled. (0) | 2024.06.02 |
[게시판 서비스] 개발 과정 오류 해결 (0) | 2024.04.14 |