DBILITY

spring boot security 본문

java/spring cloud

spring boot security

DBILITY 2019. 9. 18. 14:25
반응형

기본 보안 설정(default security configuration)에  대해 알아보고, 기본 설정을 비활성화(disable)하여 사용자 정의 설정으로 변경해 본다.

spring-boot-starter-parent 버전은 2.1.8.RELEASE, spring-cloud 버전은 Greenwich.SR3 이다.

maven project 기준으로 Spring Boot App에 security를 추가하기 위해서는 다음과 같이 pom.xml에 security starter dependancy를 추가한다.

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>

spring-boot-autoconfigure-2.1.8.RELEASE.jar에 포함된 SecurityAutoConfiguraton class에 의해 기본 설정 및 초기화가 이루어진다.

기본 설정에 의해 인증(Authenticatoin)은 활성화(enabled) 상태로 content negotiation에 따라 basic 또는 formLogin 방식의 인증이 이루어진다.

spring.security.user.password 속성이 설정되어 있지 않을 시 랜덤 하게 비밀번호가 생성되고, 다음과 같은 형태로 console에 출력된다.
Using generated security password: d493499c-aff8-48ae-897c-72f136116b31

 

application.properties에 spring.security.user.name 속성, spring.security.user.password 속성을 설정해 주면 된다.

spring security에서 기본적으로 제공하는 사용자는 하나뿐이며, 메모리에 저장한다.

자세한 내용은 Spring Common Application Properties 페이지를 참조한다.

spring.security.user.name=master
#spring.security.user.password=1234

웹브라우저 접속 시 다음과 같이 formLogin이 나타난다. /logout경로 요청 시 logout confirm화면이 나타난다.

 

자동 설정을 비활성화(disable)하는 방법은 다음과 같이 두 가지다.

첫 번째는 @SpringBootApplication 애노테이션에서 SecurityAutoConfiguration.class를 제외하는 방법으로 다음과 같다.

package com.dbility.spring.cloud.security;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;

@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
public class SecurityTestApplication {

	public static void main(String[] args) {
		SpringApplication.run(SecurityTestApplication.class, args);
	}
}

다른 방법은 application.properties에서 제외 설정을 하는 것으로 다음과 같다

spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration

보안을 비활성화할 거면 뭐하러 dependancy를 추가할까?

비활성화하지 않고, 사용자가 원하는 대로 처리할 수 있도록 WebSecurityConfigurerAdapter를 상속받아 configure 메서드를 오버 라이딩하면 된다.

위 두 가지 방법을 사용했던 걸 모두 제거하고, dependancy만 추가한 상태에서 다음과 같이 처리해 보자.

SecurityController.java

package com.dbility.spring.cloud.security;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@RestController
public class SecurityController {

	@RequestMapping(value = {"","/"})
	public String getDefault() throws Exception {
		log.debug("index");
		return "<a href='/logout'>logout</a><br />";
	}

	@GetMapping(path = "/nonsecurity/datetime")
	public String getDatetime() throws Exception {
		String dt = LocalDateTime.now(ZoneId.of("Asia/Seoul")).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")).toString();
		log.debug("{}", dt);
		return dt;
	}
}

ManuallySecurityConfiguration.java

package com.dbility.spring.cloud.security;

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.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
public class ManuallySecurityConfiguration extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {

		auth
		.inMemoryAuthentication()
		.withUser("clerk")
		.password(passwordEncoder().encode("1234"))
		//.password("{noop}1234") // NoOpPasswordEncoder
		.roles("USER")
		.and()
		.withUser("master")
		.password(passwordEncoder().encode("1234"))
		//.password("{noop}1234") // NoOpPasswordEncoder
		.roles("USER","ADMIN");

	}

	@Override
	protected void configure(HttpSecurity http) throws Exception {

		http
		.authorizeRequests()
		.antMatchers("/nonsecurity/datetime")
		.permitAll()
		.anyRequest()
		.authenticated()
		.and()
		.httpBasic()
		.and()
		.formLogin();
	}

	@Bean
	public PasswordEncoder passwordEncoder() {
		return PasswordEncoderFactories.createDelegatingPasswordEncoder();
	}
}

/nonsecurity/datetime 경로는 인증 없이 접근이 가능하고, 그 이외는 모두 인증 화면이 출력된다.

 

