참고 사이트
https://blog.outsider.ne.kr/904
https://velog.io/@ssseungzz7/Java-Exception-handling
https://github.com/kdevkr/spring-demo-security
Spring Security와 h2-console 함께 쓰기(★★★★★) - https://github.com/HomoEfficio/dev-tips/blob/master/Spring%20Security%EC%99%80%20h2-console%20%ED%95%A8%EA%BB%98%20%EC%93%B0%EA%B8%B0.md
H2 에러 : https://sosohanya.tistory.com/27
New Password Storage In Spring Security 5 : https://www.baeldung.com/spring-security-5-password-storage
Default Password Encoder in Spring Security 5 : https://www.baeldung.com/spring-security-5-default-password-encoder
Registration with Spring Security – Password Encoding : https://www.baeldung.com/spring-security-registration-password-encoding-bcrypt
스프링 시큐리티(★★★★★) : https://github.com/kdevkr/spring-demo-security/tree/4.x
[스프링프레임워크] 스프링 시큐리티 -2.사용자 상세 서비스 선택 : https://m.blog.naver.com/kimnx9006/220634017538
Spring Security Custom UserDetailsService(DB) 구현하기 : https://fntg.tistory.com/189
X-Frame-Options in Spring Security 문제)
대응 : http.headers().frameOptions().sameOrigin(); : https://stackoverflow.com/questions/40165915/why-does-the-h2-console-in-spring-boot-show-a-blank-screen-after-logging-in
@EnableWebSecurity : https://m.blog.naver.com/kimnx9006/220633299198
@Configuration : http://tech.javacafe.io/spring/2018/11/04/%EC%8A%A4%ED%94%84%EB%A7%81-Configuration-%EC%96%B4%EB%85%B8%ED%85%8C%EC%9D%B4%EC%85%98-%EC%98%88%EC%A0%9C/
PasswordEncoder : https://stackoverflow.com/questions/49582971/encoded-password-does-not-look-like-bcrypt
PasswordEncoder : https://logical-code.tistory.com/106
Bcrypt 코딩시 주의사항 : http://yoonbumtae.com/?p=1202
Bcrypt 간단 설명 : https://www.joinc.co.kr/w/man/12/bcrypt
Bcrypt Password Generator : https://www.browserling.com/tools/bcrypt
외부 H2 이용 : https://www.baeldung.com/spring-boot-h2-database
H2 속성 : https://github.com/raycon/til/blob/master/framework/spring-boot-H2-database.md
H2 사용법 : https://developerhive.tistory.com/34
시큐리티 설정법 : https://sjh836.tistory.com/165
<scope>test</scope>가 있으면 제거해준다.
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
</dependency>
package com.example.demo.library.Security;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @Configuration
* 스프링의 @Configuration 어노테이션은 어노테이션기반 환경구성을 돕는다.
* 이 어노테이션을 구현함으로써 클래스가 하나 이상의 @Bean 메소드를 제공하고
* 스프링 컨테이가 Bean정의를 생성하고 런타임시 그 Bean들이 요청들을 처리할 것을 선언하게 된다.
*
* @EnableWebSecurity 애너테이션은 웹 보안을 활성화 한다.
* 하지만 그자체로는 유용하지 않고, 스프링 시큐리티가 WebSecurityConfigurer를 구현하거나
* 컨텍스트의 WebSebSecurityConfigurerAdapter를 확장한 빈으로 설정되어 있어야 한다.
*/
@Configuration
@EnableWebSecurity
public class LibrarySecurityConfig extends WebSecurityConfigurerAdapter implements WebMvcConfigurer {
// WebSecurityConfigurerAdapter 가 없을 때 기본 보안 설정을 한다. 하나 이상 발견되면 설정된 보안 설정을 사용한다.
@Autowired
private PasswordEncoder passwordEncoder;
public LibrarySecurityConfig() {
super(true); // 기본 보안 구성 비활성화, 공부하기위해 일부러 생성 원래는 없어도됨
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("index");
registry.addViewController("/login.html").setViewName("login"); // 매핑
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.securityContext()
.and().exceptionHandling()
// 예외 핸들링
.and().servletApi()
// 서블릿 API 통합
.and().httpBasic()
// http 기본인증
.and().logout().logoutSuccessUrl("/")
.and().headers()
// 로그아웃후 브라우저의 뒤로 가기 버튼을 사용하면 로그아웃이 성공하더라도 이전 페이지를 계속볼수 있다.
// 이는 브라우적 ㅏ페이지를 캐시한다는 사실과 관련이 있다.
// 그러므로 header() 구성 메소드로 보안 헤더를 활성화하면 브라우저가 페이지를 캐시하지 않도록 지시한다.
// no-cache 헤더 옆에서 콘텐츠 스니핑이 비활성화되고 x-frame 보호가 활성화된다.
.and().csrf()
.and().anonymous().principal("guest").authorities("ROLE_GUEST")
.and().rememberMe()
// 기본적으로 사용자 이름, 암호, remember-me 만료 시간 및 개인 키를 토큰으로 인코딩하고 쿠키로 사용자의 브라우저에 저장한다.
// 다음에 사용자가 동일한 웹 애플리케이션에 액세스하면 이 토큰이 감지돼 사용자가 잗종으로 로그인할 수 있따.
// 정적 Remeber-Me 토큰은 해커가 캡쳐할 수 있기 때문에 보안문제가 있다. 그러므로 롤링토큰을 지원하지만,
// 토큰을 유지하려면 데이터베이스가 필요하다. 자세한것은 다음게시물에 작성예정이다.
.and().formLogin().loginPage("/login.html").defaultSuccessUrl("/books.html").failureUrl("/login.html?error=true").permitAll()
// 폼기반 로그인
.and().authorizeRequests()
.mvcMatchers("/").permitAll()
// "/"들어오는 요청은 모두허용
.anyRequest().authenticated();
// 어떤요청이든 인증확인
}
// 인메모리 기반 사용자 인증
/*
스프링 보안에서 인증은 체인으로 연결된 하나 이상의 AuthenticationProvider에 의해 수행된다.
이중 하나가 사용자를 성공적으로 인증하면 해당 사용자는 애플리케이션에 로그인할 수 있다.
어느 인증 제공자라도 사용자가 비활성화됐거나 잠겼거나 자격 증명이 잘못됐다고 기록되거나 인증 제공자가
사용자를 인증할 수 없는 경우에는 사용자는 애플리케이션에 로그인할 수 없다.
스프링 보안은 암호(BCrypt 와 SCrypt 포함)를 암호화하는 여러 가지 알고리즘을 지원하며 이러한 알고리즘을 위한 기본 암호 인코더를 제공
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
UserDetails adminUser = User.builder()
.username("admin@books.io")
.password(passwordEncoder.encode("secret"))
.authorities("ADMIN","USER").build();
UserDetails normalUser = User.builder()
.username("marten@books.io")
.password(passwordEncoder.encode("user"))
.authorities("USER").build();
UserDetails disabledUser = User.builder()
.username("jdoe@books.net")
.password(passwordEncoder.encode("user"))
.disabled(true)
.authorities("USER").build();
auth.inMemoryAuthentication()
.withUser(adminUser)
.withUser(normalUser)
.withUser(disabledUser);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Spring Boot Recipes - Library</title>
</head>
<body>
<h1>Library</h1>
<a th:href="@{/books.html}" href="#">List of books</a>
<a th:href="@{/login.html}" href="#">Login</a>
</body>
</html>
login.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Login</title>
<link type="text/css" rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.2.10/semantic.min.css">
<style type="text/css">
body {
background-color: #DADADA;
}
body > .grid {
height: 100%;
}
.column {
max-width: 450px;
}
</style>
</head>
<body>
<div class="ui middle aligned center aligned grid">
<div class="column">
<h2 class="ui header">Log-in to your account</h2>
<form method="POST" th:action="@{/login.html}" class="ui large form">
<input type="hidden"
th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<div class="ui stacked segment">
<div class="field">
<div class="ui left icon input">
<i class="user icon"></i>
<input type="text" name="username" placeholder="E-mail address">
</div>
</div>
<div class="field">
<div class="ui left icon input">
<i class="lock icon"></i>
<input type="password" name="password" placeholder="Password">
</div>
</div>
<button class="ui fluid large submit green button">Login</button>
</div>
<div th:if="${param.error}">
<div class="ui error message" style="display: block;">
Authentication Failed<br/>
Reason : <span th:text="${session.SPRING_SECURITY_LAST_EXCEPTION.message}">Exception Here</span>
</div>
</div>
</form>
</div>
</div>
</body>
</html>
list.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link type="text/css" rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.2.10/semantic.min.css">
<style type="text/css">
body {
background-color: #DADADA;
}
body > .grid {
height: 100%;
}
.column {
max-width: 450px;
}
</style>
</head>
<body>
<div class="ui middle aligned center aligned grid">
<div class="column">
<h2 class="ui header">Log-out</h2>
<form method="POST" th:action="@{/logout}" class="ui large form">
<div class="ui stacked segment">
<button class="ui fluid large submit green button">Logut</button>
</div>
</form>
</div>
</div>
</body>
</html>
결과)
H2 데이터베이스 이용하기
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
H2 다운로드 주소 : http://www.h2database.com/html/download.html
resources 밑에 schema.sql 파일을 만든다. 스프링 부트가 실행되면 자동으로 읽어서 실행한다.
drop table if exists USERS;
drop table if exists AUTHORITIES;
CREATE TABLE USERS (
ID BIGINT AUTO_INCREMENT,
USERNAME VARCHAR(50) NOT NULL,
PASSWORD VARCHAR(50) NOT NULL,
ENABLED SMALLINT NOT NULL,
PRIMARY KEY (USERNAME)
);
CREATE TABLE AUTHORITIES (
ID BIGINT NOT NULL,
USERNAME VARCHAR(50) NOT NULL,
AUTHORITY VARCHAR(50) NOT NULL,
FOREIGN KEY (USERNAME) REFERENCES USERS(USERNAME)
);
INSERT INTO USERS (USERNAME, PASSWORD,ENABLED) VALUES
('admin@books.io', '{noop}secret',true),
('marten@books.io', '{noop}user',true),
('jdoe@books.net', '{noop}user',false);
-- {noop}은 저장된 암호에 암호화가 적용되지 않았음을 나타낸다.
-- 스프링 보안은 위임을 사용해 사용할 인코딩 방법을 결정한다.
-- 값은 {bcrypt}, {scrypt}, {pdkdf2}, {sha256}이 될수 있다.
-- {sha256}은 주로 호한성을 이유로 존재하며 비보안으로 간주해야 한다.
INSERT INTO AUTHORITIES (ID, USERNAME, AUTHORITY) VALUES
(1,'admin@books.io', 'ADMIN'),
(1,'admin@books.io', 'USER'),
(2,'marten@books.io', 'USER'),
(3,'jdoe@books.net', 'USER');
package com.example.demo.library.Security;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.sql.DataSource;
/**
* @Configuration
* 스프링의 @Configuration 어노테이션은 어노테이션기반 환경구성을 돕는다.
* 이 어노테이션을 구현함으로써 클래스가 하나 이상의 @Bean 메소드를 제공하고
* 스프링 컨테이가 Bean정의를 생성하고 런타임시 그 Bean들이 요청들을 처리할 것을 선언하게 된다.
*
* @EnableWebSecurity 애너테이션은 웹 보안을 활성화 한다.
* 하지만 그자체로는 유용하지 않고, 스프링 시큐리티가 WebSecurityConfigurer를 구현하거나
* 컨텍스트의 WebSebSecurityConfigurerAdapter를 확장한 빈으로 설정되어 있어야 한다.
*/
@Configuration
@EnableWebSecurity
public class LibrarySecurityConfig extends WebSecurityConfigurerAdapter implements WebMvcConfigurer {
// WebSecurityConfigurerAdapter 가 없을 때 기본 보안 설정을 한다. 하나 이상 발견되면 설정된 보안 설정을 사용한다.
@Autowired
private DataSource dataSource;
public LibrarySecurityConfig() {
super(true); // 기본 보안 구성 비활성화, 공부하기위해 일부러 생성 원래는 없어도됨
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("index");
registry.addViewController("/login.html").setViewName("login"); // 매핑
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.securityContext()
.and().exceptionHandling()
// 예외 핸들링
.and().servletApi()
// 서블릿 API 통합
.and().httpBasic()
// http 기본인증
.and().logout().logoutSuccessUrl("/")
.and().headers().frameOptions().sameOrigin()
// 로그아웃후 브라우저의 뒤로 가기 버튼을 사용하면 로그아웃이 성공하더라도 이전 페이지를 계속볼수 있다.
// 이는 브라우저 페이지를 캐시한다는 사실과 관련이 있다.
// 그러므로 header() 구성 메소드로 보안 헤더를 활성화하면 브라우저가 페이지를 캐시하지 않도록 지시한다.
// no-cache 헤더 옆에서 콘텐츠 스니핑이 비활성화되고 x-frame 보호가 활성화된다.
.and().csrf().ignoringAntMatchers("/h2-console/**")
.and().anonymous().principal("guest").authorities("ROLE_GUEST")
.and().rememberMe()
// 기본적으로 사용자 이름, 암호, remember-me 만료 시간 및 개인 키를 토큰으로 인코딩하고 쿠키로 사용자의 브라우저에 저장한다.
// 다음에 사용자가 동일한 웹 애플리케이션에 액세스하면 이 토큰이 감지돼 사용자가 잗종으로 로그인할 수 있따.
// 정적 Remeber-Me 토큰은 해커가 캡쳐할 수 있기 때문에 보안문제가 있다. 그러므로 롤링토큰을 지원하지만,
// 토큰을 유지하려면 데이터베이스가 필요하다. 자세한것은 다음게시물에 작성예정이다.
.and().formLogin().loginPage("/login.html").defaultSuccessUrl("/books.html").failureUrl("/login.html?error=true").permitAll()
// 폼기반 로그인
.and().authorizeRequests().mvcMatchers("/","/h2-console/**" ).permitAll().anyRequest().authenticated();
// "/"들어오는 요청은 모두허용 && 어떤요청이든 인증확인
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource)
.rolePrefix("ROLE_")
.usersByUsernameQuery("SELECT USERNAME, PASSWORD, 'TRUE' as enabled FROM USERS WHERE USERNAME = ?")
.authoritiesByUsernameQuery("SELECT A.USERNAME AS USERNAME, B.AUTHORITY AS AUTHORITIES " +
"FROM USERS as A INNER JOIN AUTHORITIES as B " +
"ON A.USERNAME = B.USERNAME " +
"WHERE A.USERNAME = ?");
}
}
주석처리된것은 기본값이다.
#spring.datasource.url=jdbc:h2:mem:testdb
#spring.datasource.driverClassName=org.h2.Driver
#spring.datasource.username=sa
#spring.datasource.password=
spring.h2.console.enabled=true
https://localhost:8443/h2-console 에 들어가서 아이디와 비번을 성공할시
결과)
바뀐부분)
INSERT INTO USERS (USERNAME, PASSWORD,ENABLED) VALUES
('admin@books.io', '{bcrypt}$2a$10$48zF8yixzhFXLaKQKOFwjO3l8I.4li2yF3a4GPCR2R8adOTXrDDQW',true),
('marten@books.io', '{bcrypt}$2a$10$qIoqRufr5O08a12DuroPWOoDGjVnJx5DbH9c2J8hjnehAfujYNr9y',true),
('jdoe@books.net', '{bcrypt}$2a$10$B8oFxwSvHEYGiqaaIr8AYuVPSorzaf0/cTqwOu7j0kVvqbYIXyasS',false);
-- {noop}은 저장된 암호에 암호화가 적용되지 않았음을 나타낸다.
-- 스프링 보안은 위임을 사용해 사용할 인코딩 방법을 결정한다.
-- 값은 {bcrypt}, {scrypt}, {pdkdf2}, {sha256}이 될수 있다.
-- {sha256}은 주로 호한성을 이유로 존재하며 비보안으로 간주해야 한다.
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource)
.rolePrefix("ROLE_")
.usersByUsernameQuery("SELECT USERNAME, replace(PASSWORD, '$2y', '$2a') AS PASSWORD, 'TRUE' as enabled FROM USERS WHERE USERNAME = ?")
.authoritiesByUsernameQuery("SELECT A.USERNAME AS USERNAME, B.AUTHORITY AS AUTHORITIES " +
"FROM USERS as A INNER JOIN AUTHORITIES as B " +
"ON A.USERNAME = B.USERNAME " +
"WHERE A.USERNAME = ?");
}
결과)
성공
(이미지 같으므로 생략)
'WEB > 스프링 부트 2' 카테고리의 다른 글
내장데이터베이스 Derby (0) | 2020.02.13 |
---|---|
Spring Security Method SpEL (0) | 2020.02.13 |
TestRestTemplate, WebTestClient (0) | 2020.02.11 |
Spring Security (0) | 2020.02.10 |
WebFlux + Thymeleaf (0) | 2020.02.07 |