로그인 기능 만들기

 

spring.io 에서 의존성 복사

https://spring.io/guides/gs/securing-web/

 

Securing a Web Application

this guide is designed to get you productive as quickly as possible and using the latest Spring project releases and techniques as recommended by the Spring team

spring.io

implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.security:spring-security-test'

 

설정 코드도 복사

package com.example.securingweb;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
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.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			.authorizeHttpRequests((requests) -> requests
				.antMatchers("/", "/home").permitAll()
				.anyRequest().authenticated()
			)
			.formLogin((form) -> form
				.loginPage("/login")
				.permitAll()
			)
			.logout((logout) -> logout.permitAll());

		return http.build();
	}

	@Bean
	public UserDetailsService userDetailsService() {
		UserDetails user =
			 User.withDefaultPasswordEncoder()
				.username("user")
				.password("password")
				.roles("USER")
				.build();

		return new InMemoryUserDetailsManager(user);
	}
}

 

user 테이블 만들기

 

 

커스터마이징 하기

https://www.baeldung.com/spring-security-jdbc-authentication

 

Spring Security: Exploring JDBC Authentication | Baeldung

Explore the capabilities offered by Spring to perform JDBC Authentication using an existing DataSource configuration.

www.baeldung.com

 

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) 
  throws Exception {
    auth.jdbcAuthentication()
      .dataSource(dataSource)
      .usersByUsernameQuery("select email,password,enabled "
        + "from bael_users "
        + "where email = ?")
      .authoritiesByUsernameQuery("select email,authority "
        + "from authorities "
        + "where email = ?");
}
package com.toy.board.config;

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.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

import javax.sql.DataSource;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

   @Autowired
   private DataSource dataSource;

   @Bean
   public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
      http
         .authorizeHttpRequests((requests) -> requests
            .antMatchers("/", "/home").permitAll()
            .anyRequest().authenticated()
         )
         .formLogin((form) -> form
            .loginPage("/login")
            .permitAll()
         )
         .logout((logout) -> logout.permitAll());

      return http.build();
   }

   @Autowired
   public void configureGlobal(AuthenticationManagerBuilder auth)
         throws Exception {
      auth.jdbcAuthentication()
            .dataSource(dataSource)
            .passwordEncoder(passwordEncoder())
            .usersByUsernameQuery("select username, password, enabled "
                  + "from user "
                  + "where username = ?")
            .authoritiesByUsernameQuery("select email,authority "
                  + "from authorities "
                  + "where email = ?");
   }

   @Bean
   public PasswordEncoder passwordEncoder() {
      return new BCryptPasswordEncoder();
   }
}

 

 

role 테이블 생성

 

user_role 테이블 생성

WebSecurityConfig.java

package com.toy.board.config;

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.WebSecurityConfiguration;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

import javax.sql.DataSource;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

   @Autowired
   private DataSource dataSource;

   @Bean
   public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
      http
            .authorizeHttpRequests((requests) -> requests
                  .antMatchers("/", "/css/**").permitAll()
                  .anyRequest().authenticated()
            )
            .formLogin((form) -> form
                  .loginPage("/login")
                  .permitAll()
            )
            .logout((logout) -> logout.permitAll());

      return http.build();
   }



   @Autowired
   public void configureGlobal(AuthenticationManagerBuilder auth)
         throws Exception {
      auth.jdbcAuthentication()
            .dataSource(dataSource)
            .passwordEncoder(passwordEncoder())
            .usersByUsernameQuery("select username, password, enabled "
                  + "from user "
                  + "where username = ?")
            .authoritiesByUsernameQuery("select username, name "
                  + "from user_role ur inner join user u on ur.user_id = u.id "
                  + "inner join role r on ur.role_id = r.id "
                  + "where email = ?");
   }

   @Bean
   public static PasswordEncoder passwordEncoder() {
      return new BCryptPasswordEncoder();
   }
}

BeanCurrentlyInCreationExeption, Error creating bean with name 'webSecurityConfig' 에러가 발생 시

PasswordEncoder 에 static을 붙여주자.

 

 

 

login 페이지 만들기

 

Bootstrap 코드 복사

https://getbootstrap.com/docs/5.2/examples/sign-in/

 

Signin Template · Bootstrap v5.2

 

getbootstrap.com

 

login.html

<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">


    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">
    <link th:href="@{/css/signin.css}" rel="stylesheet">
    <title>Login</title>
</head>

<body class="text-center">

<main class="form-signin w-100 m-auto">
    <form>
        <img class="mb-4" src="https://getbootstrap.com/docs/5.2/assets/brand/bootstrap-logo.svg" alt="" width="72" height="57">
        <h1 class="h3 mb-3 fw-normal">Please sign in</h1>

        <div class="form-floating">
            <input type="text" class="form-control" id="floatingInput" placeholder="Username">
            <label for="floatingInput">Username</label>
        </div>
        <div class="form-floating">
            <input type="password" class="form-control" id="floatingPassword" placeholder="Password">
            <label for="floatingPassword">Password</label>
        </div>

      <!--  <div class="checkbox mb-3">
            <label>
                <input type="checkbox" value="remember-me"> Remember me
            </label>
        </div>-->
        <button class="w-100 btn btn-lg btn-primary" type="submit">Sign in</button>
        <p class="mt-5 mb-3 text-muted">&copy; 2017–2022</p>
    </form>
