스프링시큐리티를 이용하여  index페이지를 통해 hello페이지는 누구나 볼 수 있으나

my페이지는 로그인한 사용자만 볼 수 있도록 해보자

 

1) 뷰 생성

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>Welcome</h1>
    <a href="/hello">Hello</a>
    <a href="/my">My Page</a>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>Hello</h1>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>My Page</h1>
</body>
</html>

먼저 다음과 같이  Thymleaf를 통해 index.html, hello.html, my.html 페이지를 생성하였다.

 

2) 컨트롤러 생성

@Controller
public class HomeController {

    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }


    @GetMapping("/my")
    public String my() {
        return "my";
    }
}

hello와 my요청에 대한 컨트롤러도 만들어주었다.

 

3) SecurityConfig 생성

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override //재정의
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/", "/hello").permitAll() // "/", "/hello" 는 모두 볼수있음
                .anyRequest().authenticated() // 그 외에는 인증필요함 
                .and()
                .formLogin()
                .and()
                .httpBasic();
    }

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

WebSecurityConfigurerAdapter를 상속받은 SecurityConfig 클래스를 생성하여

configure메소드를 재정의 해 준다. 루트와 hello에 대한 요청은 인증이 필요없도록 설정하였다.

재정의 하였기에 기존에 스프링시큐리티에서 제공한 기본설정은 사용되지 않는다.

PasswordEncoder를 이용해 비밀번호는 암호화하여 저장되도록 하였다.

 

4) Entity 생성

@Entity
public class Account {

    @Id @GeneratedValue
    private Long id;
    private String username;
    private String password;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

사용자정보를 스프링시큐리티에서 제공하는 기본정보가 아닌 직접 DB에서 관리할 수 있도록

Account객체를 생성한다.

public interface AccountRepository extends JpaRepository<Account, Long> {

    Optional<Account> findByUsername(String s);
}
<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

JPA를 사용하기 위해 Repository와 의존성도 추가해주었다.

 

5) 서비스 구현

@Service
public class AccountService implements UserDetailsService {
    @Autowired
    private AccountRepository accountRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;
    public  Account createAccount(String username, String password){
        Account account = new Account();
        account.setUsername(username);
        account.setPassword(passwordEncoder.encode(password));
        return accountRepository.save(account);
    }


    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        Optional<Account> byUsername =  accountRepository.findByUsername(s);
        Account account = byUsername.orElseThrow(() -> new UsernameNotFoundException(s));
        return new User(account.getUsername(), account.getPassword(), authorities());
    }

    private Collection<? extends GrantedAuthority> authorities() {
        return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
    }

}

DB 사용자 인증 처리을 위해 UserDetailsService 클래스를 커스트마이징 해야 한다.

UserDetailsService를 implement하여 loadUserByUsername를 구현해준다.

loadUserByUsername이 핵심이다. 이 부분에서 사용자계정과 DB가 연동된다.

 

6) 테스트 및 실행

@Component
public class AccountRunner implements ApplicationRunner {

    @Autowired
    AccountService accountService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        Account ny = accountService.createAccount("ny", "1234");
        System.out.println(ny.getUsername() + "password :" +ny.getPassword());
    }
}

ApplicationRunner클래스를 생성하여 테스트를 진행해보자

 

 

실행해보면 인덱스페이지와 hello페이지는 접속되지만 my페이지를 클릭하면 로그인페이지가 뜬다.

 

인코딩된 사용자 정보가 찍히는 것도 확인할 수 있다.

스프링 시큐리티를 이용한 로그인을 알아보자!

 

 

1) 스프링 시큐티리 적용

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

먼저 의존성을 추가시켜준다.

 

@Controller
public class HomeController {

    @GetMapping("/my")
    public String hello() {
        return "my";
    }
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>My</h1>
</body>
</html>

컨트롤러와 Thymleaf를 이용해 뷰를 만들어준다.

 

http://localhost:8080/my 로 접속하면 사용자 인증이 필요하기 때문에 로그인페이지로 이동되는 것을 볼 수 있다.

 

@RunWith(SpringRunner.class)
@WebMvcTest(HomeController.class)
public class HomeControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void my() throws Exception {
        mockMvc.perform(get("/my"))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(view().name("my"));
    }
}

