티스토리 뷰

 네번째는 기존 프로젝트에 spring security를 적용해보겠습니다. 어떠한 시스템이든지 보안은 필수적인 시대가 되었습니다. Spring Security는 이러한 보안에서 중요한 요소인 인증과 권한 처리를 쉽게 하도록 도와줍니다. 인증은 일반적으로 로그인을 말하고, 권한은 admin과 일반 user 사이에 권한을 달리하여 특정 페이지는 일반 user에게 접속하지 못하도록 막는것을 뜻합니다.


Spring Boot에서는 이러한 Security도 간편하게 적용할 수 있습니다.

1. build.gradle 파일의 dependencies에 아래와 같은 dependency를 추가하고 dependency refresh를 통해 security lib를 내려받습니다.
compile('org.springframework.boot:spring-boot-starter-security')

2. 서버를 실행 후 http://localhost:8080/cusonar 로 접속해봅니다. 평소와 다르게 인증창이 나타나는 것을 볼 수 있습니다. default ID는 user이며, Password는 콘솔창에서 확인할 수 있습니다. 인증을 거치게 되면 해당 페이지로 이동할 수 있고, 아니면 401(권한없음) 에러가 나는것을 볼수 있습니다. 이것만 가지고는 실제 시스템에 사용하긴 무리가 있어 보입니다. 그래서 customizing이 필요합니다.

3. 우선 User를 정의해야 합니다. 보통은 DB 조회를 통해서 가져오는데 이 User가 시스템마다 각기 다르기 때문에 시스템에 맞게 custonmizing 해야 합니다. DB에 user와 authority 테이블을 만들고, com.cusonar.example.user.domain 패키지를 만들고, 아래에 User 클래스를 생성합니다. 기본적으로 Spring Security를 사용하기 위해서는 UserDetails 인터페이스에 정의된 메소드를 구현해줘야 합니다.
create table user (
     username varchar(20),
     password varchar(500),
     name varchar(20),
     isAccountNonExpired boolean,
     isAccountNonLocked boolean,
     isCredentialsNonExpired boolean,
     isEnabled boolean
);
     . 기본적으로 제공하는 기능에 충실하게 만들되 추가 필드를 참고하기 위해 name을 추가했습니다.
     . 만약 사용하지 않을 기능이라면 만들지 필드를 만들지 않으셔도 됩니다.
     . 각 필드는 영어를 그대로 해석하시면 됩니다. 계정이 만료되었는지, 계정이 잠겼는지(패스워드를 몇회 틀려서), 패스워드가 만료되었는지(몇 개월 주기로 변경), 계정 활성화 여부입니다.
create table authority (
    username varchar(20),
    authority_name varchar(20)
);
     . 권한은 한사람이 여러개 권한을 가질 수 있으므로 별도 테이블로 만들었습니다. username을 통해 user 테이블과 조인할 수 있습니다.
     . authority_name의 경우 number 타입으로 해서 enum으로 처리할 수도 있습니다. 저는 직관적인것을 위해서 varchar로 했습니다.

package com.cusonar.example.user.domain;

import java.util.Collection;

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

public class User implements UserDetails {
     private String username;
     private String password;
     private String name;
     private boolean isAccountNonExpired;
     private boolean isAccountNonLocked;
     private boolean isCredentialsNonExpired;
     private boolean isEnabled;
     private Collection<? extends GrantedAuthority> authorities;
    
     @Override
     public Collection<? extends GrantedAuthority> getAuthorities() {
          return authorities;
     }

     @Override
     public String getPassword() {
          return password;
     }

     @Override
     public String getUsername() {
          return username;
     }

     @Override
     public boolean isAccountNonExpired() {
          return isAccountNonExpired;
     }

     @Override
     public boolean isAccountNonLocked() {
          return isAccountNonLocked;
     }

     @Override
     public boolean isCredentialsNonExpired() {
          return isCredentialsNonExpired;
     }

     @Override
     public boolean isEnabled() {
          return isEnabled;
     }

     public String getName() {
          return name;
     }

     public void setName(String name) {
          this.name = name;
     }

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

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

     public void setAccountNonExpired(boolean isAccountNonExpired) {
          this.isAccountNonExpired = isAccountNonExpired;
     }

