본문 바로가기
Back-end/Spring Boot + REST API

Spring boot - blog application (REST API) : Securing REST APIs

by javapp 자바앱 2022. 7. 20.
728x90

 

REST API 에서 스프링 시큐리티는 어떻게 적용이 될까?

 

Dependency

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

 

 

application.properties

logging.level.org.springframework.security=DEBUG

spring.security.user.name=kjh
spring.security.user.password=password
spring.security.user.roles=ADMIN

 

 

실행을 하면 시큐리티 자체 로그인 페이지로 넘어가게 되는데

application.properties 에서 user name, password 를 설정하여 시큐리티에 로그인 할 수 있다.

 

 


 

Postman 에서 시큐리티 적용된 서버와 통신

시큐리티 로그인을 하지 않고 통신을 하면 

401번 Unauthorized 상태로 통신 불가

 

Authorization 탭에서 Username, password 입력후 접근

 

 

 

 



 

 

In-memory Authentication

properties 를 쓰지 않고

UserDetails 내에 User를 담기위해 InMemoryUserDetailsManager 클래스 사용하여 담을 수 있다.

 

 

SecurityConfig 추가

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter 
{
	@Bean
	PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception 
	{
		http.csrf().disable()
				.authorizeHttpRequests()
				.antMatchers(HttpMethod.GET, "/api/**").permitAll()
				.anyRequest()
				.authenticated()
				.and()
				.httpBasic();
	}
	
	@Override
	@Bean
	protected UserDetailsService userDetailsService() {
		UserDetails kjhUser = User.builder().username("kjh").password(passwordEncoder().encode("password"))
												.roles("USER").build();
		UserDetails admin = User.builder().username("admin").password(passwordEncoder().encode("admin"))
												.roles("ADMIN").build();

		return new InMemoryUserDetailsManager(kjhUser,admin);
	}
	
}

passwordEncoder 함수를 통해 비밀번호 암호화

kjh, password , USER

admin, admin, ADMIN

으로 로그인 가능

 

 

properties 주석 처리

 


 

@EnableGlobalMethodSecurity

@EnableGlobalMethodSecurity(prePostEnabled = true) 를 통해 어디든지 @PreAuthorize 를 사용할 수 있다.

 

SecurityConfig 

@Configuration
@@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter 
{

 

ex) PostController

	@PostMapping
	@PreAuthorize("hasRole('ADMIN')")
	public ResponseEntity<PostDto> createPost(@Valid @RequestBody PostDto postDto){
		return new ResponseEntity<>(postService.createPost(postDto),HttpStatus.CREATED);
	}

ADMIN 권한을 가진 User 만 접근 가능

 

 


 

Create JPA Entities

User , Role 

 

User

package com.springboot.blog.entity;


@Data
@Entity
@Table(name="users" , uniqueConstraints= {
		@UniqueConstraint(columnNames= {"username"}),
		@UniqueConstraint(columnNames= {"email"})
})
public class User 
{
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private long id;
	private String name;
	private String username;
	private String email;
	private String password;

	@ManyToMany(fetch =FetchType.EAGER, cascade=CascadeType.ALL)
	@JoinTable(name= "user_roles",
				joinColumns = @JoinColumn(name="user_id", referencedColumnName= "id"),
				inverseJoinColumns =@JoinColumn(name="role_id", referencedColumnName="id")
			)
	private Set<Role> roles;
}

@ManyToMany 관계 테이블 생성

 

 

Role

package com.springboot.blog.entity;

@Setter
@Getter
@Entity
@Table(name="roles")
public class Role 
{
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private long id;
	
	@Column(length= 60)
	private String name;
}

 

MySQL

MySQL

 

 

 


 

 

Creating JPA Repositories

UserRepository and RoleRepository

 

public interface UserRepository extends JpaRepository<User, Long> 
{
	Optional<User> findByEmail(String email);
	Optional<User> findByUsernameOrEmail(String username, String email);
	Optional<User> findByUsername(String username);
	Boolean existsByUsername(String username);
	Boolean existsByEmail(String email);
}

.

public interface RoleRepository extends JpaRepository<Role, Long>
{
	Optional<Role> findByName(String name);
}

 

 


 

 

UserDetailsService Interface Implementation

Spring Security에서 사용자의 정보를 담는 인터페이스는 UserDetails 인터페이스이다.

이 인터페이스를 구현하게 되면 Spring Security에서 구현한 클래스를 사용자 정보로 인식하고 인증 작업을 한다.

 

@Service
public class CustomUserDetailsService implements UserDetailsService
{
	private UserRepository userRepository;
	
	public CustomUserDetailsService(UserRepository userRepository) {
		super();
		this.userRepository = userRepository;
	}


