3월 30일

SJY0000·2022년 3월 30일
0

Springboot

목록 보기
5/24

오늘 배운 것

  • Security 사용하기(2)
  • User 등록 페이지 만들기
  • Error 페이지 설정
  • custom login페이지 사용하기
  • navbar 수정

Security 사용하기

  • DB에 테이블을 직접 만들고 설정
  • User 정보를 관리할 객체 만들기
package com.myapp.pma.entities;

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

@Entity
//테이블 자동생성을 사용하지 않고 DB에 테이블을 직접 생성해서 클래스 이름과 다르기 때문에 적어줘야함)
@Table(name = "user_accounts") 
public class UserAccount {
	@Id	// 자동증가
	@GeneratedValue(strategy = GenerationType.IDENTITY) // DB에서 자동으로 생성
	@Column(name = "user_id") // 테이블의 Column 이름과 변수명이 다르기 때문에 일치시키기 위해 적어줘야함
	private long userId;
	
	@Column(name = "username") // 테이블의 Column 이름과 변수명이 다르기 때문에 일치시키기 위해 적어줘야함
	private String userName;
	
	private String email;
	private String password;
	private String role = "ROLE_USER";
	private boolean enabled = true;
	
	public UserAccount() {
		// 빈 유저 객체 생성시 role은 유저, enable은 true
		this.role = "ROLE_USER";
		this.enabled = true;
	}
	public long getUserId() {
		return userId;
	}
	public void setUserId(long userId) {
		this.userId = userId;
	}
	public String getUserName() {
		return userName;
	}
	public void setUserName(String userName) {
		this.userName = userName;
	}
	public String getEmail() {
		return email;
	}
	public void setEmail(String email) {
		this.email = email;
	}
	public String getPassword() {
		return password;
	}
	public void setPassword(String password) {
		this.password = password;
	}
	public String getRole() {
		return role;
	}
	public void setRole(String role) {
		this.role = role;
	}
	public boolean isEnabled() {
		return enabled;
	}
	public void setEnabled(boolean enabled) {
		this.enabled = enabled;
	}
}
  • User 정보 등록할 페이지 만들기
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
      rel="stylesheet"
      integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
      crossorigin="anonymous"
    />
    <title>시큐리티</title>
  </head>
  <body style="background-color: #ededed">
    <div style="background-color: #337ab7; height: 50px"></div>
    <div class="container-fluid" style="margin-top: 30px">
      <div class="row col-lg-4 mx-auto" style="margin-top: 40px; background-color: #fff; padding: 20px; border: solid 1px #ddd">
        <form autocomplete="off" th:action="@{/register/save}" th:object="${userAccount}" method="post" class="form-signin" role="form">
          <h3 class="form-signin-heading">가입하기 FORM</h3>
          <div class="form-group">
            <div class="mb-2">
              <input type="text" th:field="*{userName}" placeholder="유저이름" class="form-control" />
            </div>
          </div>
          <div class="form-group">
            <div class="mb-2">
              <input type="text" th:field="*{email}" placeholder="이메일" class="form-control" />
            </div>
          </div>
          <div class="form-group">
            <div class="mb-2">
              <input type="password" th:field="*{password}" placeholder="패스워드" class="form-control" />
            </div>
          </div>
          <!-- thymeleaf를 사용하면 아래의 것은 필요 없음-->
          <!-- <input type="hidden" name="_csrf" th:value="${_csrf.token}"> -->
          <div class="form-group">
            <div class="d-grid mb-2">
              <button type="submit" class="btn btn-primary">가입하기</button>
            </div>
          </div>
          <!-- <span th:utext="${successMessage}"></span> -->
        </form>
      </div>
    </div>
  </body>
</html>

  • 가입하기 페이지에서 Controller로 전송
  • Password 암호화 하여 DB에 저장
package com.myapp.pma.controllers;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

import com.myapp.pma.dao.UserAccountRepository;
import com.myapp.pma.entities.UserAccount;

@Controller
public class SecurityController {

	@Autowired
	private UserAccountRepository userRepo;
	
	// 암호화는 저장할 때와 인증할때 필요함
	@Autowired
	private BCryptPasswordEncoder bEncoder;
	
	// 가입하기 화면 표시
	@GetMapping("/register")
	public String register(Model model) {
		UserAccount userAccount= new UserAccount();
		model.addAttribute("userAccount", userAccount);
		return "security/register";
	}
	
	@PostMapping("/register/save")
	public String saveUser(Model model, UserAccount user) {
		//비밀번호 암호화 후 저장
		// 넘어온 user정보에서 비밀번호만 얻어서 BCryptPasswordEncoder로 인코딩한 후 저장
		user.setPassword(bEncoder.encode(user.getPassword()));
		userRepo.save(user);
		return "redirect:/";
	}
}

  • 메모리에 임시등록해서 사용하는 방식에서 DB에 저장된 데이터를 불러와 인증하는 방식으로 전환
  • BCryptPasswordEncoder객체로 저장되어 있는 password를 decode해서 입력한 비밀번호와 일치하는지 확인
package com.myapp.pma.security;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
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;

