티스토리 뷰

 
Spring Boot Security 3편입니다. 일단 지난 시간까지 DB의 계정과 패스워드를 비교해서 로그인을 하는 것까지 했습니다. 지금 상태로도 쓸려면 쓸순 있겠지만 만약 해커가 DB를 털었다간 큰일 나겠죠? 사용자 암호가 고스란히 넘어가는거니깐요. 그래서 이번에는 암호를 암호화하는 방법에 대해 진행해볼까 합니다. (암호를 암호화-_-?)

일단은 DB에 암호화된채로 들어가 있어야 사용자가 입력한 패스워드를 암호화 해서 DB와 비교할 수 있습니다. 따라서 사용자를 암호화해서 등록하는 것부터 시작할께요. (4.1 번외가 적용되었다고 가정하겠습니다.)

1. UserService 인터페이스에 유저 등록 메소드를 추가합니다. 유저 등록만 계속 하면 테스트 하기 어려워지므로 삭제 메소드와 읽기 메소드도 같이 추가해줍니다. 그리고 사용할 PasswordEncoder를 리턴해줄 메소드도 추가해줍니다. (why? singleton으로 사용하기 위해서)
* 왜 insert를 안쓰고 create를 써요? 저는 통일된 CRUD 메소드는 CRUD 단어를 사용합니다. 경험상 insert는 개발자마다 ins로 줄여버리기도 하더라구요. 그래서 전 CRUD 전체 단어를 사용하는걸 좋아합니다.
package com.cusonar.example.user.service;

import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetailsService;

import com.cusonar.example.user.domain.User;

public interface UserService extends UserDetailsService {
     Collection<GrantedAuthority> getAuthorities(String username);
     public User readUser(String username);
     public void createUser(User user);
     public void deleteUser(String username);
     public PasswordEncoder passwordEncoder();
}
 

2. UserServiceImpl 클래스에도 메소드를 추가 구현해줍니다. (왜 굳이 Service와 ServiceImpl로 나눌까요? 이건 나중에 Tips에서 다뤄봅시다. Tip이라기엔 아주 중요한거죠.)
package com.cusonar.example.user.service;

import java.util.Collection;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import com.cusonar.example.user.domain.User;
import com.cusonar.example.user.mapper.UserMapper;

@Service
public class UserServiceImpl implements UserService {
    
     @Autowired UserMapper userMapper;
     private PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

     @Override
     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
          User user = userMapper.readUser(username);
          user.setAuthorities(getAuthorities(username));
         
          return user;
     }
    
     public Collection<GrantedAuthority> getAuthorities(String username) {
          Collection<GrantedAuthority> authorities = userMapper.readAuthority(username);
         
          return authorities;
     }
    
     @Override
     public User readUser(String username) {
          User user = userMapper.readUser(username);
          user.setAuthorities(userMapper.readAuthority(username));
          return user;
     }

     @Override
     public void createUser(User user) {
          String rawPassword = user.getPassword();
          String encodedPassword = new BCryptPasswordEncoder().encode(rawPassword);
          user.setPassword(encodedPassword);
          userMapper.createUser(user);
          userMapper.createAuthority(user);
     }

     @Override
     public void deleteUser(String username) {
          userMapper.deleteUser(username);
          userMapper.deleteAuthority(username);
     }


     @Override
     public PasswordEncoder passwordEncoder() {
          return this.passwordEncoder;
     }
}
     . createUser 메소드는 userMapper의 createUser를 호출하기 전에 패스워드를 암호화해줍니다. 암호화에 사용된 인코더는 BCryptPasswordEncoder 입니다.
     . Spring Security에 사용하는 인코더는 PasswordEncoder 인터페이스를 구현한 것이어야 합니다. 이것만 구현해준다면 본인만의 암호화 기법을 구현하시면 됩니다.
     . PasswordEncoder는 인터페이스가 두개입니다. org.springframework.security.authentication.encoding.PasswordEncoder과 org.springframework.security.crypto.password.PasswordEncoder 두가지입니다. authentication 안에 있는 것은 deprecated(사용되지 않는) 된다고 적혀 있으니 사용하지 맙시다.

3. 이렇게 하려니 UserMapper에서 createUser, createAuthority, deleteUser, deleteAuthority를 추가해줘야합니다.
package com.cusonar.example.user.mapper;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;
import org.springframework.security.core.GrantedAuthority;

import com.cusonar.example.user.domain.User;

