Spring

spring security DB 계정을 활용한 로그인 (spring security 6, spring boot 3.1)

slow333 2023. 6. 3. 04:29

어렵게 성공함

db는 postgre로 적용

build.gradle, application.properties (특이한 부분은 없음...)

plugins {
id 'java'
id 'org.springframework.boot' version '3.1.0'
id 'io.spring.dependency-management' version '1.1.0'
}

group = 'spring.aop'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
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'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'org.postgresql:postgresql'
runtimeOnly 'com.mysql:mysql-connector-j'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}

tasks.named('test') {
useJUnitPlatform()
}
bootJar{
archivesBaseName = 'aop-test-sec'
archiveFileName = 'app-sec-aop-test.jar'
archiveVersion = "0.6.2"
}
spring.datasource.url=jdbc:postgresql://localhost:5432/project-and-employee
# default super user
spring.datasource.username=postgres
spring.datasource.password=xxxxxxx
spring.datasource.driver-class-name=org.postgresql.Driver
# initial sql file
spring.sql.init.mode=never
# create, create-drop, validate, update, none
spring.jpa.hibernate.ddl-auto=none

#JPA config 출시 시에는 false
spring.jpa.show-sql=true
#spring.jpa.defer-datasource-initialization=true
# 출시 시에는 true
spring.thymeleaf.cache=false

# this set by windows system > env variable (시스템>시스템변수>환경변수> 새값:
# key(원하는 변수명) > envVersionNum , 원하는 값 > 컴파일하면 윈도우 값을 기본으로 가저옮
version=local_postgre_Db_0.2.3
#version=${envVersionNum}


# log setting
logging.level.root = WARN
logging.level.com.example.aop_after_aws = DEBUG
#logging.level.org.springframework = ERROR
logging.level.org.springframework = INFO
logging.file.name=app.log
logging.pattern.dateformat=yy-MM-dd H:m

데이터베이스에 table 생성(데이터 없음)

============================

CREATE SEQUENCE IF NOT EXISTS users_seq;

CREATE TABLE IF NOT EXISTS users (
user_id BIGINT NOT NULL DEFAULT nextval('users_seq') PRIMARY KEY,
username varchar(255) NOT NULL,
email varchar(255) NOT NULL,
password varchar(255) NOT NULL,
role varchar(255),
enabled boolean NOT NULL
);

=========================================

UserAccounts entities

package spring.aop.sec.entities;

import jakarta.persistence.*;

@Entity
@Table(name = "user_accounts")
public class UserAccounts {
@Id
@SequenceGenerator(name = "user_accounts_seq", sequenceName = "user_accounts_seq", allocationSize = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_accounts_seq")
private long userId;

@Column(name = "username")
private String userName;

private String password;
private String email;
private boolean enabled = true;

public UserAccounts() {
}

public String getPassword() {
return password;
}
.....

public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}

주의 사항 id 생성시

@SequenceGenerator(name = "user_accounts_seq", sequenceName = "user_accounts_seq", allocationSize = 1)

이 거 안해주면 애러남... 이유는 모름 아마도 postgre 특성인 듯...

=======================================

SecurityConfiguration => 이전 spring security는 WebSecurityConfigurerAdapter 를 상속 받아서 override해서 사용하는 방식이나 여기서 많이 바뀜.... 고생, 고생

 

@EnableWebSecurity
@Configuration
public class SecurityConfiguration {

@Autowired
BCryptPasswordEncoder passwordEncoder;
@Autowired
private ProjectRepository projectRepository;

@Autowired
DataSource dataSource;
// authentication(login 사용자 인증)
@Bean
public UserDetailsManager userDetailsService() {
JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
users.setUsersByUsernameQuery(
"select username, password, enabled from user_accounts where username= ?"
);
users.setAuthoritiesByUsernameQuery(
"select username, role from user_accounts where username = ?"
);
return users;
}

//authorization(사용자별 권한 설정)
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/projects/new").hasRole("ADMIN")
.requestMatchers("/projects/save").hasRole("ADMIN")
.requestMatchers("/employees/new").hasRole("ADMIN")
.requestMatchers("/employees/save").hasRole("ADMIN")
.requestMatchers("/", "/**").permitAll()
)
.formLogin(in -> in
.loginProcessingUrl("/").permitAll())
.logout(logout -> logout
.deleteCookies("remove")
.invalidateHttpSession(false)
.permitAll()
)
.httpBasic(withDefaults())
.headers(headers -> headers
.frameOptions(frameOptions -> frameOptions
.disable()
// .sameOrigin()
)
);
return http.build();
}
}

기본적으로 scrf가 적용되어 있음...

 

