4월 12일

SJY0000·2022년 4월 12일
0

Springboot

목록 보기
13/24

오늘 배운 것

  • Security

Security

Security 사용 전

  1. Pom.xml에 Security Library 추가
  2. 기본 아이디는 user 비밀번호는 실행시 console창에 나오지만
    간편하게 사용하기 위해 application.properties에 설정
    # Security login 설정
    spring.security.user.name=user
    spring.security.user.password=1234

참고하기(Security)
https://velog.io/@tutu10000/3%EC%9B%94-29%EC%9D%BC
https://velog.io/@tutu10000/3%EC%9B%94-30%EC%9D%BC

Security 설정

  • 인증(로그인)시 UserDetailsService를 사용하여 인증절차를 간편하게 할 수 있음
  • 권한설정을 해서 권한확인이 필요한 페이지로 이동시 로그인이 되어있지 않은 상태면 자동으로 로그인페이지로 이동
    +++ 주소창에 /logout 적어도 로그아웃 되지않고 nav에 설정된 logout버튼을 눌러야 로그아웃 됨
package com.myapp.shoppingmall.security;

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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

// 1. WebSecurityConfigurerAdapter 상속, 2. @EnableWebSecurity
@Configuration // Class 안에 등록할 객체 또는 메소드가 있음을 표시
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	// Security는 1. 인증(로그인) , 2. 허가(role에 따른 권한)
	@Autowired
	private UserDetailsService userDetailsService;
	
	@Bean // 이 메소드를 Spring에 빈(메소드)로 등록
	public PasswordEncoder encoder() {
		return new BCryptPasswordEncoder(); // 비밀번호 인코더 객체
	}
	
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		// 인증메소드 구현, 인증하기위해서 userDetailsService로 User의 username, password, role 등을 찾아서 인증
		auth.userDetailsService(userDetailsService) // 관리자 또는 유저를 찾음
			.passwordEncoder(encoder());			// 비밀번호를 다시 풀기위한 암호화 객체 필요
	}
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		// 허가(rore에 따른 권한설정)
		http.authorizeHttpRequests()
			.antMatchers("/category/**").hasAnyRole("USER", "ADMIN") // 카테고리 안의 페이지는 유저, 관리자 접근가능
			.antMatchers("/admin/**").hasAnyRole("ADMIN")			// 관리자 안의 페이지는 관리지만 접근가능
			.antMatchers("/").permitAll()							// 누구나 접근 가능
			.and()
			.formLogin().loginPage("/login")						// 로그인페이지 주소
			.and()
			.logout().logoutSuccessUrl("/")							// Logout 시 홈으로 이동
			.and()
			.exceptionHandling().accessDeniedPage("/");				// 예외 발생시 홈으로 이동
		
	}
  • UserDetails를 implements
  • @Transient로 실제 DB column에는 없지만 없어도 임시적으로 사용할 수 있게 해줌
  • collection 메소드에서 User Table에 있는 모든 계정의 권한을 'USER'로 설정함
package com.myapp.shoppingmall.entities;

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

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

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

import lombok.Data;

@Entity
@Table(name = "users") // DB의 users Table과 매칭
@Data
public class User implements UserDetails{
	private static final long serialVersionUID = 1L;

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private int id;
	
	@NotBlank
	@Size(min = 2, message = "최소 2글자 이상 입력해주세요.")
	private String username;
	
	@NotBlank
	@Size(min = 4, message = "비밀번호는 최소 4글자이상 입력해주세요.")
	private String password;
	
	@Transient // 사용은 할 수 있지만 데이터는 없는 더미, 패스워드 확인 시 사용
	private String confirmPassword;
	
	@Email(message = "이메일 양식에 맞게 입력해주세요.")
	private String email;
	
