온라인 강좌/유튜브 강의

Spring Boot 9. Spring Security를 이용한 로그인 처리

범박사 2024. 2. 19. 23:02

목표

1. Password Encode 구현

2. 로그인 했을 때/안했을 때에 따른 로그인 구현

 

register.html

<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">

<head>
    <link th:href="@{https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css}" rel="stylesheet">
    <link th:href="@{/css/signin.css}" rel="stylesheet">
    <title>회원가입</title>
</head>

<body class="text-center">
    <form class="form-signin" th:action="@{/account/register}" method="post">
        <a th:href="@{/}"><img class="mb-4" src="https://getbootstrap.com/docs/4.5/assets/brand/bootstrap-solid.svg"
                alt="" width="72" height="72"></a>
        <h1 class="h3 mb-3 font-weight-normal">회원가입</h1>
        <label for="username" class="sr-only">Username</label>
        <input type="text" id="username" name="username" class="form-control" placeholder="Username" required autofocus>
        <label for="inputPassword" class="sr-only">Password</label>
        <input type="password" id="inputPassword" name="password" class="form-control" placeholder="Password" required>
        <!-- <div class="checkbox mb-3">
            <label>
                <input type="checkbox" value="remember-me"> Remember me
            </label>
        </div> -->
        <button class="btn btn-lg btn-primary btn-block" type="submit">회원가입</button>
        <p class="mt-5 mb-3 text-muted">&copy; 2017-2020</p>
    </form>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
        crossorigin="anonymous"></script>
</body>

</html>

username과 password를 입력 후 회원가입 버튼을 누르면 /account/register 요청 발생

 

AccountController

@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:/";
    }
}

 

PostMapping(추가)로 요청 받아서 userService의 save메소드로 user 전달

localhost:8080으로 이동(redirext:/)

 

UserService

@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);
    }
}

PasswordEncoder를 Autowired 해서 encode 메소드를 통해 비밀번호 암호화 구현

role은 하드코딩으로 1l을 설정하여 return

그럼 이렇게 들어간다!

암호화 더 복잡한 줄 알았는데.. 생각보다 간단하게 구현돼서 당황

다만 복호화는 할 수 없는 일방향 암호화이다. enabled=1은 활성화된 유저임을 의미한다.

 

로그인 상태에 따라 nav의 버튼이 다르게 뜨고, 로그인 되지 않으면 게시판을 볼 수 없게 설정..하는 과정에서 맨 마지막에 문제가 생겼다. dependency를 추가했는데 구글링해서 온갖 해결법을 다 해봐도 dependency not found가 뜬다..

springsecurity5 빼고는 다 잘 되는데.. 하.. 어쩔 수 없이 일단 진행했다

 

login.html

<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">

<head>
    <link th:href="@{https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css}" rel="stylesheet">
    <link th:href="@{/css/signin.css}" rel="stylesheet">
    <title>Login</title>
</head>

<body class="text-center">
    <form class="form-signin" th:action="@{/account/login}" method="post">
        <a th:href="@{/}"><img class="mb-4" src="https://getbootstrap.com/docs/4.5/assets/brand/bootstrap-solid.svg"
                alt="" width="72" height="72"></a>
        <h1 class="h3 mb-3 font-weight-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>
        <label for="username" class="sr-only">Username</label>
        <input type="text" id="username" name="username" class="form-control" placeholder="Username" required autofocus>
        <label for="inputPassword" class="sr-only">Password</label>
        <input type="password" id="inputPassword" name="password" class="form-control" placeholder="Password" required>
        <!-- <div class="checkbox mb-3">
            <label>
                <input type="checkbox" value="remember-me"> Remember me
            </label>
        </div> -->
        <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
        <p class="mt-5 mb-3 text-muted">&copy; 2017-2020</p>
    </form>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
        crossorigin="anonymous"></script>
</body>

</html>

username과 passowrd를 입력해서 error가 발생하면 arror가 파라미터로 넘어가서 param.error 부분에 의해 alert 메세지가 출력된다. 로그인에 성공하면 /account/login 요청 발생

 