</main>



</body>


<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa" crossorigin="anonymous"></script>
</body>
</html>

 

signin.css

html,
body {
    height: 100%;
}

body {
    display: flex;
    align-items: center;
    padding-top: 40px;
    padding-bottom: 40px;
    background-color: #f5f5f5;
}

.form-signin {
    max-width: 330px;
    padding: 15px;
}

.form-signin .form-floating:focus-within {
    z-index: 2;
}

.form-signin input[type="email"] {
    margin-bottom: -1px;
    border-bottom-right-radius: 0;
    border-bottom-left-radius: 0;
}

.form-signin input[type="password"] {
    margin-bottom: 10px;
    border-top-left-radius: 0;
    border-top-right-radius: 0;
}

 

AccountController.java

package com.toy.board.web;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/account")
public class AccountController {

    @GetMapping("/login")
    public String login() {
        return "account/login";
    }
}

 

WebSecurityConfig.java

package com.toy.board.config;

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.WebSecurityConfiguration;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

import javax.sql.DataSource;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

   @Autowired
   private DataSource dataSource;

   @Bean
   public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
      http
            .authorizeHttpRequests((requests) -> requests
                  .antMatchers("/", "/css/**").permitAll()
                  .anyRequest().authenticated()
            )
            .formLogin((form) -> form
                  .loginPage("/account/login")
                  .permitAll()
            )
            .logout((logout) -> logout.permitAll());

      return http.build();
   }



   @Autowired
   public void configureGlobal(AuthenticationManagerBuilder auth)
         throws Exception {
      auth.jdbcAuthentication()
            .dataSource(dataSource)
            .passwordEncoder(passwordEncoder())
            .usersByUsernameQuery("select username, password, enabled "
                  + "from user "
                  + "where username = ?")
            .authoritiesByUsernameQuery("select u.username, r.name "
                  + "from user_role ur inner join user u on ur.user_id = u.id "
                  + "inner join role r on ur.role_id = r.id "
                  + "where username = ?");
   }

   @Bean
   public static PasswordEncoder passwordEncoder() {
      return new BCryptPasswordEncoder();
   }
}

 

 

login form 전송

 

error 메시지 추가

body에 코드 추가

       <div th:if="${param.error}">
            Invalid username and password.
        </div>
        <div th:if="${param.logout}">
            You have been logged out.
        </div>

 

 

alert 를 Bootstrap 에서 코드 복사

https://getbootstrap.com/docs/5.2/components/alerts/

class="alert alert-danger" role="alert"
<div th:if="${param.error}" class="alert alert-danger" role="alert">
    Invalid username and password.
</div>
<div th:if="${param.logout}" class="alert alert-primary" role="alert">
    You have been logged out.
</div>

 

login.html

<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">


    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">
    <link th:href="@{/css/signin.css}" rel="stylesheet">
    <title>Login</title>
</head>

<body class="text-center">

<main class="form-signin w-100 m-auto">
    <form th:action="@{/account/login}" method="post">
        <img class="mb-4" src="https://getbootstrap.com/docs/5.2/assets/brand/bootstrap-logo.svg" alt="" width="72" height="57">
        <h1 class="h3 mb-3 fw-normal">Please sign in</h1>
        <div th:if="${param.error}" class="alert alert-danger" role="alert">
            Invalid username and password.
        </div>
        <div th:if="${param.logout}" class="alert alert-primary" role="alert">
            You have been logged out.
        </div>

        <div class="form-floating">
            <input type="text" class="form-control" id="username" name="username" placeholder="Username">
            <label for="username">Username</label>
        </div>
        <div class="form-floating">
            <input type="password" class="form-control" id="password" name="password" placeholder="Password">
            <label for="password">Password</label>
        </div>

      <!--  <div class="checkbox mb-3">
            <label>
                <input type="checkbox" value="remember-me"> Remember me
            </label>
        </div>-->
        <button class="w-100 btn btn-lg btn-primary" type="submit">Sign in</button>
        <p class="mt-5 mb-3 text-muted">&copy; 2017–2022</p>
    </form>
</main>



</body>


<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa" crossorigin="anonymous"></script>
</body>
</html>

 

 

회원가입 페이지 만들기

 

register.html

<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">


    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">
    <link th:href="@{/css/signin.css}" rel="stylesheet">
    <title>Login</title>
</head>

<body class="text-center">

