IT 프로젝트/블로그 만들기

[Spring Boot] 스프링 부트 프로젝트/블로그 만들기 - 로그인(login) 페이지 만들기

happygram 2018. 11. 17. 14:40

작성 중인 '블로그 프로젝트' 에서는 기본적으로 '관리자' 가 작성한 글들을 '사용자' 가 읽을 수 있도록 합니다.

사용자는 주로 조회를 할 수 있고, 관리자는 카테고리 추가/수정/삭제, 글 추가/수정/삭제 등의 블로그 전체를 관리 할 수 있도록 합니다.

사용자가 아닌 관리자를 식별하기 위해서 로그인 기능이 필요합니다.


로그인 페이지를 구현하고, Spring boot Web Security 기능을 이용하여 어떤 기능으로 어떻게 처리 하는 지 소개하도록 하겠습니다.


참조 URL


데이터베이스

테이블 정의

users

authorities


개체 관계 다이어그램(ERD)


데이터 정의어(DDL)
CREATE TABLE `users` (
  `username` varchar(50) COLLATE utf8_bin NOT NULL COMMENT '사용자 아이디',
  `password` varchar(500) COLLATE utf8_bin NOT NULL COMMENT '비밀번호',
  `enabled` tinyint(1) NOT NULL COMMENT '사용 여부',
  PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
CREATE TABLE `authorities` (
  `username` varchar(50) COLLATE utf8_bin NOT NULL COMMENT '사용자 아이디',
  `authority` varchar(50) COLLATE utf8_bin NOT NULL COMMENT '권한',
  UNIQUE KEY `ix_auth_username` (`username`,`authority`),
  CONSTRAINT `fk_authorities_users` FOREIGN KEY (`username`) REFERENCES `users` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

데이터 조작어(DML)
INSERT INTO users
(username, password, enabled)
VALUES('happygram', '$2a$10$Ev7ngM/5BdtA7iRI9PkmduvXQFYA9T0vz0zl3KFIbe5JTQ7aN2Fuu', 1);
users 테이블의 password 는 Spring boot 'BCryptPasswordEncoder' 클래스를 이용하여 생성 하였습니다.
main 클래스에서 다음처럼 간단히 수행하여 추출할 수 있습니다.
System.err.println(new BCryptPasswordEncoder().encode("happygram"));

INSERT INTO authorities
(username, authority)
VALUES('happygram', 'ROLE_ADMIN');
authorities 테이블의 authority 는 'ROLE_' 의 Prefix 를 붙여서 정의합니다.

Dependencies

build.gradle
dependencies {
    compile("org.springframework.boot:spring-boot-starter-security")
}

Spring boot 라이브러리 중 'security' 를 사용합니다.

로그인 및 로그아웃 처리에 유용하고, 보안 관련한 기능도 설정할 수 있습니다.


Spring boot Security 참조 문서


소스

login.html


<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="context-path" th:content="@{/}"/>
    
    <title>HM & SB Blog</title>
    
    <!-- login -->
    <link th:href="@{/css/login.css}" rel="stylesheet">
    
    <!--  Common header -->
    <th:block th:include="common/include-link"></th:block>
</head>
<body>
    <!-- Common script -->
    <th:block th:include="common/include-script"></th:block>
    
    <div class="container centered text-center">
        <form class="form-signin" method="POST" th:action="@{/login}">
            <h1 class="h3 mb-3 font-weight-normal">[[${login_message}]]</h1>
            <label for="username" class="sr-only">ID</label>
                <input type="text" id="username" name="username" class="form-control" placeholder="ID" required="" autofocus="">
            <label for="password" class="sr-only">Password</label>
                <input type="password" id="password" name="password" class="form-control" placeholder="Password" required="">
            <div class="checkbox mb-3">
                <label><input type="checkbox" value="remember-me"> Remember me</label>
            </div>
            <div align="center" th:if="${param.error}">
                <p style="font-size: 20; color: #FF1C19;">ID or Password invalid, please verify</p>
            </div>
            <button class="btn btn-primary btn-block" type="submit">Sign in</button>
        </form>
    </div>
</body>
</html>
cs


templates 패키지에 'login.html' 파일을 추가 하였습니다.

앞서 소개한 글에서는 'index.html' 파일을 추가 하였고, 해당 파일이 레이아웃이 되어서, 다른 페이지를 포함하는 형태로 소개 하였습니다.

'login.html' 은 'index.hmlt' 과 별개의 파일로 로그인 시도 시 별도로 로딩 되도록 구성 하였습니다. 즉, 단일 페이지 입니다.


공통적으로 사용하는 'include-link.html', 'include-script.html' 페이지들을 포함 시켰고, 로그인 페이지에서 필요한 파일은 'login.css' 파일에 기술하고 포함 시켰습니다.


'username', 'password' 를 입력하고, 'Sign in' 버튼을 누르면 '/login' 'POST' 형태로 로그인을 시도를 합니다.

[[${login_message}]] : thymeleaf 문법으로, 서버에서 받은 정보를 출력합니다.

th:if="${param.error}" : 로그인 실패 시 '/login?error' 형태로 리다이렉트 되고, 해당 영역을 보여줍니다.


LoginController.java
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/login")
public class LoginController {
	
	@GetMapping
	public String viewLogin(Model model) {
		// View attribute
		model.addAttribute("login_message", "로그인이 필요합니다.");
		return "login";
	}
	
}

'/login' 의 'GET' 요청이 있는 경우 'login.html' 을 로딩 합니다.

해피그램의 '블로그 만들기' 프로젝트에서는 '/login' 을 요청하기 위한 버튼을 따로 만들어두지 않았습니다.

사용자 입장에서는 필요한 기능이 아니기 때문입니다.

필요한 경우 URL 에 직접 타이핑 하여 접근하도록 하였습니다.

model.addAttribute("login_message", "로그인이 필요합니다.") : 'login_message' 변수에 담아서 Front-End 로 전달합니다.


WebSecurityConfig.java
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.builders.WebSecurity;
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;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
	@Autowired
	private DataSource dataSource;
	
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/write", "/write/*").authenticated()
                .antMatchers("/admin", "/admin/*").authenticated()
                .antMatchers("/users", "/users/*").authenticated()
                .anyRequest().permitAll()
                .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
                .defaultSuccessUrl("/")
                .and()
            .logout()	
            	.logoutUrl("/logout")
            	.logoutSuccessUrl("/")
            	.invalidateHttpSession(true)
                .permitAll();
    }
    @Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth
			.jdbcAuthentication()
				.dataSource(dataSource)
				.passwordEncoder(new BCryptPasswordEncoder())
		;
	}
	@Override
	public void configure(WebSecurity web) throws Exception {
		web
			.ignoring()
				.antMatchers("/favicon.ico", "/css/**", "/image/**", "/js/**", "/webjars/**");
	}
}

Spring boot Web Security 기능을 이용하여 로그인 처리를 합니다.

로그인 Service 기능을 따로 구현하여 처리할 수도 있지만, Spring boot 에서 제공하는 좋은 기능을 이용합니다.


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/write", "/write/*").authenticated()
                .antMatchers("/admin", "/admin/*").authenticated()
                .antMatchers("/users", "/users/*").authenticated()
                .anyRequest().permitAll()
                .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
                .defaultSuccessUrl("/")
                .and()
            .logout()	
            	.logoutUrl("/logout")
            	.logoutSuccessUrl("/")
            	.invalidateHttpSession(true)
                .permitAll();
    }

Front-End 에서 '/write', '/admin', '/users' 에 대한 요청이 있는 경우 모두 인증을 진행하도록 하고, 그 외의 경우에는 모두 허용합니다.

'login' 요청이 있는 경우 인증을 진행하여, 인증이 성공한 경우 기본 경로로 설정한 '/' 으로 이동합니다.

'logout' 요청이 있는 경우 로그아웃 처리를 하고, 성공한 경우 기본 경로로 설정한 '/' 으로 이동합니다.


    @Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth
			.jdbcAuthentication()
				.dataSource(dataSource)
				.passwordEncoder(new BCryptPasswordEncoder())
		;
	}

인증 방식을 설정할 수 있습니다.

해피그램의 '블로그 만들기' 프로젝트에서는 데이터베이스를 이용하여 인증을 진행합니다.


기본적으로 'users', 'authorities' 테이블과 비교하여, 인증을 진행합니다.

('users' 테이블 - username, password, enabled, 'authorities' 테이블 - username, authority)

사용자 테이블을 이용하여 비교하고 싶은 경우 'usersByUsernameQuery', 'authoritiesByUsernameQuery' 메소드를 추가 호출하여, 쿼리를 지정할 수 있습니다.


비밀번호 인증 방식은 'BCrypt' 를 이용한 방식입니다. 그 외에도 여러가지 방식이 있습니다.

BCryptPasswordEncoder : 해시 문자열을 이용한 암호화 방식

BCrypt 관련 자료


	@Override
	public void configure(WebSecurity web) throws Exception {
		web
			.ignoring()
				.antMatchers("/favicon.ico", "/css/**", "/image/**", "/js/**", "/webjars/**");
	}

인증에서 제외할 경로를 설정합니다.

인증 대상이 아닌 resources 역할을 하는 자원들을 주로 제외 시킵니다.

CSS, Images, Javascript, Third Party Library 등이 될 수 있습니다.


결과

username, password 를 입력하고 'Sing In' 버튼을 클릭해서 로그인을 시도합니다.


실패하는 경우

param 변수에 error 담고, 로그인 페이지로 리다이렉트 합니다.

그리고 필요한 영역을 보여줍니다.


성공하는 경우

인증을 성공하면 '로그아웃' 버튼, 관리자 전용 메뉴, 버튼이 보입니다.


참조 도구 목록 - 도움에 감사를 드립니다.

https://dbeaver.io/