SPRING - Spring Security

Udemy - RESTful Web Services, Java, Spring Boot, Spring MVC and JPA강좌를 수강하며 정리

Spring Security를 추가해보자.
Maven Repository에서 Spring Boot Security를 검색하면 다음과 같이 나온다.

들어가서 복사하고 pom.xml에 디펜던시를 추가해준다.

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>

이번에도 역시 버젼 속성을 빼고 작성해서, spring-boot-starter-parent을 따라갈 수 있도록 해준다.
그리고 JsonWebToken(jwt)도 쓸거기 때문에 jsonwebtoken도 검색해서 디펜던시에 추가해준다.

<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>0.9.1</version>
</dependency>

jwt는 spring-boot에 포함되는 디펜던시가 아니기 때문에 버젼정보까지 다 적어준다.
그리고, 서버를 껐다가 다시 켜준다.
그러면

다음과 같이 security password가 나오는 것을 볼 수 있다.
그리고 이전 글에서 했던 것 처럼 postman으로 요청을 보내보면,

위와 같이 response도 오지 않고, 401 Unauthorized를 볼 수 있다.
그리고 브라우저에 http://localhost:8080/users등을 입력해서 들어가보면 다음과 같은 창이 나온다.

spring security가 적용된 것이다. Username은 user, password는 위에서 봤던 콘솔창에 나왔던 비밀번호를 입력하면 들어갈 수 있다. 그리고 다음과 같은 화면이 나온다.

여기서 url주소를 지금까지 만들었던 users를 붙여서 입력해주면(http://localhost:8080/users)

위와 같이 UserController클래스에서 만들어줬던 get요청에 맞는 response를 볼 수 있다.

이제 다시 스프링으로 돌아와서, MobileAppWsApplication클래스에 BCryptPasswordEncoder객체를 반환하는 bCryptPasswordEncoder메서드를 작성하고 @Bean으로 등록해 다른데서 @Autowired해서 사용할 수 있게 만들어준다.

package com.appdeveloperblog.app.ws;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@SpringBootApplication
public class MobileAppWsApplication {

	public static void main(String[] args) {
		SpringApplication.run(MobileAppWsApplication.class, args);
	}
	
	@Bean
	public BCryptPasswordEncoder bCryptPasswordEncoder() {
		return new BCryptPasswordEncoder();
	}

}

그리고 UserServiceImpl클래스에서 기존의

userEntity.setEncryptedPassword("test");

이렇게 하드코딩되어있던 부분을

userEntity.setEncryptedPassword(bCryptPasswordEncoder.encode(user.getPassword()));

이렇게 고쳐준다.

We're going to create a new class which will define at least one mthod like sign up method that users can use to create a new account. And that method will be public and all other Web service entry points there will be protected and require authorization and authentication.
새로운 클래스를 만들어준다. com.appdeveloperblog.app.ws.security패키지를 만들어주고 WebSecurity라는 클래스를 만들어주었다. 그리고, WebSecurityConfigurerAdapter를 상속해주고, 클래스에는 @EnableWebSecurity어노테이션을 달아준다. WebSecurityConfigurerAdapter를 상속받은 config 클래스에 @EnableWebSecurity 어노테이션을 달면SpringSecurityFilterChain이 자동으로 포함된다고 한다.[Spring Security] 스프링시큐리티 시작하기 /기본세팅 그래서 http요청이 들어왔을 때 이 클래스를 거쳐가게 되는 듯하다.

package com.appdeveloperblog.app.ws.security;

import org.springframework.http.HttpMethod;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import com.appdeveloperblog.app.ws.service.UserService;

@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter{
	private final UserService userDetailsService;
	private final BCryptPasswordEncoder bCryptPasswordEncoder;
	
	public WebSecurity(UserService userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder) {
		this.userDetailsService = userDetailsService;
		this.bCryptPasswordEncoder = bCryptPasswordEncoder;
	}
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		// TODO Auto-generated method stub
//		super.configure(http);
		http.csrf().disable().authorizeRequests()
		.antMatchers(HttpMethod.POST, "/users")
		.permitAll().anyRequest().authenticated();
	}

	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		// TODO Auto-generated method stub
//		super.configure(auth);
		auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
	}
	
}

