티스토리 뷰
Spring Advanced : Spring Boot + Security login custom 메소드로 구현하기
Korean Eagle 2020. 6. 9. 00:591. 이 포스트는 Spirng Security를 사용하지만, Security filters 로그인을 맡기는 것이 아닌 직접 코딩하는 내용이다.
1-1 보통 WebSecurity Config 클래스의 AuthenticationMangerBuilder를 통하여 접근방법을 등록한다.
1-1-1 UserDetailsService를 구현하여 이 객체를 AuthenticationProvider에 제공하고
1-1-2 이것을 AuthenticationManager가 사용한다.
2. 작업 순서는
2-0 의존성 추가
2-1 데이터베이스 생성 및 연결
2-2 User, Role Entity 생성 및 연결
2-3 UserRepository, RoleRepository 생성
2-4 서비스 코드 생성
2-4-1 UserDetails 구현하는 UserDetailsImpl
2-4-2 로그인 로직을 가지는 SecurityService
2-4-3 컨트롤러가 사용하는 UserService
2-5 SecurityConfig 작성하기
2-6 UserController 생성
3. 의존성 추가
3-1 전체 소스를 다 넣을 건 아니기 때문에 의미가 없지만 데이터베이스, 보안 부분는 곡 필요하다.
<!-- 데이터베이스 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 보안 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<scope>compile</scope>
</dependency>
<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>9.0.34</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
4. 데이터베이스 생성
4-1 가장 간단한 구조이다.
4-2 user와 role이 서로 many to many의 구조를 갖는다.
5. Entity 설정
5-0 AbstractEntity 클래스 - @MappedSuperclass로 내부적으로 자식 클래스가 단순히 복사해서 Entity를 구성한다.
5-1 @MappedSuperclass에 대한 내용은 아래 포스트를 참고한다.
5-2 내용은 단순히 id 설정을 모든 entity에서 하기 싫어서 그냥 추출한 것 뿐이다.
package pe.pilseong.flightreservation.entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import lombok.Data;
@MappedSuperclass
@Data
public class AbstractEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
}
5-1 User Entity
5-1-1 ManyToMany 매핑만 신경쓰면 된다.
package pe.pilseong.flightreservation.entity;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Entity
@Table(name = "user")
@Data
@EqualsAndHashCode(callSuper=false)
public class User extends AbstractEntity{
@Column(name = "first_name")
private String firstName;
@Column(name = "last_name")
private String lastName;
@Column(name = "email")
private String email;
@Column(name = "password")
private String password;
@ManyToMany
@JoinTable(
name = "user_role",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles;
}
5-2 Role Entity
5-2-1 이 Entity는 GrantedAuthority 인터페이스를 를 상속받고 있다. getAuthority 메소드만 가지고 있다.
5-2-2 GrantedAuthority는 보안 모듈이 내부적으로 getAuthrity를 통해 Role정보를 가지고 온다.
5-2-3 이렇게 하지 않아도 되는데 나중에 UserDetails를 구현하는 User를 만들 때 지저분한 코드를 써야 한다.
5-2-4 GrantedAuthority를 구현한 경우는
return new org.springframework.security.core.userdetails.User(
user.getEmail(), user.getPassword(), user.getRoles()
);
5-2-5 구현하지 않은 경우는 아래처럼 작업하면 된다.
return new org.springframework.security.core.userdetails.User(
user.getEmail(), user.getPassword(),
user.getRoles().stream().map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList());
);
5-2-6 Role Entity 코드
package pe.pilseong.flightreservation.entity;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
import org.springframework.security.core.GrantedAuthority;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.JoinColumn;
@Entity
@Table(name = "role")
@Setter
@Getter
public class Role extends AbstractEntity implements GrantedAuthority {
private static final long serialVersionUID = 1L;
@Column(name = "name")
private String name;
@ManyToMany
@JoinTable(
name = "user_role",
joinColumns = @JoinColumn(name = "role_id"),
inverseJoinColumns = @JoinColumn(name = "user_id")
)
private Set<User> users;
@Override
public String getAuthority() {
return this.name;
}
}
6. Repository 생성
6-1 data-jpa를 사용하고 있기 때문에 JpaRepository를 상속한다.
6-2 UserRepository
6-2-1 아래 소스에서는 username으로 email을 사용하고 있기 때문에 별도의 메소드를 만들어 주었다.
package pe.pilseong.flightreservation.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import pe.pilseong.flightreservation.entity.User;
public interface UserRepository extends JpaRepository<User, Long> {
@Query("from User where email=:email")
User findUserByEmail(@Param("email") String email);
}
6-3 RoleRepository
package pe.pilseong.flightreservation.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import pe.pilseong.flightreservation.entity.Role;
public interface RoleRepository extends JpaRepository<Role, Long> {
}
7. Service 생성
7-0 UserDetailsServiceImpl - UserDetailsService를 구현한 클래스
7-0-1 UserService에 포함할 수도 있지만 보기 편하게 별도로 추출하였다.
7-0-2 위에서 설명한 것 처럼 User의 세번 째 인자가 Collection<GrantedAuthority>이므로 코드가 간단하다.
package pe.pilseong.flightreservation.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import pe.pilseong.flightreservation.entity.User;
import pe.pilseong.flightreservation.repository.UserRepository;
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = this.userRepository.findUserByEmail(username);
if (user == null) {
throw new UsernameNotFoundException("User not found :: " + username);
}
return new org.springframework.security.core.userdetails.User(
user.getEmail(), user.getPassword(), user.getRoles()
);
}
}
7-1 로그인 로직을 담는 SecurityService 인터페이스 - login 정보를 제공하고 결과만 알려주면 된다.
7-1-1 이 인터페이스를 사용하면 간단하게 로그인 처리가 가능하다.
package pe.pilseong.flightreservation.service;
public interface SecurityService {
boolean login(String username, String password);
}
7-2 SecurityServiceImple 코드
7-2-1 여기에 핵심로직이 다 들어 있다.
7-2-2 위에서 작성한 UserDetailsService를 주입받아서 사용하고 있다. 데이터베이스 정보를 가지고 온다.
7-2-3 로그인 처리는 인증관리자에서 하기 때문에 주입받아야 한다.
7-2-3-1 스프링 2.x 에서는 인증관리자가 기본적으로 외부로 노출되지 않는다. 별도의 Bean 생성이 필요하다.
7-2-3-2 이 작업은 다음 항에서 설명한다.
7-2-4 주어진 계정, 비밀번호와 데이터베이스의 정보와 일치하는지 인증관리자를 통해 처리한다.
7-2-5 인증 정보가 일치하는 경우 보안 컨텍스트에 인증 정보를 저장하게 된다. 인증이 필요시마다 체크한다.
7-2-6 이 인증정보는 세션과 일치하므로 원할 때 마다 접근하여 인증 정보를 받아올 수 있다.
package pe.pilseong.flightreservation.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;
@Service
public class SecurityServiceImpl implements SecurityService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsServce;
@Override
public boolean login(String username, String password) {
UserDetails userDetails = this.userDetailsServce.loadUserByUsername(username);
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
authenticationManager.authenticate(token);
boolean result = token.isAuthenticated();
if (result) {
SecurityContextHolder.getContext().setAuthentication(token);
}
return result;
}
}
7-2 UserService 인터페이스
7-2-1 사용자 저장과 로그인 하는 메소드가 있다.
package pe.pilseong.flightreservation.service;
import pe.pilseong.flightreservation.dto.UserDTO;
public interface UserService {
void saveUser(UserDTO userDTO);
boolean login(UserDTO userDTO);
}
7-3 UserServiceImpl 클래스
7-3-1 컨트롤러에서 이 서비스를 호출하여 인증을 처리하게 된다. login을 보면 SecurityService의 login을 사용한다.
7-3-2 회원가입 시는 데이터베이스 저장 전에 암호화하는 것이 필요하다. 여기서는 BCrypt를 사용하였다.
package pe.pilseong.flightreservation.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import pe.pilseong.flightreservation.dto.UserDTO;
import pe.pilseong.flightreservation.entity.User;
import pe.pilseong.flightreservation.repository.UserRepository;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private SecurityService securityService;
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Override
public void saveUser(UserDTO userDTO) {
User user = new User();
user.setFirstName(userDTO.getFirstName());
user.setLastName(userDTO.getLastName());
user.setEmail(userDTO.getEmail());
user.setPassword(this.passwordEncoder.encode(userDTO.getPassword()));
this.userRepository.save(user);
}
@Override
public boolean login(UserDTO userDTO) {
return this.securityService.login(userDTO.getEmail(), userDTO.getPassword());
}
}
7-3-3 UserDTO는 다음과 같다.
package pe.pilseong.flightreservation.dto;
import lombok.Data;
@Data
public class UserDTO {
private Long id;
private String firstName;
private String lastName;
private String email;
private String password;
private String confirmPassword;
}
8. 보안 설정을 작성한다.
8-1 WebConfig
8-1-1 가장 중요한 부분은 인증관리자를 생성하는 부분이다. 수동으로 해야 한다.
8-1-2 WebSecurityConfigurerAdapter를 Override하는 메소드는 이름이 중요하다. authenticationManagerBean이다.
8-1-3 암호화 클래스 생성도 별도로 해주어야 한다.
8-1-4 인증없이 페이지로 접근하는 경우 인증화면으로 전환이 필요하다. 그것을 위해서 formLogin을 사용하였다.
package pe.pilseong.flightreservation.security.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
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;
@Configuration
@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/showRegistration", "/", "/index.html", "/registerUser",
"/login", "/showLogin", "/login/*", "/reservations/*")
.permitAll()
.antMatchers("/admin/showAddFlight").hasAuthority("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/showLogin").permitAll()
.and()
.csrf().disable();
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
9. 컨트롤러 작성
9-1 UserController이다.
9-1-1 어떻게 사용하는지만 보면 된다.
9-1-2 UserService를 주입 받아서 login 메소드만 호출하면 된다.
package pe.pilseong.flightreservation.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import pe.pilseong.flightreservation.dto.FlightSearchDTO;
import pe.pilseong.flightreservation.dto.UserDTO;
import pe.pilseong.flightreservation.service.UserService;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
public class UserController {
@Autowired
private UserService userService;
private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class);
@GetMapping("/showLogin")
public String showLogin(Model model) {
model.addAttribute("user", new UserDTO());
return "/login/login";
}
@PostMapping("/login")
public String login(@ModelAttribute UserDTO userDTO, Model model) {
LOGGER.info("Inside login " + userDTO.toString());
if (this.userService.login(userDTO)) {
model.addAttribute("flightSearch", new FlightSearchDTO());
return "findFlights";
} else {
return "redirect:showLogin";
}
}
@GetMapping("/showRegistration")
public String showRegistrationPage(Model model) {
LOGGER.info("Inside showRegistrationPage");
model.addAttribute("user", new UserDTO());
return "/login/registerUser";
}
@PostMapping(value = "/registerUser")
public String registerUser(@ModelAttribute UserDTO userDTO, Model model) {
LOGGER.info("Inside registerUser " + userDTO.toString() );
if (userDTO.getPassword().equals(userDTO.getConfirmPassword())) {
this.userService.saveUser(userDTO);
return "redirect:showLogin";
} else {
userDTO.setPassword("");
userDTO.setConfirmPassword("");
model.addAttribute("user", userDTO);
return "/login/registerUser";
}
}
}
'Spring > Spring Advanced' 카테고리의 다른 글
WebFlux : Spring MVC에서 WebFlux 변환 (0) | 2020.08.30 |
---|---|
WebFlux : 데이터를 찾지 못한 경우 Exception 발생하기 (0) | 2020.08.29 |
Spring Advanced : Rest + Security + Thymeleaf 로그인, 회원가입 기능이 포함된 Rest Template CRUD 클라이언트 작성하기 (0) | 2020.05.27 |
Spring Advanced : Rest + Security + Data JPA 로그인, 회원가입 기능이 포함된 CRUD서비스 작성하기 (1) | 2020.05.25 |
Spring Advanced : REST + Hibernate with Java Config - CRUD 서비스 인증 추가 (0) | 2020.05.23 |
- Total
- Today
- Yesterday
- 도커 개발환경 참고
- AWS ARN 구조
- Immuability에 관한 설명
- 자바스크립트 멀티 비동기 함수 호출 참고
- WSDL 참고
- SOAP 컨슈머 참고
- MySql dump 사용법
- AWS Lambda with Addon
- NFC 드라이버 linux 설치
- electron IPC
- mifare classic 강의
- go module 관련 상세한 정보
- C 메모리 찍어보기
- C++ Addon 마이그레이션
- JAX WS Header 관련 stackoverflow
- SOAP Custom Header 설정 참고
- SOAP Custom Header
- SOAP BindingProvider
- dispatcher 사용하여 설정
- vagrant kvm으로 사용하기
- git fork, pull request to the …
- vagrant libvirt bridge network
- python, js의 async, await의 차이
- go JSON struct 생성
- Netflix Kinesis 활용 분석
- docker credential problem
- private subnet에서 outbound IP 확…
- 안드로이드 coroutine
- kotlin with, apply, also 등
- 안드로이드 초기로딩이 안되는 경우
- navigation 데이터 보내기
- 레이스 컨디션 navController
- raylib
- 스프링
- hibernate
- jsp
- form
- one-to-many
- Security
- Validation
- 하이버네이트
- WebMvc
- MYSQL
- 설정하기
- Spring Security
- Angular
- 자바
- 상속
- spring boot
- Many-To-Many
- 매핑
- login
- Spring
- mapping
- XML
- Rest
- 스프링부트
- crud
- RestTemplate
- one-to-one
- 설정
- 로그인
- 외부파일