DB 접속해서 사용자 인증, 권한을 갖고 오기 위해서는 sql 작성 필요(적용 방법 찾는데... 사례가 없어서...)
@Autowired
   DataSource dataSource;
  // authentication(login 사용자 인증)
  @Bean
  public UserDetailsManager userDetailsService() {
     JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
     users.setUsersByUsernameQuery(
             "select username, password, enabled from user_accounts where username= ?"
     );
     users.setAuthoritiesByUsernameQuery(
             "select username, role from user_accounts where username = ?"
     );
     return users;
  }

  ===============================================
  필터 거는 방법 (위에서 부터 아래로 적용됨)
  //authorization(사용자별 권한 설정)
   @Bean
   public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
      http
              .authorizeHttpRequests((auth) ->  auth
                      .requestMatchers("/projects/new").hasRole("ADMIN")   // 위의 db설정(ROLE_ADMIN)과 관련..
                      .requestMatchers("/projects/save").hasRole("ADMIN")
                      .requestMatchers("/employees/new").hasRole("ADMIN")
                      .requestMatchers("/employees/save").hasRole("ADMIN")
                      .requestMatchers("/", "/**").permitAll()
              )
              .formLogin(in -> in
                      .loginProcessingUrl("/").permitAll())
              .logout(logout -> logout
                      .deleteCookies("remove")
                      .invalidateHttpSession(false)
                      .permitAll()
              )
              .httpBasic(withDefaults())
              .headers(headers -> headers
                      .frameOptions(frameOptions -> frameOptions
                              .disable()
//                              .sameOrigin()
                      )
              );
      return http.build();
   }
}
headers 부분은 => security http response header
X-Frame-Options
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ...
.headers(headers -> headers
.frameOptions(frameOptions -> frameOptions
.sameOrigin()
)
);
return http.build();
}

==================================================

사용자 등록을 위한 html 작성(thymeleaf)

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:replace="layout :: header"><title>new employee</title></head>
<body>
<nav th:replace="layout :: navbar"></nav>
<div class="container-md pt-3">
<h1>Register User Account</h1>
<form th:action="@{/register/save}" th:object="${userAccount}" method="post">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text" id="" style="width: 220px">username</span>
</div>
<input type="text" class="form-control" th:field="*{userName}" placeholder="userName">
</div>

<div class="input-group mb-3 mt-2">
<div class="input-group-prepend">
<span class="input-group-text" id="inputGroup-sizing-default" style="width: 220px">Email</span>
</div>
<input type="text" class="form-control" aria-label="Default" aria-describedby="inputGroup-sizing-default" th:field="*{email}" placeholder="email">
</div>
<div class="input-group mb-3 mt-2">
<div class="input-group-prepend">
<span class="input-group-text" id="inputGroup-sizing-default3" style="width: 220px">password</span>
</div>
<input type="text" class="form-control" aria-label="Default" aria-describedby="inputGroup-sizing-default" th:field="*{password}" placeholder="password">
</div>
<input type="hidden" name="_csrf" th:value="${_csrf.token}">
<div class="container">
<div class="row">
<div class="col text-center ">
<button type="submit" style="width: 260px" class="btn btn-dark">Register User Account</button>
</div>
</div>
</div>
</form>
</div>

</body>
</html>

 

<form th:action="@{/register/save}" th:object="${userAccount}" method="post"> 

action에서 @{/경로} 를 지정하면 thymeleaf에서 자동으로 csrf.token을 전달함

일반 action으로 하면 별도로

<input type="hidden" name="_csrf" th:value="${_csrf.token}"> 을 지정해야 함

csrf 설정 안하면 접속 권한 없다고 나오(403) 신경써야함

============================================

controller 생성

@Controller
public class SecurityController {

@Autowired
BCryptPasswordEncoder passwordEncoder;

@Autowired
UserAccountRepository accountRepository;

@GetMapping("/register")
public String register(Model model) {
UserAccounts userAccount = new UserAccounts();
model.addAttribute("userAccount", userAccount);
return "security/register";
}

@PostMapping("/register/save")
public String saveUser(Model model, UserAccounts userAccount) {
userAccount.setPassword(passwordEncoder.encode(userAccount.getPassword()));
accountRepository.save(userAccount);
return "redirect:/";
}
}

여기서 password를 enconding 해서 db에 저장함

      userAccount.setPassword(passwordEncoder.encode(userAccount.getPassword()));

form 경로로 접속해서 사용자 생성

DB에 접속해서 사용자 권한 생성

===============================================

update user_accounts
set role = 'ROLE_USER'
where username = 'user';

update user_accounts
set role = 'ROLE_ADMIN'
where username = 'admin';

========================================================

==> 주의 사항 role을 그냥 USER, ADMIN으로 하면 
SecurityFilterChain에서 hasAthority("ADMIN")으로 해야 동작하고
ROLE_ADMIN 으로 하면 hasRole("ADMIN")으로 해도 동작함... (위의 security config 관련)

'Spring' 카테고리의 다른 글

spring security 6  (0) 2023.06.08
RESTful 아키텍처  (0) 2023.01.25
Spring boot 구성 시 버전 관련  (0) 2023.01.25
Spring boot 개요  (0) 2023.01.20
Spring 용어 정리  (0) 2023.01.20