// Security 사용순서
//1. Security 설정을 위해서 WebSecurityConfigurerAdapter 상속 받음
//2. 어노테이션 @EnableWebSecurity
@EnableWebSecurity
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
	
	@Autowired
	private DataSource dataSource; // MySQL DB와 연결
	
	@Autowired
	private BCryptPasswordEncoder bCryptPasswordEncoder; // 패스워드 인코딩 객체
	
	@Override // 3. 인증
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		// 메모리에 아이디, 비밀번호, 역할(권한)설정
		auth.jdbcAuthentication()
			.usersByUsernameQuery("select username,password,enabled from user_accounts where username = ? ") // DB에 있는지 확인
			.authoritiesByUsernameQuery("select username,role from user_accounts where username = ? ")
			.dataSource(dataSource)
			.passwordEncoder(bCryptPasswordEncoder); // 암호화된 패스워드를 디코딩해서 입력된 비밀번호와 비교
	}
	
	@Override // 4. 허가
	protected void configure(HttpSecurity http) throws Exception {
		http.authorizeHttpRequests()
			.antMatchers("/projects/new").hasRole("ADMIN") // 새 프로젝트는 관리자만
			.antMatchers("/projects/save").hasRole("ADMIN")
			.antMatchers("/employees/new").hasRole("ADMIN") // 새 직원은 관리자만
			.antMatchers("/employees/save").hasRole("ADMIN")
			.antMatchers("/employees/").authenticated() // 인증된 유저
			.antMatchers("/projects/").authenticated()
			.antMatchers("/","/**").permitAll()				// 누구든 접근 가능
			.and()
			.formLogin(); // 로그인창 사용
	
		
		// Security에서는 기본적으로 csrf 방지가 적용중
//		http.csrf().disable(); // 사용자 의도치 않게 행동하는 것을 방지, save 후 redirect 하는 과정에서 csrf 룰에 위배되어 에러 출력
	}
}

Error 페이지 설정

  • application.properties에 기본적으로 설정된 에러페이지를 사용되지 않게 설정
    server.error.whitelabel.enabled=false

  • 에러발생 시 출력되는 코드를 받아서 코드에 따라 에러페이지를 다르게 이동함

package com.myapp.pma.controllers;

import javax.servlet.RequestDispatcher;
import javax.servlet.http.HttpServletRequest;

import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class AppErrorController implements ErrorController {
	
	// 에러 발생시 주소가 "/error"로 들어온다.
	@GetMapping("/error")
	public String handleError(HttpServletRequest request) {
		// 에러 상태 코드 확인
		Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
		
		if (status != null) { // 에러가 맞을 시
			Integer statusCode = Integer.valueOf(status.toString()); // 403, 404, 500
			
			if (statusCode == HttpStatus.NOT_FOUND.value()) {
				return "errorpages/404";
			}
			else if (statusCode == HttpStatus.INTERNAL_SERVER_ERROR.value()) {
				return "errorpages/500";
			}
			else if (statusCode == HttpStatus.FORBIDDEN.value()) {
				return "errorpages/403";
			}
		}
		
		// 위의 에러상태가 아닐 경우 그냥 error 페이지로 이동
		return "errorpages/error";
		
	}
}
  • 커스텀한 Error페이지를 준비


Login 페이지 설정하기

  • SecurityConfiguration의 허가 메소드를 수정
			.formLogin(form -> form.loginPage("/login")
									.permitAll()) // 커스텀 로그인페이지 사용
			.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout")); // 로그아웃 추가, 로그인 페이지를 새로 만들면 로그아웃도 새로 설정
	
  • templates 폴더에 login.html 파일을 생성

  • 인증이 되어야 접근할 수 있는 페이지에 기본 로그인 페이지가 아닌 Custom한 로그인페이지로 이동하게 됨


  • layouts.html의 navbar를 수정
    가입하기, 로그인 버튼 추가
    로그인 시 인증된 user의 username과 로그아웃버튼 출력
          <ul class="navbar-nav me-5" th:if="${principal == null}">
            <li class="nav-item">
              <a class="nav-link active" th:href="@{/register}">가입하기</a>
            </li>
            <li class="nav-item">
              <a class="nav-link active" th:href="@{/login}">로그인</a>
            </li>
          </ul>
          <form th:if="${principal != null}" method="post" th:action="@{/logout}">
            <span class="text-white" th:text="${'Hi🥇, ' + principal}"></span>
            <button class="btn btn-secondary">로그아웃</button>
          </form>
  • ControllerAdvice는 모든 Controller에 적용하게 해줌
  • 모든 페이지에 인증된 user의 정보를 보냄
package com.myapp.pma;

import java.security.Principal;

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

@ControllerAdvice // 모든 Controller에 적용(모든 주소에 적용)
public class Common {

	// 각 Controller가 화면(뷰)에 보내는 Model객체에 추가
	@ModelAttribute
	public void sharedData(Model model, Principal principal) {
		
		// Principal은 Security인증시 인증된 유저정보를 담고 있는 객체
		if (principal != null) {
			model.addAttribute("principal", principal.getName()); // 인증 유저의 유저네임을 전달
		}
	}
}

0개의 댓글