본문 바로가기
웹개발/스프링시큐리티

3. 스프링 시큐리티 - h2base 적용하기

by 어컴띵 2021. 4. 17.

스프링시큐리티에 db를 적용해보자. 여기서는 적용하기 쉽게 h2base적용해보자.

 

프로젝트 생성은 이미 되있다고 가정하고 진행을 한다.

 

먼저 h2db를 셋팅하고 유저 테이블과 유저엔티티를 만든후 유저를 한명 등록하고 스프링시큐리티에서 로그인하는 걸 한번 구현해보겠다.

build.grade

프로젝트는 gradle로 설정을 한다.

plugins {
    id 'org.springframework.boot' version '2.4.4'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'com.samlasoft'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'
}

test {
    useJUnitPlatform()
}

h2 db 설정

application.yml 설정

spring:
  h2:
    console:
      enabled: true
    port: 8888
  profiles:
    active: local
  datasource:
    hikari:
      jdbc-url: jdbc:h2:./data/testdb
      driver-class-name: org.h2.Driver
      username: sa
      password:
  jpa:
    hibernate:
      ddl-auto: update
    properties: 
      hibernate:
        format_sql: true

 

config 설정

h2db를 서버방식으로 띄운다. 이렇게 띄우면 서버가 종료되도 db정보가 유지된다.

@Configuration
@Profile("local")
public class H2ServerConfiguration {
    @Value("${spring.h2.port}")
    String port;

    @Bean
    @ConfigurationProperties("spring.datasource.hikari")
    public DataSource dataSource() throws SQLException {
        Server.createTcpServer("-tcp", "-tcpAllowOthers", "-tcpPort", port).start();
        return new com.zaxxer.hikari.HikariDataSource();
    }
}

서버시작시 h2 로그 확인

아래와 같은 로그가뜨면 정상적용 된 상태이다.

디렉토리구조

 

스프링 시큐리티 config 설정

SecurityConfig 클래스 작성

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http
                .csrf().disable()
                .authorizeRequests( authorize -> authorize
                        .antMatchers("/css/**","/js/**","/index").permitAll()
                        .antMatchers("/user/**").hasRole("USER")
                )
                .formLogin( formLogin -> formLogin.loginPage("/login"));
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

 

HTML 페이지 작업

페이지 레이아웃작업

전체적인 페이지 레이아웃을 간단하게 잡는다. 부트스트랩에서 샘플 페이지를 가지고 작업을 한다.

header.html

<head> 부분과, 네이게이션 부분은 import 한다.

<!doctype html>
<html lang="en">
<head>
  <th:block th:replace="layout/head"/>
</head>
<body>
<div th:replace="layout/nav::nav"></div>

head.html

html의 head부분을 별도로 분리한다.

<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="Mark Otto, Jacob Thornton, and Bootstrap contributors">
<meta name="generator" content="Jekyll v4.1.1">
<title>Admin Template</title>
<link rel="canonical" href="https://getbootstrap.com/docs/4.5/examples/starter-template/">
<!-- Bootstrap core CSS -->
<link href="/css/bootstrap.min.css" rel="stylesheet">
<style>
  .bd-placeholder-img {
    font-size: 1.125rem;
    text-anchor: middle;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
  }
  @media (min-width: 768px) {
    .bd-placeholder-img-lg {
      font-size: 3.5rem;
    }
  }
</style>
<!-- Custom styles for this template -->
<link href="/css/starter-template.css" rel="stylesheet">

nav.html

네비게션 부분을 별로도 분리한다. sec 태그라이브러리를 써서 로그인전과 후를 표시한다.

<nav th:fragment="nav" class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
  <a class="navbar-brand" href="#">AT</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 active">
      <a class="nav-link" href="/">Home <span class="sr-only">(current)</span></a>
    </li>
  </ul>
  <ul class="navbar-nav ml-auto">
    <li sec:authorize="!isAuthenticated()">
      <a class="nav-link" href="/login">로그인</a>
    </li>
    <li sec:authorize="isAuthenticated()">
      <a class="nav-link" href="/user/profile">프로필</a>
    </li>
    <li sec:authorize="isAuthenticated()">
      <a class="nav-link" href="/logout">로그아웃</a>
    </li>
  </ul>
</div>
</nav>

footer.html

하단 부분을 별도로 분리한다.

<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script>window.jQuery || document.write('<script src="/js/vendor/jquery.slim.min.js"><\/script>')</script><script src="/js/bootstrap.bundle.min.js"></script>
</body>
</html>

index 페이지작성

index.html

<th:block th:replace="layout/header"/>
<main role="main" class="container">
  <div class="starter-template">
    <h1>Spring Security DEMO</h1>
    <p class="lead">스프링 시큐리티 데모 페이지 입니다.</p>
  </div>
</main><!-- /.container -->
<th:block th:replace="layout/footer"/>

login 페이지 작성

/login/index.html

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="Mark Otto, Jacob Thornton, and Bootstrap contributors">
    <meta name="generator" content="Jekyll v4.1.1">
    <title>Signin Template · Bootstrap</title>

    <link rel="canonical" href="https://getbootstrap.com/docs/4.5/examples/sign-in/">

    <!-- Bootstrap core CSS -->