	@Override
	public UserDetails loadUserByUsername(String usernameOrEmail) throws UsernameNotFoundException {
		User enUser= userRepository.findByUsernameOrEmail(usernameOrEmail, usernameOrEmail)
					.orElseThrow(()-> new UsernameNotFoundException("User not found with username or email: "+ usernameOrEmail));

		// 시큐리티로 감싸서 반환
		return new org.springframework.security.core.userdetails.User(enUser.getEmail(), enUser.getPassword(), mapRolesToAuthorizities(enUser.getRoles()));
	}
	
	private Collection<? extends GrantedAuthority> mapRolesToAuthorizities(Set<Role> roles){
		return roles.stream().map(role-> new SimpleGrantedAuthority(role.getName())).collect(Collectors.toList());
	}

}

 

 



 

SecurityConfig 재구현

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter 
{
	// 사용자 정의 유저 정보 DB와 접근
	@Autowired
	private CustomUserDetailsService customUserDetailsService;
	
	@Bean
	PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception 
	{
		http.csrf().disable()
				.authorizeHttpRequests()
				.antMatchers(HttpMethod.GET, "/api/**").permitAll()
				.anyRequest()
				.authenticated()
				.and()
				.httpBasic();
	}
	
	// 인증시 비밀번호 암호화
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder());
	}
	
//	@Override
//	@Bean
//	protected UserDetailsService userDetailsService() {
//		UserDetails kjhUser = User.builder().username("kjh").password(passwordEncoder().encode("password"))
//												.roles("USER").build();
//		UserDetails admin = User.builder().username("admin").password(passwordEncoder().encode("admin"))
//												.roles("ADMIN").build();
//
//		return new InMemoryUserDetailsManager(kjhUser,admin);
//	}
	
}

 

 

TEST

테이블에 유저 정보를 임의로 저장

 

users table

password의 해시값은 별도로 만들어서 저장

public class PasswordEncoderGenerator {
	public static void main(String[] args) {
		// test
		PasswordEncoder passwordEncoder= new BCryptPasswordEncoder();
		System.out.println(passwordEncoder.encode("password"));
		System.out.println(passwordEncoder.encode("admin"));
	}
}

 

 

roles table

user_roles table


Post man Test

  • 회원정보가 없을 경우, 비회원일 경우 접근이 안되는 것을 볼 수 있다.

회원정보가 없을 경우, 비회원일 경우 접근이 안되는 것을 볼 수 있다.

 

이는 SecurityConfig 클래스의 메소드에서

 

 

  • 회원일 경우

 

 

  • admin 접근만 허용하는 경우
  • @PreAuthorize("hasRole('ADMIN')") 에 의해서 접근 통제된 경우
	@PostMapping
	@PreAuthorize("hasRole('ADMIN')")
	public ResponseEntity<PostDto> createPost(@Valid @RequestBody PostDto postDto){
		return new ResponseEntity<>(postService.createPost(postDto),HttpStatus.CREATED);
	}

참고로 SecurityConfig 클래스에서 @EnableGlobalMethodSecurity(prePostEnabled = true) 를 통해 어디든지 @PreAuthorize 를 사용할 수 있다.

 

실패 성공

 

 

 



 

시큐리티 로그인

SecurityConfig

authenticationManager() 추가

	// 로그인을 위한 인증 매니저
	@Override
	@Bean
	protected AuthenticationManager authenticationManager() throws Exception {
		return super.authenticationManager();
	}
    
    	@Override
	protected void configure(HttpSecurity http) throws Exception 
	{
		http.csrf().disable()
				.authorizeHttpRequests()
				.antMatchers(HttpMethod.GET, "/api/**").permitAll()
				.antMatchers("/api/auth/**").permitAll()
				.anyRequest()
				.authenticated()
				.and()
				.httpBasic();
	}

.antMatchers("/api/auth/**").permitAll()  // 인증 경로에 접근 가능 하도록 설정

 

 

LoginDto 추가

package com.springboot.blog.payload;

import lombok.Data;

@Data
public class LoginDto 
{
	private String usernameOrEmail;
	private String password;
}

 

AuthController 추가

package com.springboot.blog.controller;

@RestController
@RequestMapping("/api/auth")
public class AuthController 
{
	@Autowired
	private AuthenticationManager authenticationManager;
	
	@PostMapping("/signin")
	public ResponseEntity<String> authnticateUser(@RequestBody LoginDto loginDto)
	{
		Authentication authentication= authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
				loginDto.getUsernameOrEmail()	, loginDto.getPassword()	));
		
		SecurityContextHolder.getContext().setAuthentication(authentication);
		return new ResponseEntity<>("User signed-in successfully",HttpStatus.OK);
	}
}

 