	@Column(name = "phone_number")
	@Size(min = 6, message = "전화번호를 제대로 입력해주세요.")
	private String phoneNumber;
	
	
	
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		// 권한 목록을 Return(USER 권한)
		return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
	}

	@Override
	public boolean isAccountNonExpired() {
		// 계정 만료 여부
		return true; // 만료 안됨
	}

	@Override
	public boolean isAccountNonLocked() {
		// 계정 잠금 여부
		return true;
	}

	@Override
	public boolean isCredentialsNonExpired() {
		// 비밀번호 만료 여부
		return true;
	}

	@Override
	public boolean isEnabled() {
		// 사용 가능한 계정인지 체크
		return true;
	}

}
  • Admin도 User와 유사함
package com.myapp.shoppingmall.entities;

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

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

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

import lombok.Data;

@Entity
@Table(name = "admin") // DB의 admin Table과 매칭
@Data
public class admin implements UserDetails{
	private static final long serialVersionUID = 2L;

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private int id;

	private String username;

	private String password;


	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		// 권한 목록을 Return(ADMIN 권한)
		return Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN"));
	}

	@Override
	public boolean isAccountNonExpired() {
		// 계정 만료 여부
		return true; // 만료 안됨
	}

	@Override
	public boolean isAccountNonLocked() {
		// 계정 잠금 여부
		return true; // 잠기지 않음
	}

	@Override
	public boolean isCredentialsNonExpired() {
		// 비밀번호 만료 여부
		return true; // 만료 안됨
	}

	@Override
	public boolean isEnabled() {
		// 사용 가능한 계정인지 체크
		return true; // 사용 가능
	}

}
  • UserDetailsService를 사용할 때 admin, user Table둘 다 찾아서 확인
    +++ throw로 예외생기면 출력될거 같았는데 안됨 그냥 return으로 보내는게 낫지 않을까...
package com.myapp.shoppingmall.security;

import org.springframework.beans.factory.annotation.Autowired;
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.myapp.shoppingmall.dao.AdminRepository;
import com.myapp.shoppingmall.dao.UserRepository;
import com.myapp.shoppingmall.entities.User;
import com.myapp.shoppingmall.entities.admin;

@Service
public class UserDetailsServiceImp implements UserDetailsService {

	@Autowired
	private UserRepository userRepo;

	@Autowired
	private AdminRepository adminRepo;
	
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		// DB에서 필요한 유저 또는 관리자정보를 읽어온다. (입력 parameter는 username)
		User user = userRepo.findByUsername(username);
		admin admin = adminRepo.findByUsername(username);
		
		if(admin != null) return admin;
		if(user != null) return user;
		
		throw new UsernameNotFoundException("유저 " + username + "이 없습니다.");
	}

}

Login 페이지

  • name, id 값 반드시 있어야함 없으면 오류생김
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head th:replace="/fragments/head :: head-front"></head>
  <body>
    <nav th:replace="/fragments/nav :: nav-front"></nav>
    <main role="main" class="container-fluid mt-5">
      <div class="row">
        <div th:replace="/fragments/categories :: categories"></div>
        <div class="col"></div>
        <div class="col-6">
          <div class="display-4">로그인</div>
          <form method="post" th:action="@{/login}">
            <div class="form-group">
              <label for="">유저이름</label>
              <input type="text" class="form-control" name="username" id="username" placeholder="유저이름" />
            </div>
            <div class="form-group">
              <label for="">비밀번호</label>
              <input type="password" class="form-control" name="password" id="password" placeholder="비밀번호" />
            </div>
            <div class="form-group mt-5">
              <button class="btn btn-danger mr-2">로그인</button>
              <a class="btn btn-primary" th:href="@{/register}">가입하기</a>
            </div>
          </form>
        </div>
        <div class="col"></div>
      </div>
    </main>
    <footer th:replace="/fragments/footer :: footer"></footer>
  </body>
</html>


회원가입

  • 회원가입시 입력한 데이터들을 Post로 받아서 유효성검사, 비밀번호 확인체크
package com.myapp.shoppingmall.security;

import javax.validation.Valid;

import org.springframework.beans.factory.annotation.Autowired;
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 com.myapp.shoppingmall.dao.UserRepository;
import com.myapp.shoppingmall.entities.User;