스프링시큐티리 의존성을 추가하게 되면 테스트코드에도 적용되기 때문에

위와 같이 테스트코드를 실행했을 때 에러가 간다.

 

 

2) 테스트 코드 실행

<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-test</artifactId>
  <version>${spring-security.version}</version>
  <scope>test</scope>
</dependency>

인증된 사용자정보를 제공하여 테스트하기 위해 의존성을 추가해준다.

 

@RunWith(SpringRunner.class)
@WebMvcTest(HomeController.class)
public class HomeControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    @WithMockUser
    public void my() throws Exception {
        mockMvc.perform(get("/my"))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(view().name("my"));
    }
}

@WithMockUser를 주입하여 테스트코드를 실행하면 통과된다.

 

 

 

JPA를 사용한 데이터베이스 초기화

스프링 데이터 JPA를 구현하여 테스트 코드를 돌리면 H2데이터베이스에 실제로 디비가 생성되었다.

실제 Application에서도 디비가 생성되도록 하려면 기본설정을 바꿔줘야 한다.

 

spring.jpa.hibernate.ddl-auto=create
spring.jpa.generate-ddl=true

spring.jpa.generate-ddl 의 기본값은 false이므로 Applicaiton을 돌렸을때 postgresql에 테이블이 생성되지 않았다.

true로 설정한 후 spring.jpa.hibernate.ddl-auto 값도 지정해주었다.

spring.jpa.hibernate.ddl-auto 값으로는 create, create-drop, update, validate가 있다.

 

  • create : 기존에 있던 테이블 삭제 후 다시 생성

  • create-drtop : create와 같으나 종료할 때 테이블을 삭제

  • update : 변경된 부분만 반영

  • validate : 엔티티와 테이블이 정상적으로 매핑되었는지만 확인

spring.jpa.show-sql=true

spring.jpa.show-sql 을 true로 설정하면 실행되는 sql 쿼리를 볼 수 있다.

 

실행해보면 콘솔에 sql을 확인할 수 있다. postgresql에서도 테이블이 생성된 것을 볼 수 있다.

 

 

SQL 스크립트를 사용한 데이터베이스 초기화

jpa를 사용하지 않는다면 schema.sql를 이용하여 데이터베이스를 초기화 할 수 있다.

 

drop table account if exists
drop sequence if exists hibernate_sequence
create sequence hibernate_sequence start with 1 increment by 1
create table account (id bigint not null, password varchar(255), username varchar(255), primary key (id))

다음과 같은 초기화 쿼리문을 resource/schema.sql 이름으로 생성해 준다.

 

spring.jpa.hibernate.ddl-auto=validate
spring.jpa.generate-ddl=false

그럼 위와 같이 spring.jpa.generate-ddl이 false더라도 초기화가 이뤄진다.

 

 

스프링 데이터 JPA

스프링에서 제공하는 스프링 데이터 JPA를 통해 더욱 간편하게 JPA를 이용할 수 있다.

스프링 데이터 JPA를 사용하는 방법을 알아보자!

 

1. 의존성 추가

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

먼저 의존성을 추가해준다.

 

 

2. 엔티티 클래스 생성

@Entity
public class Account {


    @Id
    @GeneratedValue
    private Long id;

    private String username;

    private String password;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Account account = (Account) o;
        return Objects.equals(id, account.id) &&
                Objects.equals(username, account.username) &&
                Objects.equals(password, account.password);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, username, password);
    }

}

Entity 클래스를 만들어준다. SQL처리없이 클래스 생성만으로 테이블이 자동으로 생성된다.

 

 

3. Repository 인터페이스 생성

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

public interface AccountRepository extends JpaRepository<Account, Long> {
    Account findByUsername(String username);
}

JPA처리를 담당하는 Repository 인터페이스를 생성한다.

 

4. DB설정

<dependency>
	<groupId>com.h2database</groupId>
	<artifactId>h2</artifactId>
	<scope>test</scope>
</dependency>
 <dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <scope>runtime</scope>
</dependency>

테스트할떄는 h2인메모리 데이터베이스를 사용하고, 실제로는 postgresql을 사용하기 위해 의존성을 추가해주었다.

 