security와 h2db를 연동해 보자. 다음과 같이 depandancy를 추가한다.

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
	<groupId>com.h2database</groupId>
	<artifactId>h2</artifactId>
</dependency>

database 접속 정보를 기술하고, h2 console에 접근할 수 있도록 처리한다.

#mem base
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
#file base
#spring.datasource.url=jdbc:h2:file:/data/testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

/h2-console 웹 접속을 위해서는 /h2-console이하 경로에 대해 접근권한을 주고, csrf는 비활성화한다.

csrf (Cross site request forgery, 사이트 간 요청 위조)는 기본적으로 enable상태로 필요에 따라 적용 방식을 달리할 수 있다.

Clickjacking방지 iframe관련 X-Frame-Options의 경우 동일 도메인 내로 한정한다.

 

ManuallySecurityConfiguration.java의 configuration은 다음과 같다.

	@Override
	protected void configure(HttpSecurity http) throws Exception {

		http
		.authorizeRequests()
		.antMatchers("/nonsecurity/datetime","/h2-console/**")
		.permitAll()
		.anyRequest()
		.authenticated()
		.and()
		.csrf().ignoringAntMatchers("/h2-console/**")
		.and()
		.headers().addHeaderWriter(new XFrameOptionsHeaderWriter(XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN))
		.frameOptions().disable()
		.and()
		.httpBasic()
		.and()
		.formLogin();
	}

/h2-console 경로 접근 시 다음과 같은 인증 화면이 나타난다.

매번 시험용 테이블을 만들고, 데이터를 입력할 수 없으니, 자동으로 생성 및 입력이 가능하도록 src/main/resources아래에 schema.sql과 data.sql을 저장하고, spring app 시작 시 실행되도록 application.properties에 spring.jpa.hibernate.ddl-auto=none으로 설정한다. hibernate가 @Entity 애노테이션이 붙은 클래스를 읽어 자동으로 테이블을 만들어 줄 수 있어 이 기능을 비활성화한 것이다.

 

application.properties 추가 부분

spring.jpa.hibernate.ddl-auto=none
spring.datasource.schema=classpath*:schema.sql
spring.datasource.data=classpath*:data.sql

schema.sql

CREATE TABLE IF NOT EXISTS Member (
	username VARCHAR(20) NOT NULL,
	password VARCHAR(20) NOT NULL,
	role VARCHAR(20) NOT NULL,
	PRIMARY KEY(username)
);

data.sql

INSERT INTO Member VALUES ('master','1234','ADMIN');
INSERT INTO Member VALUES ('clerk','1234','USER');

Member.java

package com.dbility.spring.cloud.security;

import javax.persistence.Entity;
import javax.persistence.Id;

import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
@NoArgsConstructor
@ToString
public class Member {

	@Id
	private String username;
	private String password;
	private String role;
}

MemberRepository.java ( Persistent layer )

package com.dbility.spring.cloud.security;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface MemberRepository extends JpaRepository<Member, String> {

	Member findByUsername(String username);

}

인증을 위해 사용자 정보를 가져올 수 있게 UserDetailService 인터페이스를 구현한다.

package com.dbility.spring.cloud.security;

import javax.annotation.Resource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service("userDetailService")
public class UserDetailsServiceImpl implements UserDetailsService {

	@Autowired
	private MemberRepository repository;

	@Resource(name = "passwordEncoder")
	private PasswordEncoder passwordEncoder;

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

		Member member = repository.findByUsername(username);

		if(member == null) {
			throw new UsernameNotFoundException(username);
		}

		UserDetails details = User.builder().username(member.getUsername())
				.password(passwordEncoder.encode(member.getPassword()))
				.roles(member.getRole())
				.build();

		return details;
	}

}

ManuallySecurityConfiguration.java 일부

	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
	/*
		auth
		.inMemoryAuthentication()
		.withUser("clerk")
		.password(passwordEncoder().encode("1234"))
		//.password("{noop}1234") // NoOpPasswordEncoder
		.roles("USER")
		.and()
		.withUser("master")
		.password(passwordEncoder().encode("1234"))
		//.password("{noop}1234") // NoOpPasswordEncoder
		.roles("USER","ADMIN");
	*/
		auth.userDetailsService(userDetailsService);
	}
반응형
Comments