username으로 로그인

 

email로 로그인

 

 

 


 

회원가입

회원가입을 위한 SignUpDto 생성

package com.springboot.blog.payload;

import lombok.Data;

@Data
public class SignUpDto 
{
	private String name;
	private String username;
	private String email;
	private String password;
}

 

AuthController

회원가입 위한 registerUser() 메소드 생성

package com.springboot.blog.controller;

@RestController
@RequestMapping("/api/auth")
public class AuthController 
{
	@Autowired
	private AuthenticationManager authenticationManager;
	
	@Autowired
	private UserRepository userRepository;
	
	@Autowired
	private RoleRepository roleRepository;
	
	@Autowired
	private PasswordEncoder  passwordEncoder;
	
	@PostMapping("/signin")
	public ResponseEntity<String> authnticateUser(@RequestBody LoginDto loginDto)
	{
		Authentication authentication= authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
				loginDto.getUsernameOrEmail()	, loginDto.getPassword()	));
		
		SecurityContextHolder.getContext().setAuthentication(authentication);
		return new ResponseEntity<>("로그인 성공!",HttpStatus.OK);
	}
	
	@PostMapping("/signup")
	public ResponseEntity<?> registerUser(@RequestBody SignUpDto signUpDto)
	{
		// add check for username exists in a DB
		if(userRepository.existsByUsername(signUpDto.getUsername())) {
			return new ResponseEntity<>("유저 이름이 존재합니다.",HttpStatus.BAD_REQUEST);
		}
		
		// add check for email exists in DB
		if(userRepository.existsByEmail(signUpDto.getEmail())) {
			return new ResponseEntity<>("이메일이 존재합니다.",HttpStatus.BAD_REQUEST);
		}
		
		// create user Entity
		User user= new User();
		user.setName(signUpDto.getName());
		user.setUsername(signUpDto.getUsername());
		user.setEmail(signUpDto.getEmail());
		user.setPassword(passwordEncoder.encode(signUpDto.getPassword()));
		
		Role role= roleRepository.findByName("ROLE_USER").get();
		user.setRoles(Collections.singleton(role)); // 리스트에 하나의 객체를 set 타입으로 삽입할 경우
	
		userRepository.save(user);
		
		return new ResponseEntity<String>("회원가입 성공!",HttpStatus.OK);
	}
}

 

하이버네이트로 sql 쿼리 확인

Hibernate: insert into users (email, name, password, username) values (?, ?, ?, ?)
Hibernate: insert into user_roles (user_id, role_id) values (?, ?)

users 테이블과 user_roles 테이블에 insert 되었다.

 

User

더보기

package com.springboot.blog.entity;

@Data
@Entity
@Table(name="users" , uniqueConstraints= {
@UniqueConstraint(columnNames= {"username"}),
@UniqueConstraint(columnNames= {"email"})
})
public class User 
{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String name;
private String username;
private String email;
private String password;

@ManyToMany(fetch =FetchType.EAGER, cascade=CascadeType.ALL)
@JoinTable(name= "user_roles",
joinColumns = @JoinColumn(name="user_id", referencedColumnName= "id"),
inverseJoinColumns =@JoinColumn(name="role_id", referencedColumnName="id")
)
private Set<Role> roles;

 

user Table
user_roles

 

예외 처리 : username,. email

 

 

회원가입한 계정으로 로그인 테스트

 

 

추가로

Collections.singleton(role)

        Role role= roleRepository.findByName("ROLE_USER").get();
        user.setRoles(Collections.singleton(role)); // 리스트에 하나의 객체를 set 타입으로 삽입할 경우

유저 클래스에 있는 Set<Role> roles; 에 Role 객체 하나를 추가하고 싶을 경우 사용

 

List : singletonList(T o)
Map : singletonMap(K key, V value)
Set : Collections.singleton(T o)

 

 

 

이렇게 해서 스프링 시큐리티를 사용해보았는데요

로그인시 패스워드 암호화와 역할(Role) 부여,

역할 부여에 따른 페이지 접근에 차별을 둘 때 유용하게 쓰이는 것 같습니다.

 

 


 

 

SecurityConfig.java

더보기
package com.springboot.blog.config;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter 
{
    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    @Bean
    PasswordEncoder passwordEncoder() {
    	return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception 
    {
        http.csrf().disable()
        .authorizeHttpRequests()
        .antMatchers(HttpMethod.GET, "/api/**").permitAll()
        .antMatchers("/api/auth/**").permitAll()
        .anyRequest()
        .authenticated()
        .and()
        .httpBasic();
    }

    // 인증시 비밀번호 암호화
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    	auth.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder());
    }