그리고 configure를 오버라이딩하여 접근 권한을 작성해준다. 여기서 작성해준 것은 Http Post요청이 '/users'로 들어왔을 때 접근을 허락해주고있다. 위 블로그를 참조하면 좋다. 이 클래스는 스프링에서 대략적으로 쓰여진 틀이 있는 듯 하다. 일단은 위와 같이 하자.
❗️주의 여기서 configure메서드를 오버라이드 해줄 때, 기존의 메서드(super.configure(http);)들 을 살려두면 안된다. 실행할 때 에러발생한다. 반드시 내용을 바꿔서 적어주자.

private final UserService userDetailsService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;

public WebSecurity(UserService userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder) {
	this.userDetailsService = userDetailsService;
	this.bCryptPasswordEncoder = bCryptPasswordEncoder;
}

이렇게 생성자 지정해주는 것도 UserDetailsService와 BCryptPasswordEncoder는 무조건 지정해줘야하는 듯 하다.(일단은) 그리고 UserDetailsService는 원래 UserDetailsService라는 스프링시큐리티에서 제공하는 객체를 사용하지만, 유저서비스 클래스에서 UserDetailsService를 상속해 사용하게 만들었다.
UserService

package com.appdeveloperblog.app.ws.service;

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

import com.appdeveloperblog.app.ws.shared.dto.UserDto;

public interface UserService extends UserDetailsService{
	UserDto createUser(UserDto user);
}

그리고 이렇게 되면 UserService를 implements하고있는 UserServiceImpl클래스는 UserDetailService를 구현해야한다.

package com.appdeveloperblog.app.ws.service.impl;

import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
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.appdeveloperblog.app.ws.UserRepository;
import com.appdeveloperblog.app.ws.io.entity.UserEntity;
import com.appdeveloperblog.app.ws.service.UserService;
import com.appdeveloperblog.app.ws.shared.Utils;
import com.appdeveloperblog.app.ws.shared.dto.UserDto;

@Service
public class UserServiceImpl implements UserService {
	
	@Autowired
	UserRepository userRepository;
	
	@Autowired
	Utils utils;
	
	@Autowired
	BCryptPasswordEncoder bCryptPasswordEncoder;
	
	@Override
	public UserDto createUser(UserDto user) {
		
		if(userRepository.findByEmail(user.getEmail()) != null) throw new RuntimeException("Record already exist");
		
		
		UserEntity userEntity = new UserEntity();
		BeanUtils.copyProperties(user, userEntity);
		
		String publicUserId = utils.generateUserId(30);
		userEntity.setUserId(publicUserId);
		userEntity.setEncryptedPassword(bCryptPasswordEncoder.encode(user.getPassword()));
		
		UserEntity storedUserDetails = userRepository.save(userEntity);
		
		UserDto returnValue = new UserDto();
		BeanUtils.copyProperties(storedUserDetails, returnValue);
		
		
		return returnValue;
	}

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		// TODO Auto-generated method stub
		return null;
	}

}

맨 밑의 메서드를 보면 override해줬다.
이제 POSTMAN으로 가서 다시 요청을 보내보자.
기존에 없던 email주소로 sign up요청을 보냈다.

전까지는 401 Unauthorized였는데 이제 제대로 된 reponse가 온다! request도 잘되었고, response도 잘 왔다! 물론 아이디가 중복되게 보내면 회원가입이 안됬다는 에러메세지가 response된다!

그럼 이번에는 users가 아닌 다른 url로 요청을 보내보면 어떻게 될까?

이런식으로 Access Denied된 것을 볼 수 있다!

주님 감사드립니다!

좋은 웹페이지 즐겨찾기