<main class="form-signin w-100 m-auto">
    <form th:action="@{/account/register}" method="post">
        <img class="mb-4" src="https://getbootstrap.com/docs/5.2/assets/brand/bootstrap-logo.svg" alt="" width="72" height="57">
        <h1 class="h3 mb-3 fw-normal">회원가입</h1>
        <div th:if="${param.error}" class="alert alert-danger" role="alert">
            Invalid username and password.
        </div>
        <div th:if="${param.logout}" class="alert alert-primary" role="alert">
            You have been logged out.
        </div>

        <div class="form-floating">
            <input type="text" class="form-control" id="username" name="username" placeholder="Username">
            <label for="username">Username</label>
        </div>
        <div class="form-floating">
            <input type="password" class="form-control" id="password" name="password" placeholder="Password">
            <label for="password">Password</label>
        </div>

      <!--  <div class="checkbox mb-3">
            <label>
                <input type="checkbox" value="remember-me"> Remember me
            </label>
        </div>-->
        <button class="w-100 btn btn-lg btn-primary" type="submit">회원가입</button>
        <p class="mt-5 mb-3 text-muted">&copy; 2017–2022</p>
    </form>
</main>



</body>


<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa" crossorigin="anonymous"></script>
</body>
</html>

 

User.java

package com.toy.board.domain;

import lombok.Data;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Data
@Entity
public class User {

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

    private String username;
    private String password;
    private Boolean enabled;

    @ManyToMany
    @JoinTable(
            name = "user_role",
            joinColumns = @JoinColumn(name = "user_id"),
            inverseJoinColumns = @JoinColumn(name = "role_id")
    )
    private List<Role> roles = new ArrayList<>();

}

 

 

Role,java

package com.toy.board.domain;

import lombok.Data;

import javax.persistence.*;
import java.util.List;

@Data
@Entity
public class Role {

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

    private String name;

    @ManyToMany(mappedBy = "roles")
    private List<User> users;
}

 

UserRespository.java

package com.toy.board.reopsitory;

import com.toy.board.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
}

 

UserService.java

package com.toy.board.service;

import com.toy.board.domain.Role;
import com.toy.board.domain.User;
import com.toy.board.reopsitory.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;


    public User save(User user) {
        String encodedPassword = passwordEncoder.encode(user.getPassword());
        user.setPassword(encodedPassword);
        user.setEnabled(true);
        Role role = new Role();
        role.setId(1L);
        user.getRoles().add(role);
        return userRepository.save(user);
    }
}

 

AccountController.java

package com.toy.board.web;

import com.toy.board.domain.User;
import com.toy.board.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/account")
public class AccountController {

    @Autowired
    private UserService userService;

    @GetMapping("/login")
    public String login() {
        return "account/login";
    }

    @GetMapping("/register")
    public String register() {
        return "account/register";
    }

    @PostMapping("/register")
    public String register(User user) {
        userService.save(user);
        return "redirect:/";
    }
}

 

role 테이블에 ROLE_USER 추가

 

 

login, logout 추가

 

의존성 추가

implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'

 

common.html

<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.w3.org/1999/xhtml">
<head th:fragment="head(title)">
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title th:text="${title}">Hello Spring Boot!</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">
    <link href="starter-template.css" th:href="@{/css/starter-template.css}" rel="stylesheet">
</head>

<body class="d-flex flex-column h-100">

<header th:fragment="menu(menu)">
    <!-- Fixed navbar -->
    <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
        <div class="container-fluid">
            <a class="navbar-brand" href="#">Spring Boot Tutorial</a>

            <div class="collapse navbar-collapse" id="navbarCollapse">
                <ul class="navbar-nav me-auto mb-2 mb-md-0">
                    <li class="nav-item">
                        <a class="nav-link" th:classappend="${menu} == 'home'? 'active'" aria-current="page" href="#" th:href="@{/}">홈</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" th:classappend="${menu} == 'board'? 'active'" href="#" th:href="@{/board/list}">게시판</a>
                    </li>
                </ul>
            </div>

                <a class="btn btn-outline-success" th:href="@{/account/login}"sec:authorize="!isAuthenticated()">Login</a>
                <a class="btn btn-outline-success" th:href="@{/account/register}"sec:authorize="!isAuthenticated()">회원가입</a>

            <form class="d-flex" role="search" th:action="@{/logout}" method="post" sec:authorize="isAuthenticated()">
                <!--                <input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">-->
                <span class="text-white mx-2 my-2" sec:authentication="name">사용자</span>
                <span class="text-white mx-4 my-2" sec:authentication="principal.authorities">권한</span>
                <button class="btn btn-outline-success" type="submit" >Logout</button>
            </form>
        </div>
    </nav>
</header>





<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa" crossorigin="anonymous"></script>
</body>
</html>

 

회원이 아닌 사람

"sec:authorize="!isAuthenticated()"
    <a class="btn btn-outline-success" th:href="@{/account/login}"sec:authorize="!isAuthenticated()">Login</a>
                <a class="btn btn-outline-success" th:href="@{/account/register}"sec:authorize="!isAuthenticated()">회원가입</a>

            <form class="d-flex" role="search" th:action="@{/logout}" method="post" sec:authorize="isAuthenticated()">
                <!--                <input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">-->
                <span class="text-white mx-2 my-2" sec:authentication="name">사용자</span>
                <span class="text-white mx-4 my-2" sec:authentication="principal.authorities">권한</span>
                <button class="btn btn-outline-success" type="submit" >Logout</button>
            </form>

 

 

복사했습니다!