@Mapper
public interface UserMapper {
     public User readUser(String username);
     public List<GrantedAuthority> readAuthority(String username);
     public void createUser(User user);
     public void createAuthority(User user);
     public void deleteUser(String username);
     public void deleteAuthority(String username);
}

 
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cusonar.example.user.mapper.UserMapper">
     <select id="readUser" parameterType="String" resultType="User">
          SELECT * FROM user WHERE username = #{username}
     </select>
    
     <select id="readAuthority" parameterType="String" resultType="org.springframework.security.core.authority.SimpleGrantedAuthority">
          SELECT authority_name FROM authority WHERE username = #{username}
     </select>
    
     <insert id="createUser" parameterType="User">
          INSERT INTO user (username, password, name, isAccountNonExpired, isAccountNonLocked, isCredentialsNonExpired, isEnabled)
                    VALUES (#{username}, #{password}, #{name}, #{isAccountNonExpired}, #{isAccountNonLocked}, #{isCredentialsNonExpired}, #{isEnabled})
     </insert>
    
     <insert id="createAuthority" parameterType="org.springframework.security.core.GrantedAuthority">
          INSERT INTO authority (username, authority_name)
                    VALUES
                         <foreach item="authority" index="index" collection="authorities" separator=",">
                              (#{username}, #{authority})
                         </foreach>
     </insert>
    
     <delete id="deleteUser" parameterType="String">
          DELETE FROM user WHERE username = #{username}
     </delete>
    
     <delete id="deleteAuthority" parameterType="String">
          DELETE FROM authority WHERE username = #{username}
     </delete>
</mapper>
 
     . createRole을 보시면 foreach 구문을 사용합니다. 간단히 설명드리면 동적 SQL을 작성하는 구문으로 반복에 사용됩니다. collection에 사용될 변수를 입력하고, item은 반복되는 데이터를 표현합니다. seperator는 반복시에 반복되는 구문의 사이에 추가하는 값입니다
     . 위와 같이 했을 때, username=cusonar, authorities={USER, ADMIN}이 들어온다면 INSERT INTO role (username, rolename) VALUES (cusonar, ADMIN), (cusonar USER) 가 됩니다.

4. 여기까지 되었으면 정상적으로 들어가는지 확인해야 합니다. 간단하게 테스트 코드를 이용해서 등록 작업을 해봅시다.
package com.cusonar.example.user;

import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

import java.util.Collection;
import java.util.Iterator;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;

import com.cusonar.example.ExampleApplication;
import com.cusonar.example.user.domain.User;
import com.cusonar.example.user.service.UserService;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = ExampleApplication.class)
@WebAppConfiguration
public class UserServiceTest {
     @Autowired private UserService userService;
    
     private User user1;
    
     @Before
     public void setup() {
          user1 = new User();
          user1.setUsername("user1");
          user1.setPassword("pass1");
          user1.setAccountNonExpired(true);
          user1.setAccountNonLocked(true);
          user1.setName("USER1");
          user1.setCredentialsNonExpired(true);
          user1.setEnabled(true);
          user1.setAuthorities(AuthorityUtils.createAuthorityList("USER"));
     }
    
     @Test
     public void createUserTest() {
          userService.deleteUser(user1.getUsername());
          userService.createUser(user1);
          User user = userService.readUser(user1.getUsername());
          assertThat(user.getUsername(), is(user1.getUsername()));
         
          PasswordEncoder passwordEncoder = userService.passwordEncoder();
          assertThat(passwordEncoder.matches("pass1", user.getPassword()), is(true));

          Collection<? extends GrantedAuthority> authorities1 = user1.getAuthorities();
          Iterator<? extends GrantedAuthority> it = authorities1.iterator();
          Collection<GrantedAuthority> authorities = (Collection<GrantedAuthority>) user.getAuthorities();
          while (it.hasNext()) {
               GrantedAuthority authority = it.next();
               assertThat(authorities, hasItem(new SimpleGrantedAuthority(authority.getAuthority())));
          }
     }
}

 
5. 마지막으로 SecurityConfig에도 PasswordEncoder를 적용시켜줍시다.
package com.cusonar.example.config;

import org.springframework.beans.factory.annotation.Autowired;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import com.cusonar.example.user.service.UserService;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
     @Autowired UserService userService;
    
     @Override
     protected void configure(HttpSecurity http) throws Exception {
          http
               .csrf().disable()
               .authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
               .formLogin();
     }
    
     @Override
     protected void configure(AuthenticationManagerBuilder auth) throws Exception {
          auth.userDetailsService(userService)
               .passwordEncoder(userService.passwordEncoder())
               ;
     }
}

 
6. 테스트 코드에서 user1 계정이 등록되었으므로 user1/pass1 으로 로그인이 정상적으로 되는 것을 확인할 수 있습니다. 또한 DB 상에는 패스워드가 암호화 되서 들어가 있는것도 확인됩니다. 물론 기존에 등록된 cusonar와 abc 계정은 접속되지 않습니다. 두 계정도 똑같이 인코딩해서 넣어주면 정상적으로 로그인이 될겁니다.

여기까지 패스워드를 암호화해서 넣는 방법에 대해서 알아봤습니다. 물론 BCrypt를 사용할 필요는 없습니다. 다른 PasswordEncoder 구현체가 많으니 시스템에 맞는 것을 사용하시면 됩니다. 또한 솔루션을 사용하거나 기존에 암호화된 데이터들이 있는 경우에는 PasswordEncoder를 구현해서 사용하시면 됩니다. 이로써 DB 안의 패스워드가 그대로 노출되지 않게되었습니다.



 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함