<link href="/css/bootstrap.min.css" rel="stylesheet">

    <style>
      .bd-placeholder-img {
        font-size: 1.125rem;
        text-anchor: middle;
        -webkit-user-select: none;
        -moz-user-select: none;
        -ms-user-select: none;
        user-select: none;
      }

      @media (min-width: 768px) {
        .bd-placeholder-img-lg {
          font-size: 3.5rem;
        }
      }
    </style>
    <!-- Custom styles for this template -->
    <link href="/css/signin.css" rel="stylesheet">
  </head>
  <body class="text-center">
    <form action="/login" class="form-signin" method="post">
  <a href="/"><img class="mb-4" src="/images/bootstrap-solid.svg" alt="" width="72" height="72"></a>
  <h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
  <label for="username" class="sr-only">User name</label>
  <input type="text" name="username" id="username" class="form-control" placeholder="User name" required autofocus>
  <label for="inputPassword" class="sr-only">Password</label>
  <input type="password" name="password" id="inputPassword" 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>
</body>
</html>

profile페이지 작성

/user/profile.html

<th:block th:replace="layout/header"/>
<main>
    <div class="starter-template">
        <h1>Logged in user: <span sec:authentication="name"></span></h1>
        <p class="lead">Roles: <span sec:authentication="principal.authorities"></span></p>
    </div>
</main>
<th:block th:replace="layout/footer"/>

엔티티 작성

user 엔티티 생성

User 클래스는 UserDetails의 구현체로 스프링 시큐리티에서 유저정보를 관리하는데 사용하는 클래스이다. 유저는 하나이상의 권한을 가질수 있는 구조로 되어있다. is로 시작하는 기본 메서드(계정을 막는역할)들은 일단 true로 설정을 해두었다.

@Entity
@Table(name = "user")
public class User implements UserDetails {

    @Id
    private String username;
    private String password;

    @OneToMany(mappedBy = "user")
    private List<Role> authorities = new ArrayList<>();

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        /*GrantedAuthority grantedAuthority = new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return "ROLE_USER";
            }
        };
        ArrayList list = new ArrayList();
        list.add(grantedAuthority);*/
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    public void setAuthorities(Collection<? extends GrantedAuthority> authorities){
        this.authorities = (List<Role>) authorities;
    }
}

role 엔티티 생성

Role클래스는 권한을 나타내는 GrantedAuthority인터페이스의 구현체이다. 스프링 시큐리티에서 권한을 나타낼때 사용하는 클래스이며 user에 권한리스트에 저장된다.

@Setter
@Getter
@Entity
@Table(name = "role")
public class Role implements GrantedAuthority {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    @Id
    private Long id;
    private String authority;
    @ManyToOne
    private User user;

    @Override
    public String getAuthority() {
        return authority;
    }

    @Override
    public int hashCode() {
        return this.authority.hashCode();
    }

    @Override
    public String toString() {
        return this.authority;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj instanceof Role) {
            return this.authority.equals(((Role) obj).authority);
        }
        return false;
    }
}

User, Role데이타 insert

user 테이블

비밀번호는 user123이다

insert into user (user,password) values( 'user','$2a$10$cqQ7BoLX6hJ4KXYYoKqBEOhcNWGhF8.X/mgdHB4R.pJdZz6zz3mka');

role 테이블

insert into role(id,authority,user_username) values(1,'USER_ROLE','user');

Reporistory 작성

UserRepository

@Repository
public interface UserRepository extends JpaRepository<User, String> {
}

RoleRepository

@Repository
public interface RoleRepository extends JpaRepository<Role, Long> {
    public List<Role> findAllByUser(User user);
}

UserDetail 클래스 작성

UserDetail클래스는 UserDetailService의 구현체로 스프링시큐리티에서 로그인시 유저를 불러올때 이 클래스를 사용하게 된다. loadUserByUsername메소드를 사용해서 User를 반환한다.

User 엔티티와 Role엔티티의 관계는 1대다의 관계이며 OneToMany는 기본적으로 lazy로딩 이므로 repository.findById로 user를 가져왔을때 권한리스트에는 실제 권한 정보가 있는것이 아니고 프록시객체가 들어 있다. 그래서 해당 권한을 불러오려고 할때 오류가 발생하여 권한이 ROLE_USER가 아닌 ROLE_ANONYMOUS로 반환된다. 그래서 아래 코드에서는 권한을 직접가져와서 user.setAuthroites메서드로 직접 넣어주었다. 

@Service
public class UserService implements UserDetailsService {

    JpaRepository<User,String> repository;
    RoleRepository roleRepository;

    @Autowired
    public UserService(JpaRepository userRepository,RoleRepository roleRepository){
        this.repository = userRepository;
        this.roleRepository = roleRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = repository.findById(username).orElseThrow(() -> new UsernameNotFoundException("username not found!"));
        List list = roleRepository.findAllByUser(user);
        user.setAuthorities(list);
        return user;
    }
}

컨트롤러 작성

MainController

@Controller
public class MainController {
    @RequestMapping("/login")
    public String login(){
        return "login/index";
    }
}

UserController

@Controller
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/profile")
    public String profile(){
        return "user/profile";
    }
}

로그인 테스트

메인페이지

로그인

로그인후 메인페이지

상단메뉴에 프로필, 로그아웃 메뉴로 변경된다.

profile페이지

현재 유저명과 권한을 보여준다.

마치며

이상 h2database를 이용하여 간단하게 로그인 테스트를 해보았다. 먼저 h2databae를 이용한 로그인을 구현하기 위해서 UserDetails, GrantedAuthority의 구현체인 User, Role엔티티를 작성하고, JpaRepository를 상속받는 UserRepostiry, RoleReposity를 작성한다. 그리고 로그인시 user 정보를 가져올 UserDetailsService 인터페이스를 구현한 UserService를 작성해서 로그인시 유저정보과 권한정보를 가져올수 있게 한다. 그런 다음 메인페이지, 로그인페이지, 유저정보 페이지를 작성하고 정상적으로 로그인되는 테스트한다.

 

이상 간단하게 h2database를 이용한 로그인을 구현해보았다.