해당 내용은 책 ‘코드로 배우는 스프링 부트 웹 프로젝트’에 나오는 내용이며 이는 개인적으로 공부하기 위해 기록함을 알려드립니다
SpringBoot와 Spring Security 연동
이번 챕터에서는 아래와 같은 내용을 학습한다.
- 스프링 시큐리티에서 제공하는 로그인 처리 방식의 이해
- JPA와 연동하는 커스텀 로그인 처리
- Thymeleaf에서 로그인 정보 활용하기
프로젝트 생성
의존성 추가
build.gradle 설정
plugins {
id 'org.springframework.boot' version '2.5.2'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
id 'war'
}
group = 'org.young'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
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'
//추가한 부분
//mysql 드라이버
implementation group: 'mysql', name: 'mysql-connector-java', version: '8.0.25'
//Thymeleaf 확장 플러그인은 화면을 제작할 때 스프링 시큐리티 객체들을 처리하는 용도이다.
implementation group: 'org.thymeleaf.extras', name: 'thymeleaf-extras-springsecurity5', version: '3.0.4.RELEASE'
implementation group: 'org.thymeleaf.extras', name: 'thymeleaf-extras-java8time', version: '3.0.4.RELEASE'
//
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok'
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
test {
useJUnitPlatform()
}
application.properties
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/springsecurity?serverTimezone=UTC&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=root
#서버 시작 시점에 DDL 문을 생성하여 DB에 적용한다
spring.jpa.hibernate.ddl-auto=update
#True로 하면 sql문을 보기 좋게 설정한다
spring.jpa.properties.hibernate.format_sql=true
#적용된 sql문을 보여준다.
spring.jpa.show-sql=true
#Thymeleaf 템플릿 캐싱 비활성화
#thymeleaf를 사용하다 수정 사항이 생길 대 수정을 하면
#재시작을 해줘야 한다. cache가 계속 쌓이기 때문이다.
#이를 방지하여 브라우저 새로고침만으로도 수정 사항을 확인하기 위해서 이것을 추가한다.
spring.thymeleaf.cache=false
spring.servlet.multipart.enabled=true
spring.servlet.multipart.location=C:\\Users\\nick1\\kpu\\spring_security\\upload
spring.servlet.multipart.max-request-size=30MB
spring.servlet.multipart.max-file-size=10MB
#업데이트 실시간 반영
spring.devtools.livereload.enabled=true
#시큐리티와 관련된 부분은 좀 더 로그 레벨을 낮게 설정해서 자세한 로그를 확인할 수 있도록 한다.
logging.level.org.springframework.security.web=debug
logging.level.org.young.security=debug
이 상태에서 어플리케이션이 제대로 작동하나 실행해 본다. 실행하면 아래 사진과 같이 password가 나온다.
프로젝트 초기에 아무 계정도 없을 때 사용할 수 있는 임시 패스워드 역할을 한다. 프로젝트가 정상적으로 실행된다면 ‘http://localhost:8080/login’의 경로로 접근해서 화면에서 ‘user’라는 계정으로 설정하고 위의 패스워드를 입력해서 로그인을 테스트 한다.
컨트롤러가 작성되지 않았기 때문에 아래와 같이 나온다.
시큐리티 설정 클래스를 작성한다.
SecurityConfig 클래스
package org.young.club.config;
//시큐리티 관련 기능을 쉽게 설정하기 위해서 WebSecurity ConfigurerAdapter라는 클래스를 상속으로 처리한다.
//WebSecurityConfigurer Adapter 클래스는 주로 override를 통해서 여러 설정을 조정하게 된다.
import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@Log4j2
//모든 시큐리티 관련 설정이 추가되는 부분이므로 앞으로 작성하는 예제에서 핵심적인 역할을 한다.
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
SampleController 클래스
package org.young.club.controller;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@Log4j2
@RequestMapping("/sample/")
//시큐리티와 관련된 설정이 정상적으로 돌아가는지 확인하기 위한 간단한 컨트롤러 구성
public class SampleController {
//로그인을 하지 않은 사용자도 접근할 수 있는 /sample/all
@GetMapping("/all")
public void exAll(){
log.info("exAll.........");
}
//로그인한 사용자만이 접근할 수 있는 '/sample/member'
@GetMapping("/member")
public void exMember(){
log.info("exMember.........");
}
//관리자(admin) 권한이 있는 사용자만이 접근할 수 있는 '/sample/admin'
@GetMapping("/admin")
public void exAdmin(){
log.info("exAdmin.........");
}
}
컨트롤러로 인한 페이지가 표시될 수 있도록 대응되는 html 파일을 구성한다.
all.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Title</title>
</head>
<body>
<h1>For All</h1>
</body>
</html>
admin.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Title</title>
</head>
<body>
<h1>For Admin</h1>
</body>
</html>
member.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Title</title>
</head>
<body>
<h1>For Member</h1>
</body>
</html>
PasswordEncoding를 사용하기 위해서 SecurityConfig 클래스에 추가한다.
SecurityConfig.java
package org.young.club.config;
//시큐리티 관련 기능을 쉽게 설정하기 위해서 WebSecurity ConfigurerAdapter라는 클래스를 상속으로 처리한다.
//WebSecurityConfigurer Adapter 클래스는 주로 override를 통해서 여러 설정을 조정하게 된다.
import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@Log4j2
//모든 시큐리티 관련 설정이 추가되는 부분이므로 앞으로 작성하는 예제에서 핵심적인 역할을 한다.
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//PasswordEncoder는 패스워드를 인코딩하는 것인데 주목적은 역시 패스워드를 암호화하는 것이다
//PasswordEncoder는 인터페이스로 설계되어 있으므로 실제 설정에서는 이를 구현하거나 구현된 클래스를 이용해야만 한다.
//스프링 시큐리티에는 여러 종류의 PasswordEncoder를 제공하고 있는데 그중에서도 가장 많이 사용하는 것은
//BCryptPasswordEncoder라는 클래스이다. 이는 'bcrypt'라는 해시 함수를 이용해서 패스워드를 암호화하는 목적으로 설계됐다.
//SecurityConfig에는 @Bean을 이용해서 BCryptPasswordEncoder를 지정한다.
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
PasswordTests라는 테스트 클래스를 작성해 본다.
PasswordTests.java
package org.young.club.security;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.password.PasswordEncoder;
@SpringBootTest
public class PasswordTests {
@Autowired
private PasswordEncoder passwordEncoder;
//BCryptPasswordEncoder로 암호화된 패스워드는 다시 원래대로 복호화가 불가능하다
//매번 암호화된 값도 다르게 된다
//대신에 특정한 문자열이 암호화된 결과인지만을 확인할 수 있다.(.matches()함수를 통해서)
@Test
public void testEncoder(){
String password="1111";
String enPw=passwordEncoder.encode(password);
System.out.println("enPw:"+enPw);
boolean matchResult=passwordEncoder.matches(password, enPw);
System.out.println("matchResult: "+matchResult);
}
}
위 테스트를 통해 인코딩된 password가 나타난다.
enPw에 나오는 내용을 복사해두고 다시 SecurityConfig.java로 돌아와 코드를 추가한다.
SecurityConfig.java
package org.young.club.config;
//시큐리티 관련 기능을 쉽게 설정하기 위해서 WebSecurity ConfigurerAdapter라는 클래스를 상속으로 처리한다.
//WebSecurityConfigurer Adapter 클래스는 주로 override를 통해서 여러 설정을 조정하게 된다.
import lombok.extern.log4j.Log4j2;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@Log4j2
//모든 시큐리티 관련 설정이 추가되는 부분이므로 앞으로 작성하는 예제에서 핵심적인 역할을 한다.
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//PasswordEncoder는 패스워드를 인코딩하는 것인데 주목적은 역시 패스워드를 암호화하는 것이다
//PasswordEncoder는 인터페이스로 설계되어 있으므로 실제 설정에서는 이를 구현하거나 구현된 클래스를 이용해야만 한다.
//스프링 시큐리티에는 여러 종류의 PasswordEncoder를 제공하고 있는데 그중에서도 가장 많이 사용하는 것은
//BCryptPasswordEncoder라는 클래스이다. 이는 'bcrypt'라는 해시 함수를 이용해서 패스워드를 암호화하는 목적으로 설계됐다.
//SecurityConfig에는 @Bean을 이용해서 BCryptPasswordEncoder를 지정한다.
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//SecurityConfig에는 AuthenticationManager의 설정을 쉽게 처리할 수 있도록 도와주는
//configure() 메서드를 override해서 처리한다. 파라미터의 타입인 AuthenticationManagerBuilder는
//말 그대로 코드를 통해서 직접 인증 매니저를 설정할 때 사용한다.
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//사용자 계정은 user1, 일단 인메모리 스피링 시큐리티를 실험한다.
//inMemoryAuthentication()는 인 메모리 authentication를 AuthenticationManagerBuilder에 추가하고
//원하는데로 인 메모리 authentication를 구성하는 것이 가능한 InMemoryUserDetailsManagerConfigurer 타입을 반환한다.
//withUser()는 생성되는 UserDetailsManager에 user를 추가하는 것을 허용한다
//이 함수는 다수의 users를 등록하기 위해서 여러 번 호출이 가능하다
auth.inMemoryAuthentication().withUser("user1")
//1111 패스워드 인코딩 결과
.password("$2a$10$3mm3ssAaaIYCfmJQu2w5KedCy.1yO7O3J9/Me72i5drQkHm54F2CW")
//사용자가 가지는 권한은 USER라는 권한으로 지정한다.
.roles("USER");
}
}
애플리케이션을 실행하고 위에서 설정한 아이디와 비밀번호를 통해서 localhost:8080/sample/all에 접속해 본다.
그리고 인가(Authorization)가 필요한 리소스 설정을 한다
SecurityConfig.java
package org.young.club.config;
(...)
//모든 시큐리티 관련 설정이 추가되는 부분이므로 앞으로 작성하는 예제에서 핵심적인 역할을 한다.
public class SecurityConfig extends WebSecurityConfigurerAdapter {
(...)
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
(...)
}
//스프링 시큐리티를 이용해서 특정한 리소스에 접근 제한을 하는 방식은 크게 두 가지이다.
//1) 설정을 통해서 패턴을 지정하거나
//2) 어노테이션을 이용해서 적용하는 방법이 있다.
//어노테이션을 이용하는 방식이 더 간단하긴 하지만 우선은 SecurityConfig 클래스로 설정해 본다.
@Override
protected void configure(HttpSecurity http) throws Exception {
//http.authorizeRequests()로 인증이 필요한 자원들을 설정할 수 있고 antMatchers()는
// '**/*'와 같은 앤트 스타일의 패턴으로 원하는 자원을 선택할 수 있다.
//마지막으로 permitAll()의 경우는 말 그대로 '모든 사용자에게 허락'한다는 의미이므로
//로그인하지 않은 사용자도 익명의 사용자로 간주되어서 접근이 가능하게 된다.
//프로젝트를 재시작해서 /sample/all에 접속하면 별도의 로그인 없이도 접근이 가능해 진다.
http.authorizeRequests().antMatchers("/sample/all").permitAll();
}
}
애플리케이션을 다시 시작하고 ‘/sample/all’에 접속하면 별다른 로그인 없이도 페이지에 접근한다.
반면 다시 아래와 같이 설정을 변경하고 ‘/sample/member’를 호출하면 Access Denied된다.
SecurityConfig.java
package org.young.club.config;
(...)
//모든 시큐리티 관련 설정이 추가되는 부분이므로 앞으로 작성하는 예제에서 핵심적인 역할을 한다.
public class SecurityConfig extends WebSecurityConfigurerAdapter {
(...)
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
(...)
}
//스프링 시큐리티를 이용해서 특정한 리소스에 접근 제한을 하는 방식은 크게 두 가지이다.
//1) 설정을 통해서 패턴을 지정하거나
//2) 어노테이션을 이용해서 적용하는 방법이 있다.
//어노테이션을 이용하는 방식이 더 간단하긴 하지만 우선은 SecurityConfig 클래스로 설정해 본다.
@Override
protected void configure(HttpSecurity http) throws Exception {
//http.authorizeRequests()로 인증이 필요한 자원들을 설정할 수 있고 antMatchers()는
// '**/*'와 같은 앤트 스타일의 패턴으로 원하는 자원을 선택할 수 있다.
//마지막으로 permitAll()의 경우는 말 그대로 '모든 사용자에게 허락'한다는 의미이므로
//로그인하지 않은 사용자도 익명의 사용자로 간주되어서 접근이 가능하게 된다.
//프로젝트를 재시작해서 /sample/all에 접속하면 별도의 로그인 없이도 접근이 가능해 진다.
http.authorizeRequests().antMatchers("/sample/all").permitAll()
//아래와 같이 설정하고 /sample/member'를 호출하면 Access Denied 된다.
.antMatchers("/sample/member").hasRole("USER");
}
}
HttpSecurity의 formLogin()이라는 기능은 이와 같이 인가/인증 절차에서 문제가 발생했을 때 로그인 페이지를 보여주도록 지정할 수 있고 화면으로 로그인 방식을 지원한다는 의미로 사용된다.
SecurityConfig.java
package org.young.club.config;
(...)
//모든 시큐리티 관련 설정이 추가되는 부분이므로 앞으로 작성하는 예제에서 핵심적인 역할을 한다.
public class SecurityConfig extends WebSecurityConfigurerAdapter {
(...)
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
(...)
}
//스프링 시큐리티를 이용해서 특정한 리소스에 접근 제한을 하는 방식은 크게 두 가지이다.
//1) 설정을 통해서 패턴을 지정하거나
//2) 어노테이션을 이용해서 적용하는 방법이 있다.
//어노테이션을 이용하는 방식이 더 간단하긴 하지만 우선은 SecurityConfig 클래스로 설정해 본다.
@Override
protected void configure(HttpSecurity http) throws Exception {
//http.authorizeRequests()로 인증이 필요한 자원들을 설정할 수 있고 antMatchers()는
// '**/*'와 같은 앤트 스타일의 패턴으로 원하는 자원을 선택할 수 있다.
//마지막으로 permitAll()의 경우는 말 그대로 '모든 사용자에게 허락'한다는 의미이므로
//로그인하지 않은 사용자도 익명의 사용자로 간주되어서 접근이 가능하게 된다.
//프로젝트를 재시작해서 /sample/all에 접속하면 별도의 로그인 없이도 접근이 가능해 진다.
http.authorizeRequests().antMatchers("/sample/all").permitAll()
//아래와 같이 설정하고 /sample/member'를 호출하면 Access Denied 된다.
.antMatchers("/sample/member").hasRole("USER");
//인가/인증에 문제시 로그인 화면면
http.formLogin();
}
}
위 사진과 같이 formLogin()이 적용되면 인가/인증에 실패하는 경우에 로그인 페이지를 볼 수 있게 된다.
formLogin()과 마찬가지로 logout() 메서드를 이용하면 로그아웃 처리가 가능하다. formLogout() 역시 로그인과 마찬가지로 별도의 설정이 없는 경우에는 스프링 시큐리티가 제공하는 웹 페이지를 보게 된다. SecurityConfig에 logout()을 적용해주기만 하면 된다.
SecurityConfig.java
package org.young.club.config;
(...)
//모든 시큐리티 관련 설정이 추가되는 부분이므로 앞으로 작성하는 예제에서 핵심적인 역할을 한다.
public class SecurityConfig extends WebSecurityConfigurerAdapter {
(...)
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
(...)
}
//스프링 시큐리티를 이용해서 특정한 리소스에 접근 제한을 하는 방식은 크게 두 가지이다.
//1) 설정을 통해서 패턴을 지정하거나
//2) 어노테이션을 이용해서 적용하는 방법이 있다.
//어노테이션을 이용하는 방식이 더 간단하긴 하지만 우선은 SecurityConfig 클래스로 설정해 본다.
@Override
protected void configure(HttpSecurity http) throws Exception {
//http.authorizeRequests()로 인증이 필요한 자원들을 설정할 수 있고 antMatchers()는
// '**/*'와 같은 앤트 스타일의 패턴으로 원하는 자원을 선택할 수 있다.
//마지막으로 permitAll()의 경우는 말 그대로 '모든 사용자에게 허락'한다는 의미이므로
//로그인하지 않은 사용자도 익명의 사용자로 간주되어서 접근이 가능하게 된다.
//프로젝트를 재시작해서 /sample/all에 접속하면 별도의 로그인 없이도 접근이 가능해 진다.
http.authorizeRequests().antMatchers("/sample/all").permitAll()
//아래와 같이 설정하고 /sample/member'를 호출하면 Access Denied 된다.
.antMatchers("/sample/member").hasRole("USER");
//인가/인증에 문제시 로그인 화면면
http.formLogin();
//csrf 토큰을 발행하지 않는다.
http.csrf().disable();
//로그아웃 처리
http.logout();
}
}
프로젝트를 다시 실행하고 /sample/member로 접근하면 아래와 같이 로그아웃 되고 다시 로그인을 하라는 상태가 된다.
logout()에서 주의해야 할 점은 CSRF 토큰을 사용할 때는 반드시 POST 방식으로만 로그아웃을 처리한다는 점이다. CSRF 토큰을 이용하는 경우에는 /logout 이라는 URL을 호출했을 때는 <form> 태그와 버튼으로 구성된 화면을 보게 되지만 CSRF 토큰을 disable()로 비활성화 시키면 GET 방식(‘/logout’)으로도 로그아웃 처리된다.(위 그림과 같이)