spring.datasource.url=jdbc:postgresql://192.168.***.***:5432/springboot
spring.datasource.username=ny
spring.datasource.password=pass

postgresql은 docker를 통해 구동해주었고 위와같이 접속정보를 설정해주었다.

처음에는 jdbc:postgresql://localhost:5432/springboot 와 같이 아이피대신 localhost로 입력하였는데

접속이 안되서 docker에 설정된 아이피를 직접 입력해주었다. window 버전의 문제인가?..

 

 

5. 테스트코드 작성

@RunWith(SpringRunner.class)
@DataJpaTest
public class AccountRepositoryTest {

    @Autowired
    AccountRepository accountRepository;

    @Test
    public void di() throws SQLException {
        Account account = new Account();
        account.setUsername("ny");
        account.setPassword("pass");

        Account newAcoount = accountRepository.save(account);
        assertThat(newAcoount).isNotNull();

        Account existingAccount = accountRepository.findByUsername(newAcoount.getUsername());
        assertThat(existingAccount).isNotNull();
    }
}

@SpringBootTest 대신 @DataJpaTest를 사용하여 모든 빈을 테스트하는것이 아닌

JPA에 필요한 부분만 슬라이싱테스트를 할 수 있다.

 

6. 실행

실행해보았다..

그런데 계속 java.lang.NoClassDefFoundError:  javax/persistence/EntityManager 이런 에러가 났다..

뭔가 의존성 문제같기는한테 처음부터 다시 만들어봐도 계속 나타났다..

<dependency>
      <groupId>org.hibernate.javax.persistence</groupId>
      <artifactId>hibernate-jpa-2.1-api</artifactId>
      <version>1.0.0.Final</version>
</dependency>
<dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-entitymanager</artifactId>
      <version>5.3.2.Final</version>
</dependency>

열심히 구글링을 해서 이것저것 해보다가 위의 내용을 추가해주었더니 잘된다..아직 정확한 이유는 모르겟다..

 

테스트를 돌려보니 잘 실행되고 쿼리들도 확인할 수 있다..!

 

 

 

 

 

 

 

 

스프링부트에서 MySQL과 PostgreSQL 를 사용하기 위해 다음과 같은 설정들이 필요하다.

MySQL

1) 의존성추가

<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
</dependency>

 

2) Datasource 설정

spring.datasource.url=jdbc:mysql://localhost:3306/springboot
spring.datasource.username=username
spring.datasource.password=password

 

PostgreSQL

1) 의존성 추가

<dependency>
  <groupId>org.postgresql</groupId>
  <artifactId>postgresql</artifactId>
</dependency>

 

2)  Datasource 설정

spring.datasource.url=jdbc:postgresql://localhost:5432/springboot
spring.datasource.username=username
spring.datasource.password=password

 

H2 인메모리 데이터베이스

스프링에서는 H2 인메모리 데이터베이스를 제공한다.

 

 

DataSource 를 이용한 연동

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

h2와 jdbc 의존성을 추가해준다.

 

@Component
public class H2Runner implements ApplicationRunner {

    @Autowired
    DataSource dataSource;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        try(Connection connection = dataSource.getConnection()) {
            Statement statement = connection.createStatement();
            String sql = "CREATE TABLE USER(ID INTEGER NOT NULL, name VARCHAR(255), PRIMARY KEY (id))";
            statement.executeUpdate(sql);
        }
    }
}

ApplicationRunner를 구현하여 DataSource를 주입받아 디비에 접속할 수 있다.

 

h2-console 확인

spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.datasource.url=jdbc:h2:mem:testdb

h2-console로 확인해 보기 위해서 application.properties에 다음과 같은 값을 입력해주었다.

인메모리데이터 기본연결정보는 URL: “testdb”, username: “sa” 이다.

하지만 h2 1.4.198 버전 부터는 보안문제로 testdb를 자동으로 생성하지 않는다고 한다.

testdb를 생성해주기 위해서 spring.datasource.url 값도 설정해주었다. 

 

 

 

어플리케이션을 구동하여 h2 콘솔로 접속해보면 USER테이블이 생성된것을 볼 수 있다.

 

 

JdbcTemplate 을 이용한 연동