    // 로그인을 위한 인증 매니저
    @Override
    @Bean
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    // @Override
    // @Bean
    // protected UserDetailsService userDetailsService() {
        // UserDetails kjhUser = User.builder().username("kjh").password(passwordEncoder().encode("password"))
        // .roles("USER").build();
        // UserDetails admin = User.builder().username("admin").password(passwordEncoder().encode("admin"))
        // .roles("ADMIN").build();
        //
        // return new InMemoryUserDetailsManager(kjhUser,admin);
    // }

}

 

AuthController.java

더보기
package com.springboot.blog.controller;

@RestController
@RequestMapping("/api/auth")
public class AuthController 
{
    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private RoleRepository roleRepository;

    @Autowired
    private PasswordEncoder  passwordEncoder;

    @PostMapping("/signin")
    public ResponseEntity<String> authnticateUser(@RequestBody LoginDto loginDto)
    {
        Authentication authentication= authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
        loginDto.getUsernameOrEmail() , loginDto.getPassword() ));

        SecurityContextHolder.getContext().setAuthentication(authentication);
        return new ResponseEntity<>("로그인 성공!",HttpStatus.OK);
    }

    @PostMapping("/signup")
    public ResponseEntity<?> registerUser(@RequestBody SignUpDto signUpDto)
    {
        // add check for username exists in a DB
        if(userRepository.existsByUsername(signUpDto.getUsername())) {
        	return new ResponseEntity<>("유저 이름이 존재합니다.",HttpStatus.BAD_REQUEST);
   		 }

        // add check for email exists in DB
        if(userRepository.existsByEmail(signUpDto.getEmail())) {
        return new ResponseEntity<>("이메일이 존재합니다.",HttpStatus.BAD_REQUEST);
        }

        // create user Entity
        User user= new User();
        user.setName(signUpDto.getName());
        user.setUsername(signUpDto.getUsername());
        user.setEmail(signUpDto.getEmail());
        user.setPassword(passwordEncoder.encode(signUpDto.getPassword()));

        Role role= roleRepository.findByName("ROLE_USER").get();
        user.setRoles(Collections.singleton(role)); // 리스트에 하나의 객체를 set 타입으로 삽입할 경우

        userRepository.save(user);

        return new ResponseEntity<String>("회원가입 성공!",HttpStatus.OK);
    }
}

 

User

더보기
package com.springboot.blog.entity;

@Data
@Entity
@Table(name="users" , uniqueConstraints= {
@UniqueConstraint(columnNames= {"username"}),
@UniqueConstraint(columnNames= {"email"})
})
public class User 
{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    private String name;
    private String username;
    private String email;
    private String password;

    @ManyToMany(fetch =FetchType.EAGER, cascade=CascadeType.ALL)
    @JoinTable(name= "user_roles",
    joinColumns = @JoinColumn(name="user_id", referencedColumnName= "id"),
    inverseJoinColumns =@JoinColumn(name="role_id", referencedColumnName="id")
    )
    private Set<Role> roles;
}

 

Role

더보기
package com.springboot.blog.entity;

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

    @Column(length= 60)
    private String name;
}

 

LoginDto

더보기
package com.springboot.blog.payload;

import lombok.Data;

@Data
public class LoginDto 
{
    private String usernameOrEmail;
    private String password;
}

 

SignUpDto

더보기
package com.springboot.blog.payload;

import lombok.Data;

@Data
public class SignUpDto 
{
    private String name;
    private String username;
    private String email;
    private String password;
}

 

CustomUserDetailsService 

더보기
package com.springboot.blog.security;

@Service
public class CustomUserDetailsService implements UserDetailsService
{
    private UserRepository userRepository;

    public CustomUserDetailsService(UserRepository userRepository) {
        super();
        this.userRepository = userRepository;
    }


    @Override
    public UserDetails loadUserByUsername(String usernameOrEmail) throws UsernameNotFoundException {
        User enUser= userRepository.findByUsernameOrEmail(usernameOrEmail, usernameOrEmail)
        .orElseThrow(()-> new UsernameNotFoundException("User not found with username or email: "+ usernameOrEmail));

        // 시큐리티로 감싸서 반환
        return new org.springframework.security.core.userdetails.User(enUser.getEmail(), enUser.getPassword(), mapRolesToAuthorizities(enUser.getRoles()));
    }

    private Collection<? extends GrantedAuthority> mapRolesToAuthorizities(Set<Role> roles){
        return roles.stream().map(role-> new SimpleGrantedAuthority(role.getName())).collect(Collectors.toList());
    }

}

 

 

댓글