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
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);
}
}
회원가입
회원가입을 위한 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;
}
회원가입한 계정으로 로그인 테스트
추가로
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());
}
}
'Back-end > Spring Boot + REST API' 카테고리의 다른 글
Spring boot - blog application (REST API) : Versioning (버저닝) (0) | 2022.08.04 |
---|---|
Spring boot - blog application (REST API) : Spring Security + JWT (0) | 2022.08.02 |
Spring boot - blog application (REST API) : Validation @Valid (0) | 2022.07.06 |
Spring boot - blog application (REST API) : Global Exception Handling (0) | 2022.07.05 |
Spring boot - blog application (REST API) : ModelMapper (0) | 2022.07.04 |
댓글