WebSecurityConfig

: Spring Security 구현 

package com.javalab.myhome.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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

import javax.sql.DataSource;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
    @Autowired
    private DataSource dataSource;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests((requests) -> requests
                        .requestMatchers("/","/account/register", "/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 join user u on ur.user_id = u.id "
                        + "join role r on ur.role_id = r.id "
                        + "where u.username = ?");
    }
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

 

@Configuration : Spring Security 구성 정의
@EnaleWebSecurity : Spring Security 활성화
DB와 연결하기 위해 DataSource Autowired

SecurityFilterChain : HttpSecurity 객체를 매개변수로 받아서 보안 구성 설정
.authorizeHttpRequests
.requestMatchers : 이곳에 정의한 url mapping은 모든 사용자에게 허용(permitAll)
.anyRequest().authenticated() : 그 외 모든 요청은 인증된 사용자에게만 허용.
.formLogin : 로그인에 성공하면 이동할 페이지
.logout : 로그아웃 구성

configureGlobal(AuthenticationManagerBuilder auth) : AuthenticationManagerBuilder를 통해 인증 관련 설정
jdbcAuthentication : JDBC 기반 사용자 인증 활성화
dataSource : DataSource 설정
passwordEncoder : 비밀번호의 인코딩 방식 설정
userByUsernameQuery : 사용자 정보 조회 쿼리
authoritiesByUsernameQuery : 사용자의 권한 정보 조회

passwordEncoder : PasswordEncoder Bean 정의

 

common.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">

<head th:fragment="head(title)">
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css"
        integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
    <link href="css/starter-templates.css" th:href="@{/css/starter-templates.css}" rel="stylesheet">
    <title th:text="${title}">Hello, Spring Boot!</title>
</head>

<body>
    <nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top" th:fragment="menu(menu)">
        <a class="navbar-brand" href="#">Spring Boot Tutorial</a>
        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault"
            aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>

        <div class="collapse navbar-collapse" id="navbarsExampleDefault">
            <ul class="navbar-nav mr-auto">
                <li class="nav-item" th:classappend="${menu == 'home'} ? 'active'">
                    <a class="nav-link" href="#" th:href="@{/}">홈<span class="sr-only"
                            th:if="${menu == 'home'}">(current)</span></a>
                </li>
                <li class="nav-item" th:classappend="${menu == 'board'} ? 'active'">
                    <a class="nav-link" href="#" th:href="@{/board/list}">게시판<span class="sr-only"
                            th:if="${menu == 'board'}">(current)</span></a>
                </li>
            </ul>
            <a class="btn btn-secondary my-2 mr-2 my-sm-0" th:href="@{/account/login}"
                sec:authorize="!isAuthenticated()">로그인</a>
            <a class="btn btn-secondary my-2 my-sm-0" th:href="@{/account/register}"
                sec:authorize="!isAuthenticated()">회원가입</a>
            <form class="form-inline my-2 my-lg-0" th:action="@{/logout}" method="POST"
                sec:authorize="isAuthenticated()">
                <!-- <input class="form-control mr-sm-2" type="text" placeholder="Search" area-label="Search"> -->
                <span class="text-white" sec:authentication="name">사용자</span>
                <span class="text-white mx-2" sec:authentication="principal.authorities">권한</span>
                <button class="btn btn-secondary my-2 my-sm-0" type="submit">Logout</button>
            </form>
        </div>
    </nav>
</body>

</html>

 

인증되지 않은 사용자는 로그인, 회원가입 버튼이 보이고

인증된 사용자는 사용자, 권한 정보와 로그아웃 버튼이 보인다

 

ㅋㅋ 어떻게든 dependency 문제 해결하고 싶어 구글링하다.. 찾아서 해결했다

내가 쓰는 버전이 spring-boot-starter-parent 3.2.2버전이기 때문에 Spring Security6를 사용해야 했다

강의보고 그냥 따라하기만 하니.. 이 문제일줄은 몰랐음 ^^!

 

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity6</artifactId>
    <version>3.1.2.RELEASE</version>
</dependency>