WebSecurityConfigurerAdapter를 상속받아 HttpSecurity를 이용해 설정한다.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// JWT 제공 클래스
private final JwtProvider jwtProvider;
// 인증 실패 또는 인증헤더가 전달받지 못했을때 핸들러
private final AuthenticationEntryPoint authenticationEntryPoint;
// 인증 성공 핸들러
private final AuthenticationSuccessHandler authenticationSuccessHandler;
// 인증 실패 핸들러
private final AuthenticationFailureHandler authenticationFailureHandler;
// 인가 실패 핸들러
private final AccessDeniedHandler accessDeniedHandler;
/**
* Security 적용 무시
*/
@Override
public void configure(WebSecurity web) {
web.ignoring()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations())
.mvcMatchers("/docs/**");
}
/**
* 보안 기능 초기화 및 설정
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
final String[] GET_WHITELIST = new String[]{
"/login",
"/user/login-id/**",
"/user/email/**",
"/affiliate"
};
final String[] POST_WHITELIST = new String[]{
"/client-user"
};
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint) // 인증 실패
.accessDeniedHandler(accessDeniedHandler) // 인가 실패
.and().authorizeRequests()
.antMatchers(HttpMethod.GET, GET_WHITELIST).permitAll() // 해당 GET URL은 모두 허용
.antMatchers(HttpMethod.POST, POST_WHITELIST).permitAll() // 해당 POST URL은 모두 허용
.antMatchers("/client-user/**").hasAnyRole(UserType.CL.getRoll()) // 권한 적용
.anyRequest().authenticated() // 나머지 요청에 대해서는 인증을 요구
.and() // 로그인하는 경우에 대해 설정함
.formLogin().disable() // 로그인 페이지 사용 안함
// .loginPage("/user/loginView") // 로그인 성공 URL을 설정함
// .successForwardUrl("/index") // 로그인 실패 URL을 설정함
// .failureForwardUrl("/index").permitAll()
.addFilterBefore(authenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(jwtFilter(), UsernamePasswordAuthenticationFilter.class);
}
/**
* 사용자 요청 정보로 UserPasswordAuthenticationToken 발급하는 필터
*/
@Bean
public CustomAuthenticationFilter authenticationFilter() throws Exception {
CustomAuthenticationFilter customAuthenticationFilter = new CustomAuthenticationFilter(authenticationManager());
// 필터 URL 설정
customAuthenticationFilter.setFilterProcessesUrl("/login");
// 인증 성공 핸들러
customAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
// 인증 실패 핸들러
customAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
// BeanFactory에 의해 모든 property가 설정되고 난 뒤 실행
customAuthenticationFilter.afterPropertiesSet();
return customAuthenticationFilter;
}
/**
* JWT의 인증 및 권한을 확인하는 필터
*/
@Bean
public JwtFilter jwtFilter() {
return new JwtFilter(jwtProvider);
}
}
사용자 요청 정보로 UserPasswordAuthenticationToken 발급 후 AuthenticationManager에게 전달하고 AuthenticationProvider의 인증 메서드를 실행하는 UsernamePasswordAuthenticationFilter를 구현
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public CustomAuthenticationFilter(AuthenticationManager authenticationManager) {
super.setAuthenticationManager(authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
final UsernamePasswordAuthenticationToken authRequest;
final LoginDTO loginDTO;
try {
// 사용자 요청 정보로 UserPasswordAuthenticationToken 발급
loginDTO = new ObjectMapper().readValue(request.getInputStream(), LoginDTO.class);
authRequest = new UsernamePasswordAuthenticationToken(loginDTO.getLogin(), loginDTO.getPass());
} catch (IOException e) {
throw new NotValidException();
}
setDetails(request, authRequest);
// AuthenticationManager에게 전달 -> AuthenticationProvider의 인증 메서드 실행
return this.getAuthenticationManager().authenticate(authRequest);
}
}
AuthenticationManager 하위에 실제로 인증을 처리할 AuthenticationProvider를 구현
@Component
@RequiredArgsConstructor
public class AuthenticationProviderImpl implements AuthenticationProvider {
private final UserDetailsService userDetailsService;
private final PasswordEncoder passwordEncoder;
/**
* 인증 구현
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 전달 받은 UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
// AuthenticaionFilter에서 생성된 토큰으로부터 아이디와 비밀번호를 추출
String username = token.getName();
String password = (String) token.getCredentials();
// 해당 회원 Database 조회
UserDetailsImpl userDetail = (UserDetailsImpl) userDetailsService.loadUserByUsername(username);
// 비밀번호 확인
if (!passwordEncoder.matches(password, userDetail.getPassword()))
throw new BadCredentialsException(userDetail.getUsername() + "Invalid password");
// 인증 성공 시 UsernamePasswordAuthenticationToken 반환
return new UsernamePasswordAuthenticationToken(userDetail.getUsername(), "", userDetail.getAuthorities());
}
/**
* provider의 동작 여부를 설정
*/
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
인증 과정 중 실제 Database에 회원을 데이터를 조회하는UserDetailsService를 구현
@RequiredArgsConstructor
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) {
User user = userRepository.findFirstUserByLoginOrderByIdAsc(username).orElseThrow(() -> new NotFoundDataException("User"));
return new UserDetailsImpl(
user.getLogin(),
user.getPass(),
user.getEmail(),
Collections.singleton(new SimpleGrantedAuthority("ROLE_" + user.getType().getRoll()))
);
}
}
Hello everyone. In this article, we will see the differences between JPA, Hibernate, and Spring Data JPA.
ORM Frameworks
ORM Stands for Object-Relational Mapping that maps the data in the database to the Java Class which is called an Entity. Not only that, but ORM also preserves the relationship between the tables at the Entity level.
There are many ORM frameworks in the market and the famous ones for Java are
Hibernate
EclipseLink
iBATIS
What is Hibernate?
As we all know, Hibernate is an ORM framework that sits between the application and the database. Before Hibernate, developers used to write queries using JDBC and retrieve the data and manually set it to the DTO Objects and send it to the Front End. This was time-consuming and painful.
So Hibernate is a framework in the ORM layer that maps the relational data to the Java Objects. It also provides an abstraction to the developers so that they don't need to worry about the data source. It also provides configuration options to configure the data store and developers can also write queries with Hibernate
Features of Hibernate
1. Light Weight
2. Open Source
3. ORM (Object Relation Mapping)
4. High Performance
5. HQL (Hibernate Query Language)
6. Caching
7. Auto-Generation
8. Scalability
9. Lazy Loading
10. Database Independent
Lets us see only a few important features below
Hibernate Query Language (HQL)
SQL is low-level programming where developers have to query for the database columns in a database table. But HQL is simplified for developers in such a way that the Java class names and attributes are used in the query. Internally, hibernate converts HQL into SQL and executes it in the database.
Hibernate also supports native SQL queries along with the HQL but it is recommended to use HQL as it is independent of the underlying database. Whereas if we write SQL, the syntax differs from database to database
TheQuery interfaceprovides object-oriented methods and capabilities for representing and manipulating HQL queries.
Example of an HQL:
String queryStr = "SELECT e.name FROM Employee e";
Query query = session.createQuery(queryStr);
List results = query.list();
The attribute name is selected from the Entity Employee
Lazy Loading in Hibernate
Hibernate supports the following loading patterns
Eager Loadingis a design pattern in which data initialization occurs on the spot.
Lazy Loadingis a design pattern that we use to defer the initialization of an object as long as it’s possible and load only on demand
Lazy Loading is the default one and it makes the application efficient by not loading all the data and exhausting the DB Connection pool.
Eg: If a table has 1 million records and the relationship tables have another 1 million records. In case of lazy loading, it will only load the main table and only if requested, it will load data from the child tables.
Caching in Hibernate
Caching is the process of storing data into cache memory and improves the speed of data access.
Hibernate supports two levels of caching, first-level and second-level caching.
First Level Cache
The first level cache is a session-level cache and it is always associated with session-level object
Second Level Cache
Second-level cache is the session factory level cache and it is available across all sessions. For a second-level cache, we have to enable the cache and provide a cache provider like Ehcache and add its dependency.
JPA stands for Java Persistence API and it is the Java specification that defines how to persist java objects. It is considered as a link between an object-oriented model and a relational database system. Hibernate is the standard implementation of JPA. JPA cannot be used alone and it always needs an implementation like Hibernate, EclipseLink, iBatis, etc.
For data persistence, thejava. persistencepackage contains the JPA classes and interfaces.
JPA providesJPQL(Java Persistence Query Language) andHQLprovided by Hibernate is a superset of it. Either can be used in the application.
2. JPA providesEntityManagerFactory interfacewhereas Hibernate providesSessionFactoryto create the Session instances
3. For CRUD operations on instances of mapped entity classes, JPA usesEntityManagerwhereas Hibernate uses theSessioninterface
So, as seen above JPA provides its in-built stuff so that things won't break if we change to other ORM frameworks later and it will remain consistent.
But Hibernate provides advanced features and if you are sure that you will not change the ORM framework, then it's better to stick to the Hibernate specs.
What is Spring Data JPA?
There is always confusion between JPA and Spring Data JPA.
As we saw above, JPA is a standard for defining the persistence layer andSpring Data JPAis a sub-project under the Spring Framework umbrella which allows Spring applications to integrate with JPA.
Spring Data JPA is an abstraction that makes it easier to work with a JPA provider like Hibernate which is used by default. Specifically, Spring Data JPA provides a set of interfaces for easily creating data access repositories.
Before Spring Data JPA, we used to write all the CRUD methods in every single DAO and write an implementation for those. But then came Spring Data JPA, which abstracts the developer from that, and behind the scenes, it provides implementations for the basic crud methods. This avoids a lot of boilerplate code and makes it efficient for developers. We can still add custom methods and can use HQL or criteria etc.
Spring Data JPA also allows developers to use Transactional annotation to control the transaction boundaries.
Spring Data JPA comes with a concept called JPA Repository and Query methods. JPA Repository is nothing but a set of interfaces that defines query methods likefindByFirstNameor findByLastName etc. These methods are converted intolow-level SQL queriesby Spring.
Because of this cleaner approach, many Spring-based applications are using Spring Data JPA to implement theirData Access Layeror DAO Layer
The Spring Data Jpa dependency can be added as below and this will do the data source auto-configuration as well. After adding this, we just need to add a database dependency to make sure it is available in the class path.
in this article, we saw what is Hibernate, JPA, and Spring Data JPA. In a nutshell, JPA is a specification and we have to use one of its implementations to connect to the database. Spring Data JPA cannot work without a JPA provider and a JPA provider is mandatory to connect with the database. Spring Data JPA is not a mandatory one but it is for adding convenience to developers by removing the boilerplate code
Hope you all have enjoyed this article. Happy learning!!!
Please read my article on Transaction Management if you want to know more
public UserJoinResponse joinUser(UserJoinRequest userJoinRequest) {
User saveUser = userJoinRequest.toEntity();
User user = userRepository.save(user);
}
정상적으로 User가 데이터베이스에 저장되었다면, 클라이언트에게 보내줄 데이터를 만들어 보겠습니다.
저는 예제로 클라이언트에게 유저의 email을 클라이언트에게 보내보겠습니다. 현재 서비스단의joinUser()함수의 리턴 값은 UserJoinResponse 이기 때문에User를 UserJoinResponse로 변경해주는 작업이 필요합니다.
이 또한 이 변경의책임은 누구한테 있는지 고민한다면User에게 있을 것입니다. 그러므로 User 클래스 안에 UserJoinResponse 객체를 만들어 주는 함수가 있어야 합니다.
회원가입이 성공적으로 완료되면 클라이언트에게 email 값을 주는 UserJoinResponse 입니다.
public class UserJoinResponse {
String email;
}
User의 함수는 다음과 같습니다.
public class User {
...필드 생략
public UserJoinResponse toUserJoinResponse() {
return User.UserJoinResponse()
.email(email)
.build();
}
}
지금까지 제가 JPA를 사용하면서 주로 사용했던 내용을 작성했습니다.
저는 위와 같은 방법을 주로 사용하지만, 이렇게 클래스마다 Entity <-> DTO 변환 클래스를 작성하지 않아도, 자동으로 변환해주는 라이브러리가 있어서 소개해 드리려고 합니다.
😯 ModelMapper
ModelMapper 라이브러리를 사용하게 되면 간편하게 Entity <-> DTO 변환이 가능해 집니다.
public UserJoinResponse joinUser(UserJoinRequest userJoinRequest) {
ModelMapper mm = new ModelMapper();
User user = new User();
mm.map(userJoinRequest, user);
User user = userRepository.save(user);
}
이런 방법도 있지만, 개인적으로 처음 설명해드렸던 방법을 추천합니다.
🧐 Entity vs DTO
이렇게 특정 필드만 받을 수 있게 DTO 클래스를 만들었습니다. 그러나 프로젝트가 커지게 되면 DTO 클래스는 계속 증가합니다.
그냥 Entity 클래스를 받아서 필요한 값만 추출해서 사용하면 될것인데, 왜 굳이 이렇게 DTO 클래스를 새로 만들어서 사용 할까요?
DTO 클래스를 사용하는 이유는 다음과 같습니다.
서비스 단은 데이터베이스와 독립적이어야 합니다. 데이터베이스의 변경사항이 있으면, Entity를 사용하는 서비스 단에도 변경이 일어날 수 있습니다.
또한 Entity 클래스를 DTO로 재사용 하는 일은 코드가 더러워질 수 있습니다. 클래스가 DTO로써 사용 될 때 사용하는 메소드와 Entity로써 사용 될 때 사용하는 메소드가 공존하게 됩니다. 이런 점에서관심사가 깔끔하게 분리되지 않고, 클래스의 결합력만 높이게 됩니다.