@Component
public class H2Runner implements ApplicationRunner {

  @Autowired
  JdbcTemplate jdbcTemplate;

  @Override
  public void run(ApplicationArguments args) throws Exception {
    String sql = "INSERT INTO USER VALUES (1, 'ny')";
    jdbcTemplate.execute(sql);
  }
}

JdbcTemplate를 사용하면 더 간편하게 처리할 수 있으며,

try, catch, finally와 리소스 반납처리가 잘되어 있어 더욱 안전하다.

 

SOP

Single-Origin Policy 라는 뜻으로 같은 Origin에서만 호출할 수 있는 정책이다. 스프링을 기본적으로 SOP이다. 

 

CORS

Cross Origin Policy 라는 뜻으로 다른 Origin에서 요청이 가능하도록 CORS 기능을 사용할 수 있다.

스프링부트에서는 아무런 설정없이 바로 사용할 수 있다.

 

하나의 Origin 조건

URI 스키마 (http, https)

hostname (localhost)

포트 (8080, 18080)

 

 

CORS 사용해보기

@RestController
public class SampleController {

  @CrossOrigin(origins = "http://localhost:18080")
  @GetMapping("/hello")
  public String hello() {
    return "Hello";
  }
}

서버쪽에서 @CrossOrigin으로 클라이언트 Orgin을 설정해준다.

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>CORS Client</h1>
<script src="webjars/jquery/3.4.1/dist/jquery.min.js"></script>
<script>
    $(function() {
        $.ajax("http://localhost:8080/hello")
        .done(function (msg){
            alert(msg);
        })
        .fail(function (){
            alert("fail");
        })
    });
</script>
</body>
</html>

클라이언트 포트는 18080 설정한 후, 다음과 같이 서버쪽으로 요청을 보낼 수 있다.

 

@Configuration
public class WebConfig implements WebMvcConfigurer {

  @Override
  public void addCorsMappings(CorsRegistry registry) {
    registry.addMapping("/hello").allowedOrigins("http://localhost:18080");

    // 18080 포트에 대한 모든 요청 허용
    registry.addMapping("/**").allowedOrigins("http://localhost:18080");
  }
}

만약 하나의 요청이 아닌  여러 컨트롤러에 걸쳐 허용하고 싶다면

WebMvcConfigurer 인터페이스를 확장해준다.

스프링MVC 기능은 그대로 사용하면서 추가로 확장하는 작업이다.

 

 

 

 

HATEOAS

링크 URL정보를 리소스로 클라이언트에게 전달하여

클라이언트에서는 링크정보로  URL에 접근할 수 있도록 하는 기능이다.

{
  "content":"Hello, World!",
  "_links":{
    "self":{
      "href":"http://localhost:8080/greeting?name=World"
    }
  }
}

이런식으로 링크정보를 리소스와 함께 전달해준다고 한다.

 

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

먼저 pom.xml에 다음과 같은 의존성을 추가해준다.

 

@RunWith(SpringRunner.class)
@WebMvcTest(SampleController.class)
public class SampleControllerTest {
    @Autowired
    MockMvc mockMvc;

    @Test
    public void hello() throws Exception {
       mockMvc.perform(get("/hello"))
               .andDo(print())
               .andExpect(status().isOk())
               .andExpect(jsonPath("$._links.self").exists());
    }
}

다음과 같은 테스트코드를 작성한다.

 

@RestController
public class SampleController {

    @GetMapping("/hello")
    public EntityModel<Hello> hello() {
        Hello hello = new Hello();
        hello.setPrefix("Hey, ");
        hello.setName("ny");

        EntityModel<Hello> helloResource = new EntityModel<>(hello);
        helloResource.add(linkTo(methodOn(SampleController.class).hello()).withSelfRel());
        return helloResource;
    }

}

컨트롤러에서는 리소스에 링크정보를 담아 돌려주는 작업을 한다.

SampleController의 hello메서드를 Self 릴레이션으로 만든다.

 

public class User {

    private Long id;
    private String username;
    private String password;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

User객체도 만들어 준다.

 

테스트코드를 실행해보면 다음과 같이 리소스정보가 넘어오는것을 확인할 수 있다.

 

+ Recent posts