@Controller
@RequestMapping("/register")
public class RegistrationController {

	@Autowired
	private UserRepository userRepo;
	
	@Autowired // 비밀번호 암호화
	private PasswordEncoder passwordEncoder;
	
	@GetMapping
	public String register(User user) {
		return "register";
	}
	
	@PostMapping
	public String register(@Valid User user, BindingResult bindingResult, Model model) {
		// 1. 유효성검사 시 문제가 있을 경우 돌아감
		if (bindingResult.hasErrors()) {
			return "register";
		}
		// 2. 비밀번호 체크가 실패했을 시 돌아감
		if (!user.getPassword().equals(user.getConfirmPassword())) {
			model.addAttribute("passwordNotMatch", "비밀번호가 일치하지않습니다.");
			return "register";
		}
		// 3. 비밀번호를 암호화해서 입력
		user.setPassword(passwordEncoder.encode(user.getPassword()));
		// 4. DB에 새 유저 저장
		userRepo.save(user);
		
		return "redirect:/login"; // 회원가입이 성공적으로 끝나면 login페이지로 이동
	}
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head th:replace="/fragments/head :: head-front"></head>
  <body>
    <nav th:replace="/fragments/nav :: nav-front"></nav>
    <main role="main" class="container-fluid mt-5">
      <div class="row">
        <div th:replace="/fragments/categories :: categories"></div>
        <div class="col"></div>
        <div class="col-6">
          <div class="display-4">가입하기</div>
          <form method="post" th:object="${user}" th:action="@{/register}">
            <div th:if="${#fields.hasErrors('*')}" class="alert alert-danger">입력 내용을 확인해주세요</div>
            <div class="form-group">
              <label for="">유저이름</label>
              <input type="text" class="form-control" th:field="*{username}" placeholder="유저이름" />
              <span class="error" th:if="${#fields.hasErrors('username')}" th:errors="*{username}"></span>
            </div>
            <div class="form-group">
              <label for="">비밀번호</label>
              <input type="password" class="form-control" th:field="*{password}" placeholder="비밀번호" />
              <span class="error" th:if="${#fields.hasErrors('password')}" th:errors="*{password}"></span>
            </div>
            <div class="form-group">
              <label for="">비밀번호 확인</label>
              <input type="password" class="form-control" th:field="*{confirmPassword}" placeholder="비밀번호 확인" />
              <span class="error" th:if="${passwordNotMatch}" th:text="${passwordNotMatch}"></span>
            </div>
            <div class="form-group">
              <label for="">이메일</label>
              <input type="email" class="form-control" th:field="*{email}" placeholder="이메일" />
              <span class="error" th:if="${#fields.hasErrors('email')}" th:errors="*{email}"></span>
            </div>
            <div class="form-group">
              <label for="">전화 번호</label>
              <input type="text" class="form-control" th:field="*{phoneNumber}" placeholder="전화번호" />
              <span class="error" th:if="${#fields.hasErrors('phoneNumber')}" th:errors="*{phoneNumber}"></span>
            </div>

            <button class="btn btn-danger mb-5">가입하기</button>
          </form>
        </div>
        <div class="col"></div>
      </div>
    </main>
    <footer th:replace="/fragments/footer :: footer"></footer>
  </body>
</html>


  • 비밀번호 확인 실패 시 에러메시지 출력

기타

  • nav바에 로그인 중일 때 로그아웃버튼과 현재 로그인 중인 계정아이디 출력
  • 관리자페이지에서 Category 순서변경 시 기본페이지의 category 순서도 바뀌도록 수정
package com.myapp.shoppingmall;

import java.security.Principal;
import java.util.HashMap;
import java.util.List;

import javax.servlet.http.HttpSession;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;

import com.myapp.shoppingmall.dao.Cart;
import com.myapp.shoppingmall.dao.CategoryRepository;
import com.myapp.shoppingmall.dao.PageRepository;
import com.myapp.shoppingmall.entities.Category;
import com.myapp.shoppingmall.entities.Page;

// 모든 Controller에 적용
@ControllerAdvice
public class Common {

