나를 위한 정리이다.

 

1. DataSource 설정을 해준다. 

 

https://minwan1.github.io/2017/04/08/2017-04-08-Datasource,JdbcTemplate/

 

Wan Blog

WanBlog | 개발블로그

minwan1.github.io

위의 블로그에 잘 정리돼있는 개념을 살짝 정리하자.

 

더보기

JDBC

JDBC는 Java Database Connectivity의 약어로 자바 언어로 다양한 종류의 관계형 데이터베이스에 접속하고 SQL문을 수행하여 처리하고자할때 사용되는 표준 SQL 인터페이스 API이다.

 

 

Connection Pool

Connection Pool 이란 클라이언트의 요청 시점에 Connection을 연결하는 것이 아니라 미리 일정수의 Connection 을 만들어 놓고 필요한 어플리케이션에 전달하여 이용하도록 하는 방법이다.

 

Connection

DriverManager.getConnection()은 실제 자바프로그램과 데이터베이스를 네트워크상에서 연결을 해주는 메소드이다. 연결에 성공하면 DB와 연결 상태를 Connection 객체로 표현하여 반환한다. 보통 Connection하나당 트랜잭션 하나를 관리한다. 트랜잭션은 하나 이상의 쿼리에서 동일한 Connection 객체를 공유하는 것을 뜻한다. Mybatis의 SqlSession, Hibernate에 TransactionManager등의 Close가 이루어지면 Connection을 ConnectionPool에 반납하게 된다.

 

DataSource

javax.sql.DataSource 는 인터페이스는 ConnectionPool을 관리하는 목적으로 사용되는 객체로 Application에서는 이 Datasource 인터페이스를 통해서 Connection을 얻어오고 반납하는 등의 작업을 구현해야하는 인터페이스이다. Connection Pool에는 여러개의 Connection 객체가 생성되어 운용되어진다. 각각을 직접 웹 어플리케이션에서 이용하면 체계적인 관리가 힘들게 되므로 DataSource라는 개념을 도입하여 사용하고 있다. DataSource 인터페이스는 Connection pool을 어플리케이션단에서 어떻게 관리할지를 구현해야하는 인터페이스이다.

 

JdbcTemplate

Statement 처리를 위해서 Spring에서 JDBC를 지원하는데 그것이 JdbcTemplate 클래스, JdbcTemplate 클래스는 setDataSource()가 있어서 생성자 방식으로 Datasource를 지정가능하다. 아래는 Mybatis를이용한 간단한 JdbcTemplate예제이다.

 

 

 

JDBC는 자바에서 기본적으로 명시해 놓은, 데이터베이스 연결 및, 처리를 위한 표준 SQL *인터페이스 API야.

DataSource는 Connection Pool 을 관리하기 위해 쓰이는 *인터페이스야.

 

자바에서 기본적으로 SQL 을 다루기 위해서는 JDBC를 사용해.

그리고, 이 JDBC 를 사용하면서 (물론 Database를 이용하면 어쩔 수 없지만) Connection 이 생겨. 이 Connection 을 미리 생성해 놓는 것이 Connection Pool 이고, 이 Connection Pool 을 관리하기 위한 인터페이스는 DataSource인거지.

 

여기까지는 자바의 기본 스펙이야. 스프링만의 것이 아니지.

 

그런데 스프링은 이런 DataSource 설정을 아주 쉽게 할 수 있게 해줬지.

 

하여간. 애플리케이션을 만드는 첫 번째 스텝은 DataSource 설정이야.

 

@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties("datasource.config")
    public DataSource dataSource() {
         return DataSourceBuilder.create()
                .type(HikariDataSource.class)
                .build();
    }
}

application.yml 설정은 대충 다음과 같음

spring:
  jpa:
    hibernate:
      ddl-auto: update
    generate-ddl: true
    show-sql: true
    database-platform: "org.hibernate.dialect.MySQL5Dialect"
  jackson:
    time-zone: UTC
  datasource:
    initialization-mode: always
app:
  jwtSecret: [JWT Secret Key]
  jwtExpirationInMs: 604800000
datasource:
  config:
    jdbc-url: "jdbc:mysql://localhost:3306/[DB이름]"
    username: "아이디"
    password: "비밀번호"
    maximum-pool-size: "4"

 

2. UserDetails 를 반환할 수 있도록 하자

 

웬만한 웹 어플리케이션에는 계정이 있어야겠지. Spring Security 를 위한 인터페이스를 구현해야해.

 

@Data
@Entity
@Table(name = "accounts", uniqueConstraints = {
        @UniqueConstraint(columnNames = {"username"}),
        @UniqueConstraint(columnNames = {"email"})
})
@NoArgsConstructor
public class Account extends DateAudit {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank
    @Size(max = 40)
    private String name;

    @NotBlank
    @Size(max = 15)
    private String username;

    @NotBlank
    private String password;

    @NaturalId
    @NotBlank
    private String email;

    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "account_roles", joinColumns = @JoinColumn(name = "account_id"), inverseJoinColumns = @JoinColumn(name = "role_id"))
    private Set<Role> roles = new HashSet<>();


    public Account(String name, String username, String password, String email) {
        this.name = name;
        this.username = username;
        this.password = password;
        this.email = email;
    }
}

 

