게시판 프로젝트-7

이호영·2022년 8월 13일
0

board project

목록 보기
7/7

Spring Security 적용

Security 적용에 앞서 추가,변경해야할 사항이 있다.

application properties

context-path=/
server.servlet.encoding.charset=UTF-8
server.servlet.encoding.enabled=true
server.servlet.encoding.force=true

build.gradle

 // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security
    implementation group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: '2.7.2'

SecurityConfig 생성

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityCofig {

    @Autowired
    private PrincipalDetailService principalDetailService;

    @Bean //Ioc
    public BCryptPasswordEncoder encodePW(){
        return new BCryptPasswordEncoder();
    }

    protected void configure(HttpSecurity httpSecurity) throws Exception{
        httpSecurity.csrf().disable()
                .authorizeRequests()
                .antMatchers("/auth/**", "/", "/js/**", "/css/**", "/image/**")
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .loginProcessingUrl("/auth/loginProc")
                .defaultSuccessUrl("/");

    }
}

@Configuration
해당 클래스를 Configuration으로 등록 (IoC, 스프링 컨테이너가 관리)

@EnableWebSecurity
Spring Security 활성화,필터로 등록이 된다.

.csfr()
CSRF를 방지하는 기능을 지원한다.
임의의 csrf 토큰을 지급한 후 이를 이용해 csrf 토큰이 없거나 불일치하면 4xx 코드를 리턴한다.
이를 비활성화 하기 위해 disable 처리 한다.
CSRF는 사이트 간 요청이 발생하기 쉬운 웹에 대해 요청할 때 필요하다.

UserApiController

@RestController
public class UserApiController {

    @Autowired
    private UserService userService;

    @PostMapping("/auth/joinProc")
    public ResponseDto<Integer> saveUser(User user) {
        int result = userService.saveUser(user);
        return new ResponseDto<>(HttpStatus.OK.value(), result);
    }
}

UserService

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private BCryptPasswordEncoder encoder;

    @Transactional(readOnly = true)
    public User login(User user) {
        return userRepository.findByUsernameAndPassword(user.getUsername(), user.getPassword());
    }

    @Transactional
    public int saveUser(User user) {
        try {
            String rawPassword = user.getPassword();
            String encPassword = encoder.encode(rawPassword);
            user.setPassword(encPassword);
            user.setRole(RoleType.USER);
            userRepository.save(user);
        } catch (Exception e) {
            e.printStackTrace();
            return -1;
        }
        return 1;
    }
}

BCryptPasswordEncoder 객체를 멤버변수로 선언한다.
회원가입시 UserApiController에서 UserService 클래스의 메서드를 호출하면
encode() 메서드 호출하여 인수로 받은 비밀번호를 인코딩한다.
인코딩된 값을 String 타입으로 다시 저장하고 User 객체에 데이터를 넣는다.
이렇게 하면 회원가입시 입력한 비밀번호가 암호화되어 DB에 저장이 된다.

PricipalDetail

모든 클래스는 UID를 가지고 있다.
클래스의 내용이 변경되면 UID 값 역시 변경된다.
직렬화하여 통신하고 UID값으로 통신한 게 정상인지 확인하는데,
그 값이 바뀌면 다른 클래스로 인식하게 된다.
이를 방지하기 위해 고유값으로 미리 명시를 해주는 부분이 serialVersionUID이다.

//principal (접근 주체) = 세션처럼 사용 = Spring Security Context 에 보관됨
public class PrincipalDetail implements UserDetails {

    private static final long serialVersionUID = 7645618956884452156L;

    public PrincipalDetail(User user) {
        this.user = user;
    }

    private User user;

    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }

    // 계정의 비밀번호를 리턴한다.
    @Override
    public String getPassword() {
        return user.getPassword();
    }

    // 계정의 이름을 리턴한다.
    @Override
    public String getUsername() {
        return user.getUsername();
    }

    // 계정이 만료되지 않았는 지 리턴한다. (true: 만료안됨)
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    // 계정이 잠겨있지 않았는 지 리턴한다. (true: 잠기지 않음)
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    // 비밀번호가 만료되지 않았는 지 리턴한다. (true: 만료안됨)
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    // 계정이 활성화(사용가능)인 지 리턴한다. (true: 활성화)
    @Override
    public boolean isEnabled() {
        return true;
    }

    // 계정이 갖고있는 권한 목록을 리턴한다. (권한이 여러개 있을 수 있어서 루프를 돌아야 하는데 우리는 한개만)
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {

        Collection<SimpleGrantedAuthority> collectors = new ArrayList<>();
        collectors.add(new SimpleGrantedAuthority("ROLE_"+user.getRole()));

        return collectors;
    }

}

PricipalDetailService

@Service
public class PrincipalDetailService implements UserDetailsService {
    @Autowired
    private UserRepository userRepository;

    // LoginForm에서 action="/loginProc" 되면
    // 스프링 필터 체인이 낚아채서 loadUserByUsername함수를 호출한다.
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User principal = userRepository.findByUsername(username)
                .orElseThrow(()->{
                    return new UsernameNotFoundException("해당 사용자를 찾을 수 없습니다. : "+username);
                });

        return new PrincipalDetail(principal); //세션에 유저 정보가 저장된다.
    }
}

UserRepository 추가

Optional<User> findByUsername(string username);

Test 코드

@SpringBootTest
public class SecurityTest {

    @Autowired
    private UserService userService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Test
    @DisplayName("패스워드 암호화 테스트")
    void passwordEncode() {
        // given
        String rawPassword = "12345678";

        // when
        String encodedPassword = passwordEncoder.encode(rawPassword);

        // then
        assertAll(
                () -> assertNotEquals(rawPassword, encodedPassword),
                () -> assertTrue(passwordEncoder.matches(rawPassword, encodedPassword))
        );
    }
}

테스트 결과 성공

0개의 댓글