# 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
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("/"); // 예외 발생시 홈으로 이동
}
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;
}
}
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; // 사용 가능
}
}
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 + "이 없습니다.");
}
}
<!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>
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>
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>
<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);
});