	@Autowired
	private PageRepository pageRepo;
	
	@Autowired
	private CategoryRepository categoryRepo;
	
	@ModelAttribute
	public void sharedData(Model model, HttpSession session, Principal principal) {
		if (principal != null) { // 인증된 상태
			model.addAttribute("principal" ,principal.getName()); // 유저네임을 전달
		}
		// cpages에 모든 페이지들을 순서대로 담아서 전달
		List<Page> cpages = pageRepo.findAllByOrderBySortingAsc();
		List<Category> categories = categoryRepo.findAllByOrderBySortingAsc();
		// 현재 Cart 상태
		boolean cartActive = false; // Cart가 존재하지 않을 때 false
		
		if (session.getAttribute("cart") != null) {
			@SuppressWarnings("unchecked")
			HashMap<Integer, Cart> cart = (HashMap<Integer, Cart>) session.getAttribute("cart");
			
			int size = 0; // Cart에 담긴 상품의 갯수
			int total = 0; // 총 가격
			
			for (Cart item : cart.values()) {// 장바구니 cart 객체들을 반복, cart.values()는 key값을 빼고 데이터만 반복
				size += item.getQuantity();
				total += item.getQuantity() * Integer.parseInt(item.getPrice()); // 상품 수량 * 가격 = 총 가격
			}
			model.addAttribute("csize", size);
			model.addAttribute("ctotal", total);
			cartActive = true;					// Cart가 존재함
		}
		
		model.addAttribute("cpages", cpages);
		model.addAttribute("ccategories", categories);
		model.addAttribute("cartActive", cartActive); // Cart가 존재하면 true, 없으면 false
	}
}
<nav th:fragment="nav-front" class="navbar navbar-expand-md navbar-dark bg-dark">
  <a class="navbar-brand" th:href="@{/}">🛒SHOP</a>
  <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarShop" aria-controls="navbarShop" aria-expanded="false" aria-label="Toggle navigation">
    <span class="navbar-toggler-icon"></span>
  </button>

  <div class="collapse navbar-collapse" id="navbarShop">
    <ul class="navbar-nav mr-auto">
      <li class="nav-item active" th:each="page : ${cpages}">
        <a class="nav-link" th:if="${page.slug != 'home'}" th:href="@{'/' + ${page.slug}}" th:text="${page.title}"></a>
      </li>
    </ul>
    <!-- 로그인 정보가 없으면 회원가입, 로그인표시 -->
    <ul class="navbar-nav" th:if="${principal == null}">
      <li class="nav-item active">
        <a class="nav-link" th:href="@{'/register'}" th:text="회원가입"></a>
      </li>
      <li class="nav-item active">
        <a class="nav-link" th:href="@{'/login'}" th:text="로그인"></a>
      </li>
    </ul>
    <!-- 로그인 정보가 있으면 로그아웃(Security) 표시 -->
    <form th:if="${principal != null}" th:action="@{/logout}" method="post">
      <span class="text-white" th:text="${'🖐하이, ' + principal}"></span>
      <button class="btn btn-secondary ml-2">로그아웃</button>
    </form>
  </div>
</nav>


Security 적용 중일 때 AJAX 작동을 하지 않으니 csrf값을 줘야함

  • AJAX를 사용하는 페이지와 Script에 값을 넣어준다.
  • thymeleaf 메소드를 사용하면 자동으로 csrf값이 삽입되있어서 따로 값을 주지않아도 됨
  <meta id="_csrf" name="_csrf" th:content="${_csrf.token}" />
  <meta id="_csrf_header" name="_csrf_header" th:content="${_csrf.headerName}" />
      let token = $("meta[name='_csrf']").attr('content');
      let header = $("meta[name='_csrf_header']").attr('content');
      $(document).ajaxSend(function (e, xhr, options) {
        xhr.setRequestHeader(header, token);
      });

0개의 댓글