
1.로그인/로그아웃WEB JAVA SPRING/PROJECT2023. 9. 12. 16:58
Table of Contents
<로그인>
<회원가입>
Member
package com.shop.entity;
import com.shop.constant.Role;
import com.shop.dto.MemberFormDto;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.security.crypto.password.PasswordEncoder;
import javax.persistence.*;
@Entity
@Table(name="member_id")
@Getter@Setter
@ToString
public class Member extends BaseEntity {
@Id
@Column(name = "member_id")
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
@Column(unique = true) //회원은 이메일을 통해 유일하게 구분, 동일한 값 DB에 들어올 수 없게
private String email;
private String password;
@Embedded
private Address address;
@Enumerated(EnumType.STRING) //eum 순서가 바뀌지 않도록록
private Role role;
/*Member 엔티티를 생성하는 메소드
여기에서 관리를 한다면 코드가 변경되더라도 한군데만 수정하면 되는 이점
*/
public static Member createMember(MemberFormDto memberFormDto, PasswordEncoder passwordEncoder){
Member member = new Member();
member.setName(memberFormDto.getName());
member.setEmail(memberFormDto.getEmail());
member.setAddress(new Address("서울시","구","105호"));
//스프링 시큐리티 설정 클래스에 등록한 BCrptPasswordEncoder Bean을 파라미터로 넘겨서 비밀번호 암호화
String password = passwordEncoder.encode(memberFormDto.getPassword());
member.setPassword(password);
member.setRole(Role.ADMIN);
return member;
}
}
MemberFormDto
package com.shop.dto;
import com.shop.entity.Address;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
/**
* 회원 가입 화면으로부터 넘어오는 가입정보 담음
*/
@Getter
@Setter
public class MemberFormDto {
@NotBlank(message = "이름은 필수 입력 값입니다.")
private String name;
@NotBlank(message = "이메일은 필수 입력 값입니다.")
@Email(message = "이메일 형식으로 입력해주세요.")
private String email;
@NotBlank(message = "비밀번호는 필수 입력 값입니다.")
@Length(min = 8,max = 16,message = "비밀번호는 8자 이상, 16자 이하로 입력해주세요")
private String password;
@NotBlank(message = "주소는 필수 입력 값입니다.")
private String city;
private String street;
private String zipcode;
public void setAddress(Address address) {
}
}
MemberController
package com.shop.controller;
import com.shop.dto.MemberFormDto;
import com.shop.entity.Member;
import com.shop.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.validation.Valid;
@RequestMapping("/members")
@Controller
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
private final PasswordEncoder passwordEncoder;
//회원가입
@GetMapping(value = "/new")
public String memberForm(Model model) {
model.addAttribute("memberFormDto", new MemberFormDto()); //회원 가입 화면으로부터 넘어오는 가입정보 담아서
return "member/memberForm"; //타임리프로 넘김
}
/**
* 회원가입 성공 : main으로 리다이렉트
* 회원정보 검증 및 중복회원 가입 조건에 의해 실패 : 다시 회원가입 페이지로 돌아가 실패이유 출력
*/
@PostMapping(value = "/new")
/*검증하려는 객체 앞에 @Vaild 선언하고, bindingResult 객체 추가 후 검사후 결과 담는다.
bindingResult.hasErrors()를 호출하여 에러가 있다면 회원가입 페이지로 이동
*/
public String newMember(@Valid MemberFormDto memberFormDto, BindingResult bindingResult, Model model){
if(bindingResult.hasErrors()){
return "member/memberForm";
}
try {
Member member = Member.createMember(memberFormDto, passwordEncoder); //회원 생성
memberService.saveMember(member); //회원 정보 저장
} catch (IllegalStateException e) {
model.addAttribute("errorMessage", e.getMessage()); //중복 회원가입 에러메세지 뷰로 전달
return "member/memberForm";
}
return "redirect:/"; //성공시 리다이렉트
}
@GetMapping(value = "/login")
public String loginMember(){
return "/member/memberLoginForm";
}
@GetMapping(value = "/login/error")
public String loginError(Model model){
model.addAttribute("loginErrorMsg", "아이디 또는 비밀번호를 확인해주세요");
return "/member/memberLoginForm";
}
}
MemberService
package com.shop.service;
import com.shop.entity.Member;
import com.shop.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
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 org.springframework.transaction.annotation.Transactional;
/**
* MemberService가 UserDetailsService를 구현한다.
* <UserDetailsService>
* - DB에서 회원정보를 가지고 오는 역할
* - loadUserByUsername()메소드가 존재하며, 회원 정보를 조회하여 사용자의 정보와 권한을 갖는 UserDetails 인터페이스(내장)를 반환
*
*/
@Service
@Transactional //비즈니스 로직을 담당하는 서비스 계층에 작성, 로직처리 중 에러 발생시 변경된 데이터를 로직 수행 전으로 콜백
@RequiredArgsConstructor //bean 주입(@Setter,@Autowired) 이건 final이나 @NotNull이 붙은 필드에 생성자 생성
public class MemberService implements UserDetailsService {
//final:다른 객체로 바꾸지 않기위해(재할당x)
private final MemberRepository memberRepository;
public Member saveMember(Member member){
validateDuplicateMember(member);
return memberRepository.save(member);
}
//이미 가입된 회원인 경우 IllegalStateException 예외 발생
private void validateDuplicateMember(Member member){
Member findMember = memberRepository.findByEmail(member.getEmail());
if(findMember != null){
throw new IllegalStateException("이미 가입된 회원입니다.");
}
}
/*
throws
메소드의 내부 소스코드에서 에러가 발생했을시 예외처리를 "try ~ catch"로 자기자신이 예외처리를 하는 것이 아니라,
이 메소드를 사용하는 곳으로 책임을 전가하는 행위
throw new 발생시킬 예외;
*/
/**
*
* <UserDetail>
* - 시큐리티에서 회원의 정보를 담기위해 사용하는 인터페이스
* - 직접구현 or 시큐리티에서 제공하는 User클래스 사용
* - User 클래스는 UserDetails 인터페이스를 구현하고 있는 클래스
*
* @param email : 로그인할 유저의 email 전달받음
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
Member member =memberRepository.findByEmail(email);
if(member == null){
throw new UsernameNotFoundException(email); //내장
}
//UserDetail을 구현하고 있는 User 객체를 반환, User 객체를 생성하기 위해 생성자로 회원의 이메일,비밀번호,role을 파라미터로 넘김
return User.builder()
.username(member.getEmail())
.password(member.getPassword())
.roles(member.getRole().toString())
.build();
}
}
MemberRepository
package com.shop.repository;
import com.shop.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;
/**
* Memeber 엔티티 DB저장
*/
public interface MemberRepository extends JpaRepository<Member, Long> {
Member findByEmail(String email); //회원가입시 중복된 회원이 있는지 이메일을 검사
}
memberForm(회원가입)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/layout1}">
<!-- 사용자 CSS 추가 -->
<th:block layout:fragment="css">
<style>
.fieldError {
color: #bd2130;
}
</style>
</th:block>
<!-- 사용자 스크립트 추가 -->
<th:block layout:fragment="script">
<script th:inline="javascript">
//회원가입 실패시 에러메세지
$(document).ready(function(){
var errorMessage = [[${errorMessage}]]; //MemberController
if(errorMessage != null){
alert(errorMessage);
}
});
</script>
</th:block>
<div layout:fragment="content">
<!--th:object : form submit을 할 때, form의 데이터가 th:object에 설정해준 객체로 받아진다.-->
<form action="/members/new" role="form" method="post" th:object="${memberFormDto}">
<div class="form-group">
<label th:for="name">이름</label>
<input type="text" th:field="*{name}" class="form-control" placeholder="이름을 입력해주세요">
<!--
th:errors :해당 value의 error가 있는 경우 출력한다.
form의 validation error를 출력할 때 사용할 수 있다.
th:field : 각각 필드들을 매핑을 해주는 역할을 한다. 설정해 준 값으로, th:object에 설정해 준 객체의 내부와 매칭해준다.
-->
<p th:if="${#fields.hasErrors('name')}" th:errors="*{name}" class="fieldError">Incorrect data</p>
</div>
<div class="form-group">
<label th:for="email">이메일주소</label>
<input type="email" th:field="*{email}" class="form-control" placeholder="이메일을 입력해주세요">
<p th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="fieldError">Incorrect data</p>
</div>
<div class="form-group">
<label th:for="password">비밀번호</label>
<input type="password" th:field="*{password}" class="form-control" placeholder="비밀번호 입력">
<p th:if="${#fields.hasErrors('password')}" th:errors="*{password}" class="fieldError">Incorrect data</p>
</div>
<div class="form-group">
<label th:for="city">도시</label>
<input type="text" th:field="*{city}" class="form-control" placeholder="도시를 입력하세요">
<p th:if="${#fields.hasErrors('city')}" th:errors="*{city}" class="fieldError">Incorrect data</p>
</div>
<div class="form-group">
<label th:for="street">구</label>
<input type="text" th:field="*{street}" class="form-control" placeholder="구를 입력하세요">
<p th:if="${#fields.hasErrors('street')}" th:errors="*{street}" class="fieldError">Incorrect data</p>
</div>
<div class="form-group">
<label th:for="city">상세주소</label>
<input type="text" th:field="*{zipcode}" class="form-control" placeholder="상세주소를 입력하세요">
<p th:if="${#fields.hasErrors('zipcode')}" th:errors="*{zipcode}" class="fieldError">Incorrect data</p>
</div>
<div style="text-align: center">
<button type="submit" class="btn btn-primary" style="">Submit</button>
</div>
<!--시큐리티를 사용할 경우 기본적으로 CSRF(Cross Site Request Forgery)를 방어하기 위해
모든 post방식의 데이터 전송에는 CSRF 토큰값이 있어야한다.
실제 서버에서 허용한 요청이 맞는지 확인하기 위한 토큰이다.
사용자의 세션에 임의의 값을 저장하여 요청마다 그 값을 포함하여 전송하면
서버에서 세션에 저장된 값과 요청이 온 값이 일치하는지 확인하여 CSRF를 방어한다.-->
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
</form>
</div>
</html>
memberLoginForm(로그인)
<form role="form" method="post" action="/members/login">
<div class="form-group">
<label th:for="email">이메일주소</label>
<input type="email" name="email" class="form-control" placeholder="이메일을 입력해주세요">
</div>
<div class="form-group">
<label th:for="password">비밀번호</label>
<input type="password" name="password" id="password" class="form-control" placeholder="비밀번호 입력">
</div>
<p th:if="${loginErrorMsg}" class="error" th:text="${loginErrorMsg}"></p>
<button class="btn btn-primary">로그인</button>
<button type="button" class="btn btn-primary" onClick="location.href='/members/new'">회원가입</button>
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
</form>
<Security 코드>
SecurityConfig
/*
시큐리티에서 인증은 AuthenticationManagerBuilder를 통해 이루어진다.
AuthenticationManagerBuilder가 AuthenticationManager를 생성한다.
*/
package com.shop.config;
import com.shop.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
MemberService memberService;
/*http 요청에 대한 보안 설정
페이지 권한 설정, 로그인 페이지 설정, 로그아웃 메소드
*/
@Override
protected void configure(HttpSecurity http) throws Exception{
http.formLogin()
.loginPage("/members/login") //로그인 페이지 url
.defaultSuccessUrl("/") //로그인 성공시 이동할 url
.usernameParameter("email") //로그인 시 사용할 파라미터 이름
.failureUrl("/members/login/error") //로그인 실패시 이동할 url
.and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/members/logout")) //로그아웃 url
.logoutSuccessUrl("/"); //로그아웃 성공시 이동할 url
http.authorizeRequests() //시큐리티 처리에 HttpServletRequest를 이용한다는 것을 의미
.mvcMatchers("/", "/members/**", "/item/**", "/images/**").permitAll() // permitAll을 통해 모든 사용자가 인증(로그인)없이 해당경로에 접근 -메인,회원관련url,상품 상세페이지, 상품 이미지 경로
.mvcMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated();
http.exceptionHandling()
.authenticationEntryPoint(new CustomAuthenticationEntryPoint());
}
//비밀번호 암호화
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(memberService) // userDetailsService를 구현하고 있는 객체로 memberService를 지정
.passwordEncoder(passwordEncoder()); //비밀번호 암호화를 위해 passwordEncoder를 지정
}
}
UserDetailsService
- DB에서 회원정보를 가지고 오는 역할
- loadUserByUsername()메소드가 존재하며, 회원 정보를 조회하여 사용자의 정보와 권한을 갖는
UserDetails 인터페이스(내장)를 반환
package org.springframework.security.core.userdetails;
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
UserDetail
- 시큐리티에서 회원의 정보를 담기위해 사용하는 인터페이스
- 직접구현 or 시큐리티에서 제공하는 User클래스 사용
- User 클래스는 UserDetails 인터페이스를 구현하고 있는 클래스
/**
*
*
* @param email : 로그인할 유저의 email 전달받음
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
Member member =memberRepository.findByEmail(email);
if(member == null){
throw new UsernameNotFoundException(email); //내장
}
//UserDetail을 구현하고 있는 User 객체를 반환, User 객체를 생성하기 위해 생성자로 회원의 이메일,비밀번호,role을 파라미터로 넘김
return User.builder()
.username(member.getEmail())
.password(member.getPassword())
.roles(member.getRole().toString())
.build();
}
CustomAuthenticationEntryPoint
/**
* 인증되지 않은 사용자가 리소스를 요청할 경우 Unauthorized 에러 발생하도록
* AuthenticationEntryPoint 인터페이스를 구현
*/
package com.shop.config;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
package org.springframework.security.web;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
public interface AuthenticationEntryPoint {
void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException)
throws IOException, ServletException;
}
'WEB JAVA SPRING > PROJECT' 카테고리의 다른 글
3.상품 등록하기 (0) | 2023.09.12 |
---|---|
2.연관관계 매핑 (0) | 2023.09.12 |
스타트 스프링 부트 -1~5장 (0) | 2023.08.28 |
[Day-16] 간단한 주문 조회 V2: 엔티티를 DTO로 변환 (0) | 2023.08.28 |
[Day-15] rest api 엔티티 직접 노출 (0) | 2023.08.28 |