상속한 DateAudit 은 일단 무시하자. 위는 백기선 님의 Youtube 영상을 보며 따라 쓴 Account야. 사실 이렇게 안만들어도돼. 왜냐하면 중요한건 따로 있거든.

 

@Entity
@Data
@Table(name="roles")
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Enumerated(EnumType.STRING)
    @NaturalId
    @Column(length=60)
    private RoleName name;

}

 

Role 은 대충 위와 같이 만들자.

 

@Service
@Qualifier("custom_user_detail")
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    AccountRepository accountRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return accountRepository.findByUsername(username)
                .map(t -> new UserDetailsImpl(t))
                .orElseThrow(() -> new UsernameNotFoundException(username));
    }
}

 

중요한건 위와 같은 서비스인데. 정확히는 UserDetails를 반환하게 해주는 서비스야.

 

@Data
public class UserDetailsImpl extends User {
    private Long id;

    public UserDetailsImpl(Account account) {
        super(account.getUsername(), account.getPassword(), authorities(account));
        this.id = account.getId();
    }

    private static Collection<? extends GrantedAuthority> authorities(Account account) {
        return account.getRoles().stream()
                .map(role -> new SimpleGrantedAuthority(role.getName().name()))
                .collect(Collectors.toList());
    }
}

 

위를 보면 알 수 있다싶이, UserDetailsImpl 은 User을 상속하고 있어.

위의 User 클래스는 springframework.security.core.userdetails 에 있는 클래스야. 저 클래스가 애초에 UserDetails 를 상속하고 있지.

UserDetails ->(상속) User ->(상속) UserDetailsImpl(내가 만듦)

 

그리고 UserDetailsImpl 클래스는 Account Entity에 의존하고 있지.

 

이렇게 하면, UserDetailsImpl 의 생성자를 보면,

super() 안에

username, password, Collection<? extends GrantedAuthority> 를 넘기고 있어.

위를 보면 알다시피, SimpleGrantedAuthority 로 account의 roles 를 mapping 하고 있지.

 

정리

Spring Security를 사용할 수 있도록,

UserDetails 를 반환할 수 있게 서비스와 클래스를 구현하자.

 

 

3. Spring Security 설정

 

UserDetails 를 반환할 수 있도록 했으면, (물론 가입 및 로그인을 위한 컨트롤러 같은 것들도 만들어야 하지만.)

이제 계정을 이용해 인증을 진행할 수 있도록 해야 한다.

 

@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, jsr250Enabled = true, prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    @Qualifier("custom_user_detail")
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private JwtAuthenticationEntryPoint unAuthorizedHandler;

    @Autowired
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder);
    }

    @Bean(BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter() {
        return new JwtAuthenticationFilter();
    }

    @Override
    public void configure(WebSecurity webSecurity) {
        webSecurity.ignoring().antMatchers("/res/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/account/**").access("ROLE_USER")
                .antMatchers("/admin/**").access("ROLE_ADMIN")
                .antMatchers("/", "/login", "/login-error", "/encrypt_password", "/api/auth/signin", "/api/auth/signup").permitAll()
                .antMatchers("/**").authenticated();

        http.cors().and().csrf().disable().exceptionHandling().authenticationEntryPoint(unAuthorizedHandler)
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        http.formLogin()
                .loginPage("http://localhost:3000");

        http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

위는 짜집기하면서 더러워진 설정이다.

 

위에서 주목해야 할 것은.

 

1. configure override(1)

 

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder);
    }

userDetailService -> userDetails 반환

passowordEncoder -> 패스워드 암호화 및 복호화 담당.

 

2. configure override(2)

 

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/account/**").access("ROLE_USER")
                .antMatchers("/admin/**").access("ROLE_ADMIN")
                .antMatchers("/", "/login", "/login-error", "/encrypt_password", "/api/auth/signin", "/api/auth/signup").permitAll()
                .antMatchers("/**").authenticated();

        http.cors().and().csrf().disable().exceptionHandling().authenticationEntryPoint(unAuthorizedHandler)
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        http.formLogin()
                .loginPage("http://localhost:3000");

        http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }

 

JwtAuthenticationFilter 를 Bean 으로 만들어서 Filter 에 포함시킨다.

addFilterBefore (마지막줄) 을 잘 보자.

 

위 설정은 지금 정리돼지 않은 쓰레기이다. 왜냐면... 처음에는 session 방식으로 구현을 하다가, 중간에 jwt token 을 이용한 방식으로 바꿨기 때문이다. + spa 프론트를 사용했다. 따라서 프론트에서 알아서 토큰이 없으면 로그인 페이지로 이동시킨다.

 

따라소 http.formLogin().loginPage(~~~ 요 부분이 필요가 없다.

sessionCreatePolicy -> STATELESS 다. (jwtToken 을 사용하기 때문)

 

 

JWT 는 애초에 서버를 Stateless 하게 만드려는 목적이 있긴 한데... session 관리가 생각보다 복잡하다.

 

일단

 

1. DataSource 설정

2. UserDetails 설정

3. 인증 설정

 

여기까지 하면 기본적인 시작점 까지는 왔다고 생각한다.

블로그 이미지

맛간망고소바

,