스프링시큐리티에 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">© 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를 이용한 로그인을 구현해보았다.
'웹개발 > 스프링시큐리티' 카테고리의 다른 글
5. 스프링 시큐리티 - Remember Me 기능 구현 (0) | 2021.04.19 |
---|---|
4. 스프링 시큐리티 - 회원가입 (0) | 2021.04.19 |
2. 스프링시큐리티 - 로그인 페이지 변경 (0) | 2021.04.15 |
1. 스프링 시큐리티 - 기본 로그인 (1) | 2021.04.12 |
스프링 시큐리티에서 중요한 컴포넌트들 (0) | 2021.04.04 |