     public void setAccountNonLocked(boolean isAccountNonLocked) {
          this.isAccountNonLocked = isAccountNonLocked;
     }

     public void setCredentialsNonExpired(boolean isCredentialsNonExpired) {
          this.isCredentialsNonExpired = isCredentialsNonExpired;
     }

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

     public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
          this.authorities = authorities;
     }
}
      . UserDetails 인터페이스를 구현해줍니다. implements UserDetails만 붙여주시면 클래스에 빨간줄이 생깁니다. 거기에 마우스 갖다대고 Add unimplemented method를 클릭하면 자동으로 메소드가 생성됩니다.
     . 추가로 Alt + Shift + s 후에 Generate Getters and Setters를 선택해 모든 필드의 Getter와 Setter를 생성하면 됩니다. getter는 다 선언되어 있어서 setter만 자동적으로 생성됩니다.

4. 3에서 만든 User를 Spring Security에서 활용하기 위해서는 UserDetailsService 인터페이스를 구현한 Service Layer가 필요합니다. UserService에 활용될 Persistent Layer인 UserMapper도 같이 추가해보겠습니다.
UserMapper.java
package com.cusonar.example.user.mapper;

import java.util.List;
import org.apache.ibatis.annotations.Mapper;

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

@Mapper
public interface UserMapper {
     public User readUser(String username);
     public List<String> readAuthority(String username);
}
     . 지난번에 설명한 MyBatis를 이용해서 User를 가져 오는 Mapper 인터페이스입니다. Authority를 가져오는 인터페이스도 있어야 합니다.

     .  readAuthority의 경우 쿼리에서 String을 리턴하기 때문에 List<String> 타입으로 받아야 합니다.(만약 GrantedAuthority로 받으시려면 번외글 참조 : http://cusonar.tistory.com/12)


resource/mybatis/mapper/UserMapper.xml
<?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="String">
          SELECT authority_name FROM authority WHERE username = #{username}
     </select>
</mapper>
     . 앞서 만든 테이블에서 username을 통해 User를 가져오는 쿼리입니다. Authority을 가져오는 쿼리도 있습니다.

5. 그런데 이렇게 코드를 추가하다보면 이게 제대로 동작하는지 궁금할때가 있습니다. 그렇다고 매번 서버를 올려서 Controller까지 다 만들어서 확인을 해볼수는 없는 노릇입니다. 그래서 여기서 테스트 코드를 한번 작성해보겠습니다. test 폴더 밑에다가 com.cusonar.example.user 패키지를 만들고, UserMapperTest 클래스를 만듭니다. 그 전에 테스트를 해보려면 DB에 데이터가 있어야 하므로 추가도 시켜줍니다.
insert into user values ('cusonar', '1234', 'YCU', true, true, true, true);
insert into user values ('abc', 'abcd', 'ABC', true, true, true, true);
insert into authority values ('cusonar', 'ADMIN');
insert into authority values ('cusonar', 'USER');
insert into authority values ('abc', 'USER');

src/test/java/com.cusonar.example.user/UserMapperTest.java
package com.cusonar.example.user;
 
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.hasItems;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

import java.util.List;

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.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.mapper.UserMapper;
 
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = ExampleApplication.class)
@WebAppConfiguration
public class UserMapperTest {
     
     @Autowired UserMapper userMapper;
     
     @Test
     public void readUserTest() {
          User user = userMapper.readUser("cusonar");
          assertThat("cusonar", is(user.getUsername()));
          assertThat("YCU", is(user.getName()));
          assertThat("1234", is(user.getPassword()));
     }
     
     @Test
     public void readAuthorityTest() {
          List<String> authorities = userMapper.readAuthority("cusonar");
          assertThat(authorities, hasItems("ADMIN", "USER"));       
           
          authorities= userMapper.readAuthority("abc");
          assertThat(authorities, hasItem("USER"));     
     }
}

 
테스트까지 간단하게 해봤습니다.

어제부터 1일 1포스팅이었는데 바로 어겼네요. 어제 작성하다보니 테스트에서 막혀버리는 바람에 하루 연기되었습니다.
대신 오늘 2개 포스팅을 해야겠네요. 여기까지 User domain 및 mapper를 만드는 시간이었습니다.
댓글
댓글쓰기 폼