diff --git a/day-13/api/Security.md b/day-13/api/Security.md new file mode 100644 index 0000000..8821b05 --- /dev/null +++ b/day-13/api/Security.md @@ -0,0 +1,2217 @@ +# 1. Basic Authentication + +## 1.1. Dependency + +Öncelikle projeye yeni bir bağımlılık eklenir. + +```xml + + org.springframework.boot + spring-boot-starter-security + +``` + +Yukarıdkai bağımlılık ifadesiyle artık API güvenliği ilk adım atılır. Konsol ortamında verilen **generated security password** ve **user** kullanıcı adı ile oturum açılabilir. + +- Logout olma şansı yoktur. +- Kullanıcı adı ve şifresine müdahale edilemez. + +## 1.2. Application Security Config + +**security/config** gibi bir isimle yeni bir klasör projeye dahil edilir. + +Gelen Request yapılarını hangi kurallara göre cevap verileceğini belirlemek üzere **ApplicationSecurityConfig** sınıfı kullanılır. +Bu sınıf **WebSecurityConfigurerAdapter** sınıfından kalıtılır. + +**config** metodu (HttpSecurity imzalı olanı) **Override** edilir. + +```java +@Configuration +@EnableWebSecurity +public class ApplicationSecurityConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // TODO Auto-generated method stub + super.configure(http); + } +} +``` + +## 1.3. Authentication Nasıl Yapılır? + +Bu metot üzerinde authentication işleminin nasıl yapılması gerektiği tanımlanır. Basic Authentication ile API güvenliğini sağlayalım. + +```java +@Configuration +@EnableWebSecurity +public class ApplicationSecurityConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeRequests() + .anyRequest() + .authenticated() + .and() + .httpBasic(); + } +} +``` + +Bu işlem sonucunda herhangi bir kaynak üzerinden GET, POST, PUT ve DELETE işlemlerinin yapılması tavsiye edilir. +User ve password bilgisi girilmeden hiçbir http isteği yanıtlanmaz iken; user ve password bilgisi bir Authorization header ifadesiyle gönderildiğinde yalnızca GET isteklerinin yanıtlandığı ancak POST, PUT ve DELETE gibi işlemlerin ise yanıtlanmadığı (Forbidden) yani yasaklandığı görülür. + +![Basic Authentication](http://www.zafercomert.com/medya/java/springSecurity-BasicAuth.svg) + +# 2. UserDetailsService + +## 2.1. UserDetailsService + +**UserDetailService** üzerinden kullanıcı tanımları gerçekleştirebilir. Bu noktada kullanıcı bilgilerini tutmak üzere **InMemoryUserDetailsManager** kullanıyoruz. + +Ancak **UserDetailService** implemente eden daha farklı sınıflar da vardır. Detaylar için resmi dokümantasyon incelenebilir [Interface UserDetailsService](https://docs.spring.io/spring-security/site/docs/3.2.8.RELEASE/apidocs/org/springframework/security/core/userdetails/UserDetailsService.html). + +**UserDetailService** interface yapısını implemente eden sınıflar: + +- CachingUserDetailsService, +- InMemoryDaoImpl, +- _InMemoryUserDetailsManager_, +- JdbcDaoImpl, +- JdbcUserDetailsManager, +- LdapUserDetailsManager, +- LdapUserDetailsService, +- UserDetailsServiceWrapper + +```java +@Override +@Bean +protected UserDetailsService userDetailsService() { + + UserDetails admin = User.builder() + .username("admin") + .password(passwordEncoder.encode("admin123456")) + .roles("ADMIN") + .build(); + + UserDetails editor = User.builder() + .username("editor") + .password(passwordEncoder.encode("editor123456")) + .roles("EDITOR") + .build(); + + UserDetails user = User.builder() + .username("user") + .password(passwordEncoder.encode("user123456")) + .roles("USER") + .build(); + + return new InMemoryUserDetailsManager(admin, editor, user); +} +``` + +> UserDetailsService de bir konfigürasyon ifadesidir. Bu nedenle @Bean annotation yapısı mutlaka bu metodun üzerine eklenmelidir. + +Uygulamanın bu haliyle yukarıda verilen kullanıcı adı ve şifreler ile artık API test edilebilir durumdadur. + +## 2.2 PasswordEncoder + +```java +@Configuration +public class PasswordConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(10); + } +} +``` + +Injection unutulmamalıdır: + +```java +private final PasswordEncoder passwordEncoder; + + @Autowired + public ApplicationSecurityConfig(PasswordEncoder passwordEncoder) { + this.passwordEncoder = passwordEncoder; + } +``` + +## 2.3. configure Metodu Güncellenir. + +```java +@Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeRequests() + .antMatchers("/", "/index", "/css/*", "/js/**").permitAll() + .antMatchers("/api/**").hasAnyRole("ADMIN", "EDITOR") + .anyRequest() + .authenticated() + .and() + .httpBasic(); + } +``` + +## 3. Roles and Permissions + +## 3.1. Google Guava + +Öncelikle projemize Google Guava bağımlılığını ekleyeceğiz [Google Guava](https://github.com/google/guava). + +> Guava, yeni koleksiyon türleri (çoklu harita ve çoklu küme gibi), değişmez koleksiyonlar, bir grafik kitaplığı ve eşzamanlılık için yardımcı programları içeren Google'ın temel Java kitaplıkları kümesidir. +> pom.xml dosyasına aşağıdaki bağımlılık eklenir. + +```xml + + com.google.guava + guava + 28.1-jre + +``` + +## 3.2. ApplicationUserRole + +ApplicationUserRole içerisinde uygulamadaki rol tanımlarını gerçekleştirmeye çalışıyoruz. Bu kapsamda Admin, Editor ve User rollerinin tanımını enum formatında gerçekleştiriyoruz. + +```java +public enum ApplicationUserRole { + ADMIN(Sets.newHashSet(BOOK_READ, BOOK_WRITE, BOOK_PUT, BOOK_DELETE)), + EDITOR(Sets.newHashSet(BOOK_READ, BOOK_WRITE, BOOK_PUT)), + USER(Sets.newHashSet(BOOK_READ)); + + private final Set permissions; + + ApplicationUserRole(Set permissions) { + this.permissions = permissions; + } + + public Set getPermissions() { + return permissions; + } +} +``` + +> #### Bir role çok sayıda Permission tanımı içerebilir. + +## 3.3 ApplicationUserPermission + +**ApplicationUserPermission** da bir **enum** yapısıdır ve kaynaklara ait yazma ya da okuma gibi işlevlerin tanımlanmasını sağlar. Burada tanımlanan izin ifadeleri istenilen Rollere atanabilir. + +```java +public enum ApplicationUserPermission { + BOOK_GET("book:get"), + BOOK_DELETE("book:delete"), + BOOK_PUT("book:put"), + BOOK_POST("book:post"); + + private final String permission; + + ApplicationUserPermission(String permission) { + this.permission = permission; + } + + public String getPermission() { + return permission; + } +} +``` + +```java +import java.util.Set; + +import com.google.common.collect.Sets; +import static com.bookstore.api.security.ApplicationUserPermission.*; + +public enum ApplicationUserRole { + ADMIN(Sets.newHashSet(BOOK_GET, BOOK_POST, BOOK_PUT, BOOK_DELETE)), + EDITOR(Sets.newHashSet(BOOK_GET, BOOK_POST, BOOK_PUT)), + USER(Sets.newHashSet(BOOK_GET)); + + private final Set permissions; + + ApplicationUserRole(Set permissions) { + this.permissions = permissions; + } + + public Set getPermissions() { + return permissions; + } +} + +``` + +## 3.4. ApplicationSecurityConfig Güncellemesi + +Artık rol tanımları bir enum yapısı içerisinde tanımlandığından String ifadeleri konfigürasyon dosyamızdan çıkartabiliriz. + +```java + +import static com.bookstore.api.security.ApplicationUserRole.*; + + @Override +protected void configure(HttpSecurity http) throws Exception { + http + .authorizeRequests() + .antMatchers("/", "/index", "/css/*", "js/**").permitAll() + .antMatchers("/api/**").hasAnyRole(ADMIN.name(), EDITOR.name()) + .anyRequest() + .authenticated() + .and() + .httpBasic(); +} +``` + +# 4. CSRF Disable + +Burada öncelikle **POST**, **PUT** ya da **DELETE** gibi http isteklerinin gönderilebilmesi için ya **CSRF** (Cross-Side Request Forgery) ifadesinin kapatılması ya da **CRSF** için bir implementasyonun yapılması gerekir. + +**CSRF** ifadesini kapatarak devam edeceğiz. Bu konfigürasyonu gerçekleştirmek için de **ApplicationSecurityConfig** dosyası üzerinde çalışıyoruz. + +Ayrıca management API çağırısı yapabilmek için hasRole ifadesiyle **ADMIN** rolünü geçerli kılıyoruz. + +configure metodu içerisinde **categories** kaynağı için ADMIN yetkili kılıyoruz. + +```java + +@Override + protected void configure(HttpSecurity http) throws Exception { + http + .csrf().disable() + .authorizeRequests() + .antMatchers("/", "/index", "/css/*", "js/**").permitAll() + .antMatchers("/api/v1/categories").hasAnyRole(ADMIN.name()) + .antMatchers("/api/**").hasAnyRole(ADMIN.name(), EDITOR.name()) + .anyRequest() + .authenticated() + .and() + .httpBasic(); +} + +``` + +# 5. Permission based Authentication + +Bu bölümde permission tabanlı oturum açma işlemini gerçekleştiriyoruz. Hatırlayacağınız üzere bir role tanımı birden fazla permission içerebilirdi. Bir başka ifadeyle, çeşitli permission ifadelerinin birleşimi role tanımını oluşturmaktadır. + +## 5.1. getAuthorities() metodunun uygulanması + +**getAuthorities** metodu ApplicationUserRole sınıfı içerisinde tanımlanır. + +**getAuthorities()** aslında Role üzerinde tanımlı olan permission ifadelerinin getirilmesini sağlar. + +```java + +public Set getGrantedAuthorities() { + Set permissions = getPermissions().stream() + .map(permission -> new SimpleGrantedAuthority(permission.getPermission())) + .collect(Collectors.toSet()); + permissions.add(new SimpleGrantedAuthority("ROLE_" + this.name())); + return permissions; +} + +``` + +## 5.2 UserDetailsService Güncellemesi + +Bu durumda artık role tabanlı değil; permission tabanlı olarak kullanıcı bilgilerinin getirilmesi gerekir. Bu çerçevede **ApplicationUserRole** sınıfı içerisindeki **getGrantedAuthorities()** metodu kullanılır. + +```java +@Override +@Bean +protected UserDetailsService userDetailsService() { + UserDetails admin = User.builder() + .username("admin") + .password(passwordEncoder.encode("admin123456")) + // .roles(ADMIN.name()) + .authorities(ADMIN.getGrantedAuthorities()) + .build(); + + UserDetails editor = User.builder() + .username("editor") + .password(passwordEncoder.encode("editor123456")) + .authorities(EDITOR.getGrantedAuthorities()) + // .roles(EDITOR.name()) + .build(); + + UserDetails user = User.builder() + .username("user") + .password(passwordEncoder.encode("user123456")) + .authorities(USER.getGrantedAuthorities()) + // .roles(USER.name()) + .build(); + + return new InMemoryUserDetailsManager(admin, editor, user); +} +``` + +## 5.3. hasAuthority veya hasAnyAuthority + +Path tanımlarına bağlı olarak herhangi bir permission ifadesi için **hasAuthority** ya da **hasAnyAuthority** metotlarıyla izin tanımları gerçekleştirilir. + +```java + +import static org.btk.bookstore.security.ApplicationUserPermission.*; + +@Override + protected void configure(HttpSecurity http) throws Exception { + http + .csrf().disable() + .authorizeRequests() + .antMatchers("/", "/index", "/css/*", "js/**").permitAll() + .antMatchers(HttpMethod.DELETE, "/api/v1/**").hasAuthority(BOOK_DELETE.getPermission()) + .antMatchers(HttpMethod.PUT, "/api/v1/**").hasAuthority(BOOK_PUT.getPermission()) + .antMatchers(HttpMethod.POST, "/api/v1/**").hasAuthority(BOOK_POST.getPermission()) + .antMatchers(HttpMethod.GET, "/api/v1/**").hasAuthority(BOOK_GET.getPermission()) + .antMatchers("/api/**").hasAnyRole(ADMIN.name(), EDITOR.name()) + .anyRequest() + .authenticated() + .and() + .httpBasic(); + } + +``` + +Artık permission/authority bazlı yetkilendirme işlemi gerçekleştirilebilir durumdadır. + +Bu durumda: - Admin, GET, POST, PUT, DELETE işlemlerini yapabilir. - Editör; GET, POST, PUT yapabilir. - User; sadece GET yapabilir. + +# 6. PreAuthorize + +# 6.1. @PreAuthorize + +Metotların öncesinde kullanılan bir annotation yapısı ile herbir metoda ait **Role** ya da **Permission** tanımı gerçekleştirmek mümkündür. Bu çerçevede: + +- hasRole +- hasAuthority +- hasAnyRole +- hasAnyAuthority + +ifadeleri kullanılabilir. + +**@PreAuthorize** ifadesi metotların başına eklenir. + +```java +@RestController +@RequestMapping("/management/api/books") +public class BooksManagerController { + + @GetMapping + @PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_EDITOR','ROLE_USER')") + public List getAllBooks() { + + } + + @PostMapping + @PreAuthorize("hasAuthority('book:write')") + public ResponseEntity addOneBook(@RequestBody Book book) { + + } + + @PutMapping(path = "{id}") + @PreAuthorize("hasAuthority('book:put')") + public ResponseEntity addOneBook(@PathVariable(name = "id") int id, @RequestBody Book book) { + + } + + @DeleteMapping(path = "{id}") + @PreAuthorize("hasAuthority('book:delete')") + public ResponseEntity addOneBook(@PathVariable(name = "id") int id) { + + } +} +``` + +## 6.2. EnableGlobalMethodSecurity + +**@PreAuthorize ** annotation yapısının kullanılabilmesi için **EnableGlobalMethodSecurity** özelliğinin **ApplicationSecurityConfig** ifadesinin hemen üzerinde tanımlanması gerekir. + +```java +Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class ApplicationSecurityConfig extends WebSecurityConfigurerAdapter { + // ... +} +``` + +## 6.3. antMatchers + +Son adımda artık antMatcher ifadelerinin kullanımına gerek kalmamaktadır. İlgili ifadeler artık kaldırılabilir. Biz uygulama çerçevesinde sadece **BooksManagerController** içine **@PreAuthorize** kullandığımızdan dolayı sadece **/management/api/books** ait olan tanımları yorum satırına çeviriyoruz. + +```java +@Override + protected void configure(HttpSecurity http) throws Exception { + http + .csrf().disable() + .authorizeRequests() + .antMatchers("/", "/index", "/css/*", "js/**").permitAll() + .antMatchers("/api/**").permitAll() + .anyRequest() + .authenticated() + .and() + .httpBasic(); + } +``` + +# 7. UserDetailService Implementation + +# 7 UserDetailService Implementation + +## 7.1 ApplicationUser (-> UserDetails) + +Öncelikle **ApplicationSecurityConfig** bölümünde bulunan **userDetailsService()** metodunun işlevini bir veri tabanı ile bir başka ifadeyle bir Repository ile çalışacak şekilde düzenliyoruz. + +Bu çerçevede, öncelikle **UserDetails** interface yapısını implemente eden **ApplicationUser** isimli bir sınıf tanımı gerçekleştiriyoruz. Böylelikle **Spring** **Security** sınıfı içerisinde kullanabileceğimiz bir **UserDetails** sınıfı elde ediyoruz. + +```java + +public class ApplicationUser implements UserDetails { + + private final String username; + private final String password; + private final Set grantedAuthorities; + private final boolean isAccountNonExpired; + private final boolean isAccountNonLocked; + private final boolean isCredentialsNonExpired; + private final boolean isEnabled; + + public ApplicationUser(String username, + String password, + Set grantedAuthorities, + boolean isAccountNonExpired, + boolean isAccountNonLocked, + boolean isCredentialsNonExpired, + boolean isEnabled) { + this.username = username; + this.password = password; + this.grantedAuthorities = grantedAuthorities; + this.isAccountNonExpired = isAccountNonExpired; + this.isAccountNonLocked = isAccountNonLocked; + this.isCredentialsNonExpired = isCredentialsNonExpired; + this.isEnabled = isEnabled; + } + + @Override + public Collection getAuthorities() { + return grantedAuthorities; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getUsername() { + return username; + } + + @Override + public boolean isAccountNonExpired() { + return isAccountNonExpired; + } + + @Override + public boolean isAccountNonLocked() { + return isAccountNonLocked; + } + + @Override + public boolean isCredentialsNonExpired() { + return isCredentialsNonExpired; + } + + @Override + public boolean isEnabled() { + return isEnabled; + } +} + +``` + +## 7.2 ApplicationUserService (-> UserDetailsService) + +**UserDetails** sınıfını kullancak olan bir servis tanımı gerçekleştiriyoruz. Bu servis **UserDetailsService** interface yapısını implemente etmektedir ve sadece **loadByUsername()** metodunun implemente edilmesini gerektirmektedir. + +> Servis katmanı üzerinde bu işlemi gerçekleştiriyoruz. + +```java +@Override +public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + return applicationUserDao + .selectApplicationUserByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException(String.format("Username %s not found", username))); +} +``` + +> Burada önemli olan bir nokta Repository tanımının bir interface aracılığıyla bu sınıf içerisinde kullanılmasıdır. Herhangi bir Repository ile çalışmak için **ApplicationUserDao** isimli bir interface yapısı bu çerçevde tanımlanır. + +## 7.3. ApplicationUserDao + +**ApplicationUserService** içerisinde tanımlanan tek metot olan **loadByUsername** metodu içerisinde kullanılmak üzere; kullanıcıyı seçmek amacıyla bir metot imzası içerir. + +```java +public interface ApplicationUserDao { + Optional selectApplicationUserByUsername(String username); +} +``` + +## 7.4. FakeApplicationUserDaoService + +**ApplicationUserDao** interface yapısını uygulayan sınıftır. Bu sınıf içerisinde bir servisten ya da bir Repo üzerinden kullanıcılar alınabilir. + +Mevcut uygulamada bir private metot içerisinde kullanıcıların manuel olarak oluşturulması sağlanmıştır. + +```java +package com.bookstore.api.services; + +import com.bookstore.api.security.ApplicationUser; +import com.bookstore.api.services.Abstract.ApplicationUserDao; +import com.google.common.collect.Lists; + +import lombok.RequiredArgsConstructor; + +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +import static com.bookstore.api.security.ApplicationUserRole.*; + +@Repository("fake") +@RequiredArgsConstructor +public class FakeApplicationUserDaoService implements ApplicationUserDao { + + private final PasswordEncoder passwordEncoder; + + public Optional selectApplicationUserByUsername(String username) { + return getApplicationUsers() + .stream() + .filter(applicationUser -> username.equals(applicationUser.getUsername())) + .findFirst(); + } + + private List getApplicationUsers() { + List applicationUsers = Lists.newArrayList( + new ApplicationUser( + "admin", + passwordEncoder.encode("admin123456"), + ADMIN.getGrantedAuthorities(), + true, + true, + true, + true), + new ApplicationUser( + "editor", + passwordEncoder.encode("editor123456"), + EDITOR.getGrantedAuthorities(), + true, + true, + true, + true), + new ApplicationUser( + "user", + passwordEncoder.encode("user123456"), + USER.getGrantedAuthorities(), + true, + true, + true, + true)); + return applicationUsers; + } +} +``` + +> Dikkate edilirse tanımlanan somut Repository("Fake") olarak isimlendirilmiştir. **ApplicationUserService** gidildiğinde bu alanın **@Qualifier("fake")** ile özellikle vurgulandığı görülmektedir. Yani birden fazla Repository olması durumunda özellikle kullanılacak olan Repository kesin bir şekilde ifade edilmiştir. + +```java + private final ApplicationUserDao applicationUserDao; + + @Autowired + public ApplicationUserService(@Qualifier("fake") ApplicationUserDao applicationUserDao) { + this.applicationUserDao = applicationUserDao; + } +``` + +## 7.5.1 UserService + +**ApplicationUserDao** üzerinden import edilen bu arayüz **UserServiceImp** için bir kontrat/interface/arayüz olarak kullanılır. + +```java +package com.bookstore.api.services.Abstract; + +import java.util.List; + +import com.bookstore.api.entities.User; +import com.bookstore.api.entities.dto.UserDto; +import com.bookstore.api.entities.models.ApiResponse; + +public interface UserService extends ApplicationUserDao { + ApiResponse> getAllUsers(); + + ApiResponse getOneUser(int id); + + ApiResponse postOneUser(User user); + + ApiResponse putOneUser(int userId, User user); + + User getOneUserByUserName(String userName); + + void deleteOneUser(int userId); + + User saveOneUser(User user); + +} + +``` + +## 7.5.2 UserServiceImp + +**services** klasörü altına eklenir. +**UserService** arayüzünü implemente eder. + +```java +@Service +@RequiredArgsConstructor +@Repository("mysql") +public class UserServiceImp implements UserService { + +} +``` + +En başta UserService kalıtıldığı **ApplicationUserDao** sınıfında tanımlanan metot implemente edilir. + +```java +@Override + public Optional selectApplicationUserByUsername(String username) { + + User user = userRepository.findByUserName(username); + + Set grantedAuthorities = new HashSet<>(); + Set roles = user.getRoles(); + + for (Role role : roles) { + switch (role.getId()) { + case 1: + grantedAuthorities.addAll(ADMIN.getGrantedAuthorities()); + break; + case 2: + grantedAuthorities.addAll(EDITOR.getGrantedAuthorities()); + break; + case 3: + grantedAuthorities.addAll(USER.getGrantedAuthorities()); + break; + default: + break; + } + } + + Optional applicationUser = Optional.ofNullable(new ApplicationUser( + user.getUserName(), + user.getPassword(), + grantedAuthorities, + true, + true, + true, + true)); + return applicationUser; + } +``` + +Sonra diğer metotlar yazılabilir. + +```java +package com.bookstore.api.services; + +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import com.bookstore.api.entities.Role; +import com.bookstore.api.entities.User; +import com.bookstore.api.entities.dto.UserDto; +import com.bookstore.api.entities.models.ApiResponse; +import com.bookstore.api.exceptions.notFoundExceptions.UserNotFoundException; +import com.bookstore.api.repositories.RoleRepository; +import com.bookstore.api.repositories.UserRepository; +import com.bookstore.api.security.ApplicationUser; +import com.bookstore.api.services.Abstract.UserService; + +import lombok.RequiredArgsConstructor; + +import org.modelmapper.ModelMapper; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Repository; +import org.springframework.stereotype.Service; + +import static com.bookstore.api.security.ApplicationUserRole.*; + +@Service +@RequiredArgsConstructor +@Repository("mysql") +public class UserServiceImp implements UserService { + + private final UserRepository userRepository; + private final RoleRepository roleRepository; + private final PasswordEncoder passwordEncoder; + private final ModelMapper mapper; + + @Override + public ApiResponse> getAllUsers() { + List users = userRepository.findAll(); + + List list = users + .stream() + .map(user -> mapper.map(user, UserDto.class)) + .collect(Collectors.toList()); + + return ApiResponse.default_OK(list); + } + + @Override + public ApiResponse getOneUser(int userId) { + User user = userRepository + .findById(userId) + .orElseThrow(() -> new UserNotFoundException(userId)); + + UserDto userDto = mapper.map(user, UserDto.class); + + return ApiResponse.default_OK(userDto); + } + + @Override + public ApiResponse postOneUser(User user) { + Set roles = new HashSet<>(); + Role role = roleRepository.findByName("USER"); + if (role == null) { + throw new RuntimeException("USER role is not defined."); + } + roles.add(role); + user.setRoles(roles); + user.setPassword(passwordEncoder.encode(user.getPassword())); + userRepository.save(user); + return ApiResponse.default_CREATED(mapper.map(user, UserDto.class)); + } + + @Override + public ApiResponse putOneUser(int userId, User user) { + getOneUser(userId); + + Set roles = roleRepository.findByIdIn(user.getRoles()); + user.setId(userId); + user.setRoles(roles); + + userRepository.save(user); + return ApiResponse.default_ACCEPTED(mapper.map(user, UserDto.class)); + } + + @Override + public void deleteOneUser(int userId) { + userRepository.deleteById(userId); + } + + @Override + public User getOneUserByUserName(String userName) { + return userRepository.findByUserName(userName); + } + + @Override + public User saveOneUser(User newUser) { + newUser.setPassword(passwordEncoder.encode(newUser.getPassword())); + return userRepository.save(newUser); + } + + @Override + public Optional selectApplicationUserByUsername(String username) { + + User user = userRepository.findByUserName(username); + + Set grantedAuthorities = new HashSet<>(); + Set roles = user.getRoles(); + + for (Role role : roles) { + switch (role.getId()) { + case 1: + grantedAuthorities.addAll(ADMIN.getGrantedAuthorities()); + break; + case 2: + grantedAuthorities.addAll(EDITOR.getGrantedAuthorities()); + break; + case 3: + grantedAuthorities.addAll(USER.getGrantedAuthorities()); + break; + default: + break; + } + } + + Optional applicationUser = Optional.ofNullable(new ApplicationUser( + user.getUserName(), + user.getPassword(), + grantedAuthorities, + true, + true, + true, + true)); + return applicationUser; + } +} + +``` + +## 7.6 Application Security Config Güncelleme + +ApplicationSecurityConfig içerinde **AuthenticationManagerBuilder** kullanılarak oturum açma için bir veri servisine bağlanılacağı bildirilir. + +```java + +@Override +protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.authenticationProvider(daoAuthenticationProvider()); +} + +@Bean +public DaoAuthenticationProvider daoAuthenticationProvider() { + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setPasswordEncoder(passwordEncoder); + provider.setUserDetailsService(applicationUserService); + return provider; +} +``` + +> #### ApplicationUserService içerisinde birden fazla kaynak var ise @Qualifier("fake") annotation yapısı ile hangi dao implementasyonunun kullanılacağı belirlenir. + +# 8 UserDetails için Dao Implementasyonu + +Bu adımda MySQL üzerinde kullanıcıları ve rolleri organize ediyoruz. + +## 8.1. Entities, Repositories, Services ve Controllers Tanımları + +- **User** **ve** **Role** sınıflarını projemize ekliyoruz. Bu sınıfları entities paketi altında topluyoruz. +- **User** ve **Role** için **UserRepostiroy** ve **RoleRepository** interface yapılarını **JpaRepository** kullanarak tanımlıyoruz. +- **UserService** tanımını gerçekleştiriyoruz ve bunun **services** klasörü altında yapıyoruz. +- **UsersController** tanımı yapıp içerisinde ilgili metot tanımlarını **controllers** paketi altında gerçekleştiriyoruz. + +## 8.3. ApplicationUserDao Implementasyonu + +Uygulamamızın artık kullanıcı verilerini **MySQL** veritabanınından almasını istiyoruz. Bu amaçla öncelikle daha önce implemente ettiğimiz **UserService** sınıfına gidip **ApplicationUserDao** interface yapısını burada implemente edeceğimizi ifade ediyoruz. + +Aynı zamanda sistemde bulunan **Fake** repository tanımının geçerisiz olmasını sağlamak amacıyla **@Repository("mysql")** ifadesini burada kullanıyoruz. \*\*\*\* + +```java + +import static com.bookstore.api.security.ApplicationUserRole.*; + +@Service +@RequiredArgsConstructor +@Repository("mysql") +public class UserServiceImp implements ApplicationUserDao, UserService { + + private final UserRepository userRepository; + private final RoleRepository roleRepository; + private final PasswordEncoder passwordEncoder; + private final ModelMapper mapper; + + @Override + public ApiResponse> getAllUsers() { + List users = userRepository.findAll(); + + List list = users + .stream() + .map(user -> mapper.map(user, UserDto.class)) + .collect(Collectors.toList()); + + return ApiResponse.default_OK(list); + } + + @Override + public ApiResponse getOneUser(int userId) { + User user = userRepository + .findById(userId) + .orElseThrow(() -> new UserNotFoundException(userId)); + + UserDto userDto = mapper.map(user, UserDto.class); + + return ApiResponse.default_OK(userDto); + } + + @Override + public ApiResponse postOneUser(User user) { + Set roles = new HashSet<>(); + Role role = roleRepository.findByName("USER"); + if (role == null) { + throw new RuntimeException("USER role is not defined."); + } + roles.add(role); + user.setRoles(roles); + user.setPassword(passwordEncoder.encode(user.getPassword())); + userRepository.save(user); + return ApiResponse.default_CREATED(mapper.map(user, UserDto.class)); + } + + @Override + public ApiResponse putOneUser(int userId, User user) { + getOneUser(userId); + + Set roles = roleRepository.findByIdIn(user.getRoles()); + user.setId(userId); + user.setRoles(roles); + + userRepository.save(user); + return ApiResponse.default_ACCEPTED(mapper.map(user, UserDto.class)); + } + + public void deleteOneUser(int userId) { + userRepository.deleteById(userId); + } + + public User getOneUserByUserName(String userName) { + return userRepository.findByUserName(userName); + } + + @Override + public Optional selectApplicationUserByUsername(String username) { + + User user = userRepository.findByUserName(username); + + Set grantedAuthorities = null; + Set roles = user.getRoles(); + + for (Role role : roles) { + switch (role.getId()) { + case 1: + grantedAuthorities.addAll(ADMIN.getGrantedAuthorities()); + break; + case 2: + grantedAuthorities.addAll(EDITOR.getGrantedAuthorities()); + break; + case 3: + grantedAuthorities.addAll(USER.getGrantedAuthorities()); + break; + default: + break; + } + + } + + Optional applicationUser = Optional.ofNullable(new ApplicationUser( + user.getUserName(), + user.getPassword(), + grantedAuthorities, + true, + true, + true, + true)); + + return applicationUser; + } + +} + + + + +``` + +Bu interface yapısını kabul ettiğimizde **selectApplicationUserByUsername** metodunu aslında garanti etmiş oluyoruz. Bu noktada artık ilgili metodun implemente edilmesi gerekir. Metot gövdesi aşağıdaki gibi implemente edilir. + +```java +@Override +public Optional selectApplicationUserByUsername(String username) { + + User user = userRepository.findByUserName(username); + + Set grantedAuthorities = new HashSet<>();; + Set roles = user.getRoles(); + + for (Role role : roles) { + switch (role.getId()) { + case 1: + grantedAuthorities.addAll(ADMIN.getGrantedAuthorities()); + break; + case 2: + grantedAuthorities.addAll(EDITOR.getGrantedAuthorities()); + break; + case 3: + grantedAuthorities.addAll(USER.getGrantedAuthorities()); + break; + default: + break; + } + + } + + Optional applicationUser = Optional.ofNullable(new ApplicationUser( + user.getUserName(), + user.getPassword(), + grantedAuthorities, + true, + true, + true, + true)); + + return applicationUser; +} +``` + +![BasicAuthWithDao](http://www.zafercomert.com/medya/java/springSecurity-BasicAuthWithDao.svg) + +# 9 JWT + +[JWT IO](https://jwt.io/) + +JWT Header, Payload ve VERIFY SIGNATURE bölümlerinden oluşur. + +## 9.1. [Java JWT: JSON Web Token for Java and Android](https://github.com/jwtk/jjwt) + +Öncelikle bağımlılıkların sisteme eklenmesi gerekir. + +```xml + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime + +``` + +## 9.2. Değişken Tanımları (application.properties) + +application.jwt özellikle prefix olarak kullanılmıştır. + +``` +application.jwt.secretKey=springsecurityspringsecurityspringsecurityspringsecurityspringsecurityspringsecurityxxyy12345698 +application.jwt.expires.in=3600000 +application.jwt.tokenPrefix=Bearer +application.jwt.tokenExpirationAfterDays=10 +application.jwt.refresh.token.expires.in=604800 +``` + +## 9.3. JwtConfig + +jwt klasörünün altında eklenir. +application.properties dosyasındaki değişkenler ile ilişki kurar. +Bir konfigürasyon dosyasıdır. + +```java +package com.bookstore.api.jwt; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; + +import lombok.Data; + +@Configuration +@ConfigurationProperties(prefix = "application.jwt") +@Data +public class JwtConfig { + + private String secretKey; + private String tokenPrefix; + + @Value("${application.jwt.expires.in}") + private Long expiresIn; + + public String getAuthorizationHeader() { + return HttpHeaders.AUTHORIZATION; + } +} +``` +## 9.4. JwtSecretKey + +jwt klasörünün altında eklenir. +JwtConfig buraya enjekte edilir. +Bir konfigürasyon dosyasıdır. + +```java +package com.bookstore.api.jwt; + +import javax.crypto.SecretKey; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; + +@Configuration +public class JwtSecretKey { + + private final JwtConfig jwtConfig; + + public JwtSecretKey(JwtConfig jwtConfig) { + this.jwtConfig = jwtConfig; + } + + @Bean + public SecretKey secretKey() { + return Keys.hmacShaKeyFor(jwtConfig.getSecretKey().getBytes()); + } +} +``` + +## 9.5 JwtAuthenticationEntryPoint +Geçersiz istekleri karşılamak üzere kullanılır. + +```java +package com.bookstore.api.jwt; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +@Component +public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) throws IOException, ServletException { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage()); + } + +} +``` + +## 9.6. JwtTokenProvider +Anahtar üretmek üzere kullanılır. +**JwtSecretKey** ve **JwtConfig** ile birlikte çalışır. + +```java +package com.bookstore.api.jwt; + +import java.security.Key; +import java.util.Date; + +import javax.crypto.SecretKey; + +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +import com.bookstore.api.security.ApplicationUser; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.SignatureException; +import io.jsonwebtoken.UnsupportedJwtException; + +@Component +public class JwtTokenProvider { + + private final JwtConfig jwtConfig; + private final SecretKey secretKey; + + public JwtTokenProvider(JwtConfig jwtConfig, SecretKey secretKey) { + this.jwtConfig = jwtConfig; + this.secretKey = secretKey; + System.out.println(jwtConfig.getExpiresIn()); + } + + public String generateJwtToken(Authentication auth) { + + ApplicationUser userDetails = (ApplicationUser) auth.getPrincipal(); + + return Jwts.builder() + .setSubject(userDetails.getUsername()) + .claim("authorities", userDetails.getAuthorities()) + .setIssuedAt(new Date()) + .setExpiration(new Date(new Date().getTime() + jwtConfig.getExpiresIn())) + .signWith(secretKey) + .compact(); + } + + public String generateJwtTokenByUserId(int userId) { + + Date expireDate = new Date(new Date().getTime() + jwtConfig.getExpiresIn()); + return Jwts.builder() + .setSubject(Integer.toString(userId)) + .setIssuedAt(new Date()) + .setExpiration(expireDate) + .signWith(secretKey) + .compact(); + } + + public String generateJwtTokenByUserName(String username) { + Date expireDate = new Date(new Date().getTime() + jwtConfig.getExpiresIn()); + return Jwts.builder() + .setSubject(username) + .setIssuedAt(new Date()) + .setExpiration(expireDate) + .signWith(secretKey) + .compact(); + } + + String getUsernameFromJwt(String token) { + Claims claims = getJwtBody(token); + return claims.getSubject(); + } + + boolean validateToken(String token) { + try { + getJwtBody(token); + return !isTokenExpired(token); + } catch (MalformedJwtException e) { + return false; + } catch (ExpiredJwtException e) { + return false; + } catch (UnsupportedJwtException e) { + return false; + } catch (IllegalArgumentException e) { + return false; + } + } + + boolean isTokenExpired(String token) { + Date expiration = getJwtBody(token).getExpiration(); + return expiration.before(new Date()); + } + + private Claims getJwtBody(String token) { + + return Jwts.parser() + .setSigningKey(secretKey) + .parseClaimsJws(token) + .getBody(); + } +} + +``` +## 9.7. JwtAuthenticationFilter + +Http isteklerinde araya girerek kullanılan bir filter görevi görür. + +- **JwtTokenProvider** +- **userDetailsService** + +bu sınıfa enjekte edilir. + +```java +package com.bookstore.api.jwt; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import com.bookstore.api.services.ApplicationUserService; + +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + @Autowired + JwtTokenProvider jwtTokenProvider; + + @Autowired + ApplicationUserService userDetailsService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + try { + String jwtToken = extractJwtFromRequest(request); + if (StringUtils.hasText(jwtToken) && jwtTokenProvider.validateToken(jwtToken)) { + String username = jwtTokenProvider.getUsernameFromJwt(jwtToken); + UserDetails user = userDetailsService.loadUserByUsername(username); + if (user != null) { + UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(user, null, + user.getAuthorities()); + + auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(auth); + } + } + } catch (Exception e) { + return; + } + filterChain.doFilter(request, response); + } + + private String extractJwtFromRequest(HttpServletRequest request) { + String bearer = request.getHeader("Authorization"); + if (StringUtils.hasText(bearer) && bearer.startsWith("Bearer ")) + return bearer.substring("Bearer".length() + 1); + return null; + } +} + +``` + +## 9.8. ApplicationSecurityConfig Güncellemesi + +Bazı önemli import ifadeleri + +>import org.springframework.security.config.http.SessionCreationPolicy; +>import org.springframework.security.config.BeanIds; +>import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +>import org.springframework.security.authentication.AuthenticationManager; + +```java + +package com.bookstore.api.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +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.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.config.BeanIds; + +import com.bookstore.api.jwt.JwtAuthenticationEntryPoint; +import com.bookstore.api.jwt.JwtAuthenticationFilter; +import com.bookstore.api.services.ApplicationUserService; + +import static com.bookstore.api.security.ApplicationUserRole.*; +import static com.bookstore.api.security.ApplicationUserPermission.*; + +import lombok.RequiredArgsConstructor; + +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +@RequiredArgsConstructor +public class ApplicationSecurityConfig extends WebSecurityConfigurerAdapter { + + private final PasswordEncoder passwordEncoder; + private final ApplicationUserService applicationUserService; + private final JwtAuthenticationEntryPoint handler; + + @Bean + public JwtAuthenticationFilter jwtAuthenticationFilter() { + return new JwtAuthenticationFilter(); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .csrf().disable() + .exceptionHandling().authenticationEntryPoint(handler) + .and() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeRequests() + .antMatchers("/", "index", "/css/*", "/js/*").permitAll() + .antMatchers("/api/v1/auth/**").permitAll() + .antMatchers("/api/**").permitAll() + .anyRequest() + .authenticated(); + } + + @Bean(BeanIds.AUTHENTICATION_MANAGER) + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.authenticationProvider(daoAuthenticationProvider()); + } + + @Bean + public DaoAuthenticationProvider daoAuthenticationProvider() { + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setPasswordEncoder(passwordEncoder); + provider.setUserDetailsService(applicationUserService); + return provider; + } + +} +``` + +## 9.9. UserRole + +### Entity +```java +package com.bookstore.api.entities; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.Table; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "users_roles") +@Data +@NoArgsConstructor +public class UserRole { + + @Id + @GeneratedValue + @Column(name = "id") + private int id; + + @Column(name = "user_id") + private int userId; + + @Column(name = "role_id") + private int roleId; + + public UserRole(int userId, int roleId) { + this.userId = userId; + this.roleId = roleId; + } + +} + +``` + +## UserRoleRepository + +```java +package com.bookstore.api.repositories; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.bookstore.api.entities.UserRole; + +public interface UserRoleRepository extends JpaRepository { +} + +``` + +## UserRoleService + +```java +package com.bookstore.api.services; + +import org.springframework.stereotype.Service; + +import com.bookstore.api.entities.UserRole; +import com.bookstore.api.repositories.UserRoleRepository; + +@Service +public class UserRoleService { + private final UserRoleRepository userRoleRepository; + + public UserRoleService(UserRoleRepository userRoleRepository) { + this.userRoleRepository = userRoleRepository; + } + + public void Add(int userId, int roleid) { + UserRole userRole = new UserRole(userId, roleid); + userRoleRepository.save(userRole); + } + +} +``` + +> # Varsa UserService ve UserServiceImp'deki eksiklikler giderilir. + +## 9.9 RefreshToken +Entities eklenir. + +```java +package com.bookstore.api.entities; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +import lombok.Data; + +@Entity +@Table(name = "refresh_token") +@Data +public class RefreshToken { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + int id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) + @JsonIgnore + User user; + + @Column(nullable = false, unique = true) + String token; + + @Column(nullable = false) + @Temporal(TemporalType.TIMESTAMP) + Date expiryDate; +} + +``` +## 9.10. RefreshTokenRepository + +```java +package com.bookstore.api.repositories; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.bookstore.api.entities.RefreshToken; + +public interface RefreshTokenRepository extends JpaRepository { + RefreshToken findByUserId(int userId); +} + +``` + +## 9.11. RefreshTokenService + +```java +package com.bookstore.api.services; + +import java.time.Instant; +import java.util.Date; +import java.util.UUID; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import com.bookstore.api.entities.RefreshToken; +import com.bookstore.api.entities.User; +import com.bookstore.api.repositories.RefreshTokenRepository; + +@Service +public class RefreshTokenService { + + @Value("${application.jwt.refresh.token.expires.in}") + Long expireSeconds; + + private RefreshTokenRepository refreshTokenRepository; + + public RefreshTokenService(RefreshTokenRepository refreshTokenRepository) { + this.refreshTokenRepository = refreshTokenRepository; + } + + public String createRefreshToken(User user) { + RefreshToken token = refreshTokenRepository.findByUserId(user.getId()); + if (token == null) { + token = new RefreshToken(); + token.setUser(user); + } + token.setToken(UUID.randomUUID().toString()); + token.setExpiryDate(Date.from(Instant.now().plusSeconds(expireSeconds))); + refreshTokenRepository.save(token); + return token.getToken(); + } + + public boolean isRefreshExpired(RefreshToken token) { + return token.getExpiryDate().before(new Date()); + } + + public RefreshToken getByUser(int userId) { + return refreshTokenRepository.findByUserId(userId); + } + +} +``` + +## 9.12. RefreshTokenService + +```java +package com.bookstore.api.services; + +import java.time.Instant; +import java.util.Date; +import java.util.UUID; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import com.bookstore.api.entities.RefreshToken; +import com.bookstore.api.entities.User; +import com.bookstore.api.repositories.RefreshTokenRepository; + +@Service +public class RefreshTokenService { + + @Value("${application.jwt.refresh.token.expires.in}") + Long expireSeconds; + + private RefreshTokenRepository refreshTokenRepository; + + public RefreshTokenService(RefreshTokenRepository refreshTokenRepository) { + this.refreshTokenRepository = refreshTokenRepository; + } + + public String createRefreshToken(User user) { + RefreshToken token = refreshTokenRepository.findByUserId(user.getId()); + if (token == null) { + token = new RefreshToken(); + token.setUser(user); + } + token.setToken(UUID.randomUUID().toString()); + token.setExpiryDate(Date.from(Instant.now().plusSeconds(expireSeconds))); + refreshTokenRepository.save(token); + return token.getToken(); + } + + public boolean isRefreshExpired(RefreshToken token) { + return token.getExpiryDate().before(new Date()); + } + + public RefreshToken getByUser(int userId) { + return refreshTokenRepository.findByUserId(userId); + } +} + +``` + +## 9.12 AuthResponse + +```java +package com.bookstore.api.entities.dto; + +import lombok.Data; + +@Data +public class AuthResponse { + private String message; + + private int userId; + private String userName; + + private String firstName; + private String lastName; + + private String accessToken; + private String refreshToken; +} + +``` + +## 9.13. RefreshRequest + +```java + +package com.bookstore.api.entities.dto; + +import lombok.Data; + +@Data +public class RefreshRequest { + private int userId; + private String refreshToken; +} + +``` +## 9.14. UserDto + +```java +package com.bookstore.api.entities.dto; + +import java.util.Set; + +import com.bookstore.api.entities.Role; + +import lombok.Data; + +@Data +public class UserDto { + private int id; + private String userName; + private String firstName; + private String lastName; + private Set roles; +} + + +``` + +## 9.15. UserRequest + +```java +package com.bookstore.api.entities.dto; + +import lombok.Data; + +@Data +public class UserRequest { + private String firstName; + private String lastName; + private String userName; + private String password; +} +``` +### UserRequestForRegister + +```java +package com.bookstore.api.entities.dto; + +import lombok.Data; + +@Data +public class UserRequestForRegister { + private String firstName; + private String lastName; + private String userName; + private String password; +} +``` + +## 9.16 AuthController + +```java +package com.bookstore.api.controllers; + +import java.util.List; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.config.annotation.rsocket.RSocketSecurity.AuthorizePayloadsSpec; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.bookstore.api.entities.RefreshToken; +import com.bookstore.api.entities.User; +import com.bookstore.api.entities.dto.AuthResponse; +import com.bookstore.api.entities.dto.RefreshRequest; +import com.bookstore.api.entities.dto.UserRequest; +import com.bookstore.api.entities.dto.UserRequestForRegister; +import com.bookstore.api.jwt.JwtTokenProvider; +import com.bookstore.api.services.RefreshTokenService; +import com.bookstore.api.services.UserRoleService; +import com.bookstore.api.services.Abstract.UserService; + +@RestController +@RequestMapping("/api/v1/auth") +@CrossOrigin(origins = { "http://localhost:3000/", "http://localhost:3001" }) +public class AuthController { + + private AuthenticationManager authenticationManager; + private JwtTokenProvider jwtTokenProvider; + private UserService userService; + private PasswordEncoder passwordEncoder; + private RefreshTokenService refreshTokenService; + private UserRoleService userRoleService; + + public AuthController(AuthenticationManager authenticationManager, JwtTokenProvider jwtTokenProvider, + UserService userService, PasswordEncoder passwordEncoder, RefreshTokenService refreshTokenService, + UserRoleService userRoleService) { + this.authenticationManager = authenticationManager; + this.jwtTokenProvider = jwtTokenProvider; + this.userService = userService; + this.passwordEncoder = passwordEncoder; + this.refreshTokenService = refreshTokenService; + this.userRoleService = userRoleService; + } + + @PostMapping("/login") + public AuthResponse login(@RequestBody UserRequest loginRequest) { + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + loginRequest.getUserName(), + loginRequest.getPassword()); + + Authentication auth = authenticationManager.authenticate(authToken); + + SecurityContextHolder.getContext().setAuthentication(auth); + + String jwtToken = jwtTokenProvider.generateJwtToken(auth); + + User user = userService.getOneUserByUserName(loginRequest.getUserName()); + + AuthResponse authResponse = new AuthResponse(); + authResponse.setAccessToken("Bearer " + jwtToken); + authResponse.setRefreshToken(refreshTokenService.createRefreshToken(user)); + authResponse.setUserId(user.getId()); + authResponse.setMessage("Successed."); + authResponse.setFirstName(user.getFirstName()); + authResponse.setLastName(user.getLastName()); + + return authResponse; + } + + @PostMapping("/register") + public ResponseEntity register(@RequestBody UserRequestForRegister registerRequest) { + + AuthResponse authResponse = new AuthResponse(); + + // User exists? + if (userService.getOneUserByUserName(registerRequest.getUserName()) != null) { + authResponse.setMessage("Username already in use."); + return new ResponseEntity<>(authResponse, HttpStatus.BAD_REQUEST); + } + + // User creating... + User user = new User(); + user.setFirstName(registerRequest.getFirstName()); + user.setLastName(registerRequest.getLastName()); + user.setUserName(registerRequest.getUserName()); + user.setPassword(registerRequest.getPassword()); + + userService.saveOneUser(user); + + // Adding role -> User role is given by default + userRoleService.Add(user.getId(), 3); + + // + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + registerRequest.getUserName(), + registerRequest.getPassword()); + + Authentication auth = authenticationManager.authenticate(authToken); + SecurityContextHolder.getContext().setAuthentication(auth); + String jwtToken = jwtTokenProvider.generateJwtToken(auth); + + authResponse.setMessage("User successfully registered."); + authResponse.setAccessToken("Bearer " + jwtToken); + authResponse.setRefreshToken(refreshTokenService.createRefreshToken(user)); + authResponse.setUserId(user.getId()); + authResponse.setUserName(user.getUserName()); + authResponse.setFirstName(user.getFirstName()); + authResponse.setLastName(user.getLastName()); + + return new ResponseEntity<>(authResponse, HttpStatus.OK); + } + + @PostMapping("/refresh") + public ResponseEntity refresh(@RequestBody RefreshRequest refreshRequest) { + AuthResponse authResponse = new AuthResponse(); + + RefreshToken token = refreshTokenService.getByUser(refreshRequest.getUserId()); + + if (token.getToken().equals(refreshRequest.getRefreshToken()) && + !refreshTokenService.isRefreshExpired(token)) { + + User user = token.getUser(); + + String jwtToken = jwtTokenProvider.generateJwtTokenByUserId(user.getId()); + + authResponse.setMessage("Token has been refreshed successfully."); + authResponse.setAccessToken("Bearer " + jwtToken); + authResponse.setUserId(user.getId()); + authResponse.setFirstName(user.getFirstName()); + authResponse.setLastName(user.getLastName()); + authResponse.setUserName(user.getUserName()); + authResponse.setRefreshToken(token.getToken()); + + return new ResponseEntity<>(authResponse, HttpStatus.OK); + } else { + authResponse.setMessage("refresh token is not valid."); + return new ResponseEntity<>(authResponse, HttpStatus.UNAUTHORIZED); + } + } + + @GetMapping("/users") + public ResponseEntity getUsers() { + var response = userService.getAllUsers(); + return ResponseEntity.ok(response); + } + +} + +``` + + + + + + + + + + + + + + + + + + + + + + +## 9.3. UsernameAndPasswordAuthenticationRequest + +Kullanıcı adı ve şifre taleplerini iletmek üzere bir request nesnesi oluşturulur. + +```java +@Data +@NoArgsConstructor +public class UsernameAndPasswordAuthenticationRequest { + + private String username; + private String password; +} +``` + +## 9.4. JwtUsernameAndPasswordAuthenticationFilter + +- **JwtUsernameAndPasswordAuthenticationFilter** sınıfı öncelikle **UsernameAndPasswordAuthenticationRequest** nesnesinden gelen kullanıcı adı ve şifresini **ObjectMapper** aracılığıyla okur ve **Authentication** nesnesi oluşturur. + +- Oluşturulan **Authentication** nesnesi **AuthenticationManager** aracılığıyla oturum açmak üzere kullanılır. + +- Authentication bir interface yapısıdır. Bu interface farklı sınıflar tarafından implemente edilir ([Authentication](https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/core/Authentication.html)). + - AbstractAuthenticationToken, + - AbstractOAuth2TokenAuthenticationToken, + - AnonymousAuthenticationToken, + - BearerTokenAuthentication, + - BearerTokenAuthenticationToken, + - CasAssertionAuthenticationToken, + - CasAuthenticationToken, + - JaasAuthenticationToken, + - JwtAuthenticationToken, + - OAuth2AuthenticationToken, + - OAuth2AuthorizationCodeAuthenticationToken, + - OAuth2LoginAuthenticationToken, + - OpenIDAuthenticationToken, + - PreAuthenticatedAuthenticationToken, + - RememberMeAuthenticationToken, + - RunAsUserToken, + - Saml2Authentication, + - Saml2AuthenticationToken, + - TestingAuthenticationToken, + - _UsernamePasswordAuthenticationToken_ + +> **AuthenticationManager.authenticate(Authentication)** bu metot çalıştığı anda oturum açma isteği iletilir. + +Bu noktaya kadar istemciden -> sunucuya istek kullanıcı bilgilerini (credentials) içerik şekilde iletilir. Dolasıyla bir sonraki adımda kullanıcı bilgilerinin sunucu tarafında doğrulanması gerekir. Doğrulama işlemi başarılı olursa üretilen JWT token istemciye gönderilir. + +Eğer oturum açma işlemi başarılı olursa bir başka ifadeyle kullanıcı bilgileri (credentials) ifadeleri doğrulanırsa, **successfulAuthentication** metodu çalıştırılır. + +Bu metot içerisinde JWT Token oluşturulur. Oluşturulan token Header eklenir. + +Dolasıyla JwtUsernameAndPasswordAuthenticationFilter tasarımı bu şekilde tamamlanır. + +```java +public class JwtUsernameAndPasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter { + + private final AuthenticationManager authenticationManager; + private final JwtConfig jwtConfig; + private final SecretKey secretKey; + + public JwtUsernameAndPasswordAuthenticationFilter(AuthenticationManager authenticationManager, + JwtConfig jwtConfig, + SecretKey secretKey) { + this.authenticationManager = authenticationManager; + this.jwtConfig = jwtConfig; + this.secretKey = secretKey; + } + + @Override + public Authentication attemptAuthentication(HttpServletRequest request, + HttpServletResponse response) throws AuthenticationException { + + try { + UsernameAndPasswordAuthenticationRequest authenticationRequest = new ObjectMapper() + .readValue(request.getInputStream(), UsernameAndPasswordAuthenticationRequest.class); + + Authentication authentication = new UsernamePasswordAuthenticationToken( + authenticationRequest.getUsername(), + authenticationRequest.getPassword()); + + Authentication authenticate = authenticationManager.authenticate(authentication); + return authenticate; + + } catch (IOException e) { + throw new RuntimeException(e); + } + + } + + @Override + protected void successfulAuthentication(HttpServletRequest request, + HttpServletResponse response, + FilterChain chain, + Authentication authResult) throws IOException, ServletException { + + String token = Jwts.builder() + .setSubject(authResult.getName()) + .claim("authorities", authResult.getAuthorities()) + .setIssuedAt(new Date()) + .setExpiration(java.sql.Date.valueOf(LocalDate.now().plusDays(jwtConfig.getTokenExpirationAfterDays()))) + .signWith(secretKey) + .compact(); + + response.addHeader(jwtConfig.getAuthorizationHeader(), jwtConfig.getTokenPrefix() + token); + } +} + +``` + +## 9.5. JwtConfig + +JWT yapılandırılmasını sağlamak üzere JwtConfig sınıfı düzenlenir. + +```java + +@ConfigurationProperties(prefix = "application.jwt") +public class JwtConfig { + + private String secretKey; + private String tokenPrefix; + private Integer tokenExpirationAfterDays; + + public JwtConfig() { + } + + public String getSecretKey() { + return secretKey; + } + + public void setSecretKey(String secretKey) { + this.secretKey = secretKey; + } + + public String getTokenPrefix() { + return tokenPrefix; + } + + public void setTokenPrefix(String tokenPrefix) { + this.tokenPrefix = tokenPrefix; + } + + public Integer getTokenExpirationAfterDays() { + return tokenExpirationAfterDays; + } + + public void setTokenExpirationAfterDays(Integer tokenExpirationAfterDays) { + this.tokenExpirationAfterDays = tokenExpirationAfterDays; + } + + public String getAuthorizationHeader() { + return HttpHeaders.AUTHORIZATION; + } +} + +``` + +## 9.6. JwtSecretKey + +Anahtar değer ve şifreleme algoritması için JwtSecretKey sınıfı kullanılır. + +```java +@Configuration +public class JwtSecretKey { + + private final JwtConfig jwtConfig; + + @Autowired + public JwtSecretKey(JwtConfig jwtConfig) { + this.jwtConfig = jwtConfig; + } + + @Bean + public SecretKey secretKey() { + return Keys.hmacShaKeyFor(jwtConfig.getSecretKey().getBytes()); + } +} +``` + +## 9.7. ApplicationSecurityConfig ifadesinin yapılandırılması + +Öncelikle yapılandırıcıya enjekte edilmesi gereken yapıların enjeksiyonu gerçekleştirilir. Temelde bir filtre yapısı istek zincirine eklenir ve Session yani oturum bilgisini STATELESS olarak ayarlanır. + +```java +private final PasswordEncoder passwordEncoder; +private final ApplicationUserService applicationUserService; +private final SecretKey secretKey; +private final JwtConfig jwtConfig; + +@Autowired +public ApplicationSecurityConfig(PasswordEncoder passwordEncoder, + ApplicationUserService applicationUserService, + SecretKey secretKey, + JwtConfig jwtConfig) { + this.passwordEncoder = passwordEncoder; + this.applicationUserService = applicationUserService; + this.secretKey = secretKey; + this.jwtConfig = jwtConfig; +} +``` + +Daha sonra **configure** metodu üzerinde değişiklikler yapılır. + +```java + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .csrf().disable() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .addFilter( + new JwtUsernameAndPasswordAuthenticationFilter(authenticationManager(), jwtConfig, secretKey)) + .addFilterAfter(new JwtTokenVerifier(secretKey, jwtConfig), + JwtUsernameAndPasswordAuthenticationFilter.class) + .authorizeRequests() + .antMatchers("/", "index", "/css/*", "/js/*").permitAll() + .antMatchers("/api/**").hasRole(USER.name()) + .anyRequest() + .authenticated(); + } +``` + +## Properties + +```text +application.jwt.secretKey=springsecurityspringsecurityspringsecurityspringsecurityspringsecurityspringsecurity +application.jwt.tokenPrefix=Bearer +application.jwt.tokenExpirationAfterDays=10 +application.jwt.refresh.token.expires.in=604800 +``` diff --git a/day-13/api/pom.xml b/day-13/api/pom.xml index 6d34839..a9dc39c 100644 --- a/day-13/api/pom.xml +++ b/day-13/api/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 2.7.2 + 2.7.1 com.bookstore @@ -19,6 +19,24 @@ + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime + + com.google.guava guava diff --git a/day-13/api/src/main/java/com/bookstore/api/config/ApplicationSecurityConfig.java b/day-13/api/src/main/java/com/bookstore/api/config/ApplicationSecurityConfig.java index 76c380f..26c4c5f 100644 --- a/day-13/api/src/main/java/com/bookstore/api/config/ApplicationSecurityConfig.java +++ b/day-13/api/src/main/java/com/bookstore/api/config/ApplicationSecurityConfig.java @@ -16,12 +16,19 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import com.bookstore.api.jwt.JwtAuthenticationEntryPoint; +import com.bookstore.api.jwt.JwtAuthenticationFilter; import com.bookstore.api.services.ApplicationUserService; import lombok.RequiredArgsConstructor; import static com.bookstore.api.security.ApplicationUserRole.*; import static com.bookstore.api.security.ApplicationUserPermission.*; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.config.BeanIds; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; + @Configuration @EnableWebSecurity @RequiredArgsConstructor @@ -31,17 +38,34 @@ public class ApplicationSecurityConfig private final PasswordEncoder passwordEncoder; private final ApplicationUserService applicationUserService; + private final JwtAuthenticationEntryPoint handler; + + @Bean(BeanIds.AUTHENTICATION_MANAGER) + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + @Bean + public JwtAuthenticationFilter jwtAuthenticationFilter() { + return new JwtAuthenticationFilter(); + } @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() + .exceptionHandling().authenticationEntryPoint(handler) + .and() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() .authorizeRequests() - .antMatchers("/api/v1/**").permitAll() + .antMatchers("/", "index", "/css/*", "/js/*").permitAll() + .antMatchers("/api/v1/auth/**").permitAll() + .antMatchers("/api/**").permitAll() .anyRequest() - .authenticated() - .and() - .httpBasic(); + .authenticated(); } @Override @@ -55,4 +79,5 @@ private AuthenticationProvider daoAuthenticationProvider() { provider.setUserDetailsService(applicationUserService); return provider; } + } diff --git a/day-13/api/src/main/java/com/bookstore/api/controllers/AuthController.java b/day-13/api/src/main/java/com/bookstore/api/controllers/AuthController.java new file mode 100644 index 0000000..2634337 --- /dev/null +++ b/day-13/api/src/main/java/com/bookstore/api/controllers/AuthController.java @@ -0,0 +1,157 @@ +package com.bookstore.api.controllers; + +import java.util.List; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.config.annotation.rsocket.RSocketSecurity.AuthorizePayloadsSpec; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.bookstore.api.entities.RefreshToken; +import com.bookstore.api.entities.User; +import com.bookstore.api.entities.dto.AuthResponse; +import com.bookstore.api.entities.dto.RefreshRequest; +import com.bookstore.api.entities.dto.UserRequest; +import com.bookstore.api.entities.dto.UserRequestForRegister; +import com.bookstore.api.jwt.JwtTokenProvider; +import com.bookstore.api.services.RefreshTokenService; +import com.bookstore.api.services.UserRoleService; +import com.bookstore.api.services.Abstract.UserService; + +@RestController +@RequestMapping("/api/v1/auth") +@CrossOrigin(origins = { "http://localhost:3000/", "http://localhost:3001" }) +public class AuthController { + + private AuthenticationManager authenticationManager; + private JwtTokenProvider jwtTokenProvider; + private UserService userService; + private PasswordEncoder passwordEncoder; + private RefreshTokenService refreshTokenService; + private UserRoleService userRoleService; + + public AuthController(AuthenticationManager authenticationManager, JwtTokenProvider jwtTokenProvider, + UserService userService, PasswordEncoder passwordEncoder, RefreshTokenService refreshTokenService, + UserRoleService userRoleService) { + this.authenticationManager = authenticationManager; + this.jwtTokenProvider = jwtTokenProvider; + this.userService = userService; + this.passwordEncoder = passwordEncoder; + this.refreshTokenService = refreshTokenService; + this.userRoleService = userRoleService; + } + + @PostMapping("/login") + public AuthResponse login(@RequestBody UserRequest loginRequest) { + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + loginRequest.getUserName(), + loginRequest.getPassword()); + + Authentication auth = authenticationManager.authenticate(authToken); + + SecurityContextHolder.getContext().setAuthentication(auth); + + String jwtToken = jwtTokenProvider.generateJwtToken(auth); + + User user = userService.getOneUserByUserName(loginRequest.getUserName()); + + AuthResponse authResponse = new AuthResponse(); + authResponse.setAccessToken("Bearer " + jwtToken); + authResponse.setRefreshToken(refreshTokenService.createRefreshToken(user)); + authResponse.setUserId(user.getId()); + authResponse.setMessage("Successed."); + authResponse.setFirstName(user.getFirstName()); + authResponse.setLastName(user.getLastName()); + + return authResponse; + } + + @PostMapping("/register") + public ResponseEntity register(@RequestBody UserRequestForRegister registerRequest) { + + AuthResponse authResponse = new AuthResponse(); + + // User exists? + if (userService.getOneUserByUserName(registerRequest.getUserName()) != null) { + authResponse.setMessage("Username already in use."); + return new ResponseEntity<>(authResponse, HttpStatus.BAD_REQUEST); + } + + // User creating... + User user = new User(); + user.setFirstName(registerRequest.getFirstName()); + user.setLastName(registerRequest.getLastName()); + user.setUserName(registerRequest.getUserName()); + user.setPassword(registerRequest.getPassword()); + + userService.saveOneUser(user); + + // Adding role -> User role is given by default + userRoleService.Add(user.getId(), 3); + + // + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + registerRequest.getUserName(), + registerRequest.getPassword()); + + Authentication auth = authenticationManager.authenticate(authToken); + SecurityContextHolder.getContext().setAuthentication(auth); + String jwtToken = jwtTokenProvider.generateJwtToken(auth); + + authResponse.setMessage("User successfully registered."); + authResponse.setAccessToken("Bearer " + jwtToken); + authResponse.setRefreshToken(refreshTokenService.createRefreshToken(user)); + authResponse.setUserId(user.getId()); + authResponse.setUserName(user.getUserName()); + authResponse.setFirstName(user.getFirstName()); + authResponse.setLastName(user.getLastName()); + + return new ResponseEntity<>(authResponse, HttpStatus.OK); + } + + @PostMapping("/refresh") + public ResponseEntity refresh(@RequestBody RefreshRequest refreshRequest) { + AuthResponse authResponse = new AuthResponse(); + + RefreshToken token = refreshTokenService.getByUser(refreshRequest.getUserId()); + + if (token.getToken().equals(refreshRequest.getRefreshToken()) && + !refreshTokenService.isRefreshExpired(token)) { + + User user = token.getUser(); + + String jwtToken = jwtTokenProvider.generateJwtTokenByUserId(user.getId()); + + authResponse.setMessage("Token has been refreshed successfully."); + authResponse.setAccessToken("Bearer " + jwtToken); + authResponse.setUserId(user.getId()); + authResponse.setFirstName(user.getFirstName()); + authResponse.setLastName(user.getLastName()); + authResponse.setUserName(user.getUserName()); + authResponse.setRefreshToken(token.getToken()); + + return new ResponseEntity<>(authResponse, HttpStatus.OK); + } else { + authResponse.setMessage("refresh token is not valid."); + return new ResponseEntity<>(authResponse, HttpStatus.UNAUTHORIZED); + } + } + + @GetMapping("/users") + public ResponseEntity getUsers() { + var response = userService.getAllUsers(); + return ResponseEntity.ok(response); + } + +} diff --git a/day-13/api/src/main/java/com/bookstore/api/controllers/BookContoller.java b/day-13/api/src/main/java/com/bookstore/api/controllers/BookContoller.java index 7556df9..d82b0d2 100644 --- a/day-13/api/src/main/java/com/bookstore/api/controllers/BookContoller.java +++ b/day-13/api/src/main/java/com/bookstore/api/controllers/BookContoller.java @@ -46,7 +46,7 @@ public ResponseEntity getOneBook(@PathVariable(name = "id", required = true) } @PostMapping - @PreAuthorize("hasRole('ROLE_ADMIN')") + @PreAuthorize("hasAuthority('book:post')") public ResponseEntity postOneBook(@RequestBody @Valid BookDtoForPost book) { var response = bookService.postOneBook(book); return new ResponseEntity<>(response, response.getHttpStatus()); diff --git a/day-13/api/src/main/java/com/bookstore/api/controllers/CategoryController.java b/day-13/api/src/main/java/com/bookstore/api/controllers/CategoryController.java index da6b62b..25ac672 100644 --- a/day-13/api/src/main/java/com/bookstore/api/controllers/CategoryController.java +++ b/day-13/api/src/main/java/com/bookstore/api/controllers/CategoryController.java @@ -4,6 +4,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; @@ -25,6 +26,7 @@ @RestController @RequestMapping("api/v1/categories") +@PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_EDITOR','ROLE_USER')") public class CategoryController { private final CategoryService categoryService; diff --git a/day-13/api/src/main/java/com/bookstore/api/entities/RefreshToken.java b/day-13/api/src/main/java/com/bookstore/api/entities/RefreshToken.java new file mode 100644 index 0000000..96b0a87 --- /dev/null +++ b/day-13/api/src/main/java/com/bookstore/api/entities/RefreshToken.java @@ -0,0 +1,44 @@ +package com.bookstore.api.entities; + +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +import lombok.Data; + +@Entity +@Table(name = "refresh_token") +@Data +public class RefreshToken { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + int id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) + @JsonIgnore + User user; + + @Column(nullable = false, unique = true) + String token; + + @Column(nullable = false) + @Temporal(TemporalType.TIMESTAMP) + Date expiryDate; +} diff --git a/day-13/api/src/main/java/com/bookstore/api/entities/Role.java b/day-13/api/src/main/java/com/bookstore/api/entities/Role.java index ccc5165..e133c8e 100644 --- a/day-13/api/src/main/java/com/bookstore/api/entities/Role.java +++ b/day-13/api/src/main/java/com/bookstore/api/entities/Role.java @@ -10,21 +10,22 @@ import lombok.NoArgsConstructor; import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; @Data @Entity -@Table(name="roles") +@Table(name = "roles") @NoArgsConstructor @AllArgsConstructor public class Role { @Id - @GeneratedValue + @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "role_id") private int id; - + @Column(name = "name") private String name; - + @Column(name = "description") private String description; } diff --git a/day-13/api/src/main/java/com/bookstore/api/entities/User.java b/day-13/api/src/main/java/com/bookstore/api/entities/User.java index f49152e..aff37f6 100644 --- a/day-13/api/src/main/java/com/bookstore/api/entities/User.java +++ b/day-13/api/src/main/java/com/bookstore/api/entities/User.java @@ -2,9 +2,12 @@ import java.util.Set; +import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.FetchType; import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; @@ -22,12 +25,12 @@ @AllArgsConstructor public class User { @Id - @GeneratedValue + @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "user_id") private int id; @Column(name = "user_name") - private String username; + private String userName; @Column(name = "password") private String password; @@ -38,9 +41,8 @@ public class User { @Column(name = "last_name") private String lastName; - @ManyToMany - @JoinTable(name="user_roles", - joinColumns = @JoinColumn(name="user_id"), inverseJoinColumns = @JoinColumn(name="role_id")) + @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) + @JoinTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id")) private Set roles; } diff --git a/day-13/api/src/main/java/com/bookstore/api/entities/UserRole.java b/day-13/api/src/main/java/com/bookstore/api/entities/UserRole.java index 4bbce52..aca24e4 100644 --- a/day-13/api/src/main/java/com/bookstore/api/entities/UserRole.java +++ b/day-13/api/src/main/java/com/bookstore/api/entities/UserRole.java @@ -2,6 +2,7 @@ import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.Table; @@ -9,14 +10,12 @@ import lombok.Data; import lombok.NoArgsConstructor; -import javax.persistence.GeneratedValue; - -@Data @Entity -@Table(name = "user_roles") +@Table(name = "users_roles") +@Data @NoArgsConstructor -@AllArgsConstructor public class UserRole { + @Id @GeneratedValue @Column(name = "id") @@ -27,4 +26,10 @@ public class UserRole { @Column(name = "role_id") private int roleId; + + public UserRole(int userId, int roleId) { + this.userId = userId; + this.roleId = roleId; + } + } diff --git a/day-13/api/src/main/java/com/bookstore/api/entities/dto/AuthResponse.java b/day-13/api/src/main/java/com/bookstore/api/entities/dto/AuthResponse.java new file mode 100644 index 0000000..c19e343 --- /dev/null +++ b/day-13/api/src/main/java/com/bookstore/api/entities/dto/AuthResponse.java @@ -0,0 +1,17 @@ +package com.bookstore.api.entities.dto; + +import lombok.Data; + +@Data +public class AuthResponse { + private String message; + + private int userId; + private String userName; + + private String firstName; + private String lastName; + + private String accessToken; + private String refreshToken; +} diff --git a/day-13/api/src/main/java/com/bookstore/api/entities/dto/RefreshRequest.java b/day-13/api/src/main/java/com/bookstore/api/entities/dto/RefreshRequest.java new file mode 100644 index 0000000..e960d32 --- /dev/null +++ b/day-13/api/src/main/java/com/bookstore/api/entities/dto/RefreshRequest.java @@ -0,0 +1,9 @@ +package com.bookstore.api.entities.dto; + +import lombok.Data; + +@Data +public class RefreshRequest { + private int userId; + private String refreshToken; +} diff --git a/day-13/api/src/main/java/com/bookstore/api/entities/dto/UserDto.java b/day-13/api/src/main/java/com/bookstore/api/entities/dto/UserDto.java new file mode 100644 index 0000000..cc17409 --- /dev/null +++ b/day-13/api/src/main/java/com/bookstore/api/entities/dto/UserDto.java @@ -0,0 +1,16 @@ +package com.bookstore.api.entities.dto; + +import java.util.Set; + +import com.bookstore.api.entities.Role; + +import lombok.Data; + +@Data +public class UserDto { + private int id; + private String userName; + private String firstName; + private String lastName; + private Set roles; +} diff --git a/day-13/api/src/main/java/com/bookstore/api/entities/dto/UserRequest.java b/day-13/api/src/main/java/com/bookstore/api/entities/dto/UserRequest.java new file mode 100644 index 0000000..5c80217 --- /dev/null +++ b/day-13/api/src/main/java/com/bookstore/api/entities/dto/UserRequest.java @@ -0,0 +1,11 @@ +package com.bookstore.api.entities.dto; + +import lombok.Data; + +@Data +public class UserRequest { + private String firstName; + private String lastName; + private String userName; + private String password; +} diff --git a/day-13/api/src/main/java/com/bookstore/api/entities/dto/UserRequestForRegister.java b/day-13/api/src/main/java/com/bookstore/api/entities/dto/UserRequestForRegister.java new file mode 100644 index 0000000..0ee06a5 --- /dev/null +++ b/day-13/api/src/main/java/com/bookstore/api/entities/dto/UserRequestForRegister.java @@ -0,0 +1,11 @@ +package com.bookstore.api.entities.dto; + +import lombok.Data; + +@Data +public class UserRequestForRegister { + private String firstName; + private String lastName; + private String userName; + private String password; +} diff --git a/day-13/api/src/main/java/com/bookstore/api/exceptions/notFoundExceptions/UserNotFoundException.java b/day-13/api/src/main/java/com/bookstore/api/exceptions/notFoundExceptions/UserNotFoundException.java new file mode 100644 index 0000000..5d62269 --- /dev/null +++ b/day-13/api/src/main/java/com/bookstore/api/exceptions/notFoundExceptions/UserNotFoundException.java @@ -0,0 +1,7 @@ +package com.bookstore.api.exceptions.notFoundExceptions; + +public class UserNotFoundException extends NotFoundException { + public UserNotFoundException(int id) { + super(String.format("User with %s id could not found.", id)); + } +} diff --git a/day-13/api/src/main/java/com/bookstore/api/jwt/JwtAuthenticationEntryPoint.java b/day-13/api/src/main/java/com/bookstore/api/jwt/JwtAuthenticationEntryPoint.java new file mode 100644 index 0000000..aec37f2 --- /dev/null +++ b/day-13/api/src/main/java/com/bookstore/api/jwt/JwtAuthenticationEntryPoint.java @@ -0,0 +1,21 @@ +package com.bookstore.api.jwt; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +@Component +public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) throws IOException, ServletException { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage()); + } +} diff --git a/day-13/api/src/main/java/com/bookstore/api/jwt/JwtAuthenticationFilter.java b/day-13/api/src/main/java/com/bookstore/api/jwt/JwtAuthenticationFilter.java new file mode 100644 index 0000000..9c007e7 --- /dev/null +++ b/day-13/api/src/main/java/com/bookstore/api/jwt/JwtAuthenticationFilter.java @@ -0,0 +1,56 @@ +package com.bookstore.api.jwt; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import com.bookstore.api.services.ApplicationUserService; + +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + @Autowired + JwtTokenProvider jwtTokenProvider; + + @Autowired + ApplicationUserService userDetailsService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + try { + String jwtToken = extractJwtFromRequest(request); + if (StringUtils.hasText(jwtToken) && jwtTokenProvider.validateToken(jwtToken)) { + String username = jwtTokenProvider.getUsernameFromJwt(jwtToken); + UserDetails user = userDetailsService.loadUserByUsername(username); + if (user != null) { + UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(user, null, + user.getAuthorities()); + + auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(auth); + } + } + } catch (Exception e) { + return; + } + filterChain.doFilter(request, response); + } + + private String extractJwtFromRequest(HttpServletRequest request) { + String bearer = request.getHeader("Authorization"); + if (StringUtils.hasText(bearer) && bearer.startsWith("Bearer ")) + return bearer.substring("Bearer".length() + 1); + return null; + } +} diff --git a/day-13/api/src/main/java/com/bookstore/api/jwt/JwtConfig.java b/day-13/api/src/main/java/com/bookstore/api/jwt/JwtConfig.java new file mode 100644 index 0000000..e77743e --- /dev/null +++ b/day-13/api/src/main/java/com/bookstore/api/jwt/JwtConfig.java @@ -0,0 +1,24 @@ +package com.bookstore.api.jwt; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; + +import lombok.Data; + +@Configuration +@ConfigurationProperties(prefix = "application.jwt") +@Data +public class JwtConfig { + + private String secretKey; + private String tokenPrefix; + + @Value("${application.jwt.expires.in}") + private Long expiresIn; + + public String getAuthorizationHeader() { + return HttpHeaders.AUTHORIZATION; + } +} diff --git a/day-13/api/src/main/java/com/bookstore/api/jwt/JwtSecretKey.java b/day-13/api/src/main/java/com/bookstore/api/jwt/JwtSecretKey.java new file mode 100644 index 0000000..1fa04b4 --- /dev/null +++ b/day-13/api/src/main/java/com/bookstore/api/jwt/JwtSecretKey.java @@ -0,0 +1,24 @@ +package com.bookstore.api.jwt; + +import javax.crypto.SecretKey; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; + +@Configuration +public class JwtSecretKey { + + private final JwtConfig jwtConfig; + + public JwtSecretKey(JwtConfig jwtConfig) { + this.jwtConfig = jwtConfig; + } + + @Bean + public SecretKey secretKey() { + return Keys.hmacShaKeyFor(jwtConfig.getSecretKey().getBytes()); + } +} diff --git a/day-13/api/src/main/java/com/bookstore/api/jwt/JwtTokenProvider.java b/day-13/api/src/main/java/com/bookstore/api/jwt/JwtTokenProvider.java new file mode 100644 index 0000000..ae0f0ec --- /dev/null +++ b/day-13/api/src/main/java/com/bookstore/api/jwt/JwtTokenProvider.java @@ -0,0 +1,98 @@ +package com.bookstore.api.jwt; + +import java.security.Key; +import java.util.Date; + +import javax.crypto.SecretKey; + +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +import com.bookstore.api.security.ApplicationUser; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.SignatureException; +import io.jsonwebtoken.UnsupportedJwtException; + +@Component +public class JwtTokenProvider { + + private final JwtConfig jwtConfig; + private final SecretKey secretKey; + + public JwtTokenProvider(JwtConfig jwtConfig, SecretKey secretKey) { + this.jwtConfig = jwtConfig; + this.secretKey = secretKey; + System.out.println(jwtConfig.getExpiresIn()); + } + + public String generateJwtToken(Authentication auth) { + + ApplicationUser userDetails = (ApplicationUser) auth.getPrincipal(); + + return Jwts.builder() + .setSubject(userDetails.getUsername()) + .claim("authorities", userDetails.getAuthorities()) + .setIssuedAt(new Date()) + .setExpiration(new Date(new Date().getTime() + jwtConfig.getExpiresIn())) + .signWith(secretKey) + .compact(); + } + + public String generateJwtTokenByUserId(int userId) { + + Date expireDate = new Date(new Date().getTime() + jwtConfig.getExpiresIn()); + return Jwts.builder() + .setSubject(Integer.toString(userId)) + .setIssuedAt(new Date()) + .setExpiration(expireDate) + .signWith(secretKey) + .compact(); + } + + public String generateJwtTokenByUserName(String username) { + Date expireDate = new Date(new Date().getTime() + jwtConfig.getExpiresIn()); + return Jwts.builder() + .setSubject(username) + .setIssuedAt(new Date()) + .setExpiration(expireDate) + .signWith(secretKey) + .compact(); + } + + String getUsernameFromJwt(String token) { + Claims claims = getJwtBody(token); + return claims.getSubject(); + } + + boolean validateToken(String token) { + try { + getJwtBody(token); + return !isTokenExpired(token); + } catch (MalformedJwtException e) { + return false; + } catch (ExpiredJwtException e) { + return false; + } catch (UnsupportedJwtException e) { + return false; + } catch (IllegalArgumentException e) { + return false; + } + } + + boolean isTokenExpired(String token) { + Date expiration = getJwtBody(token).getExpiration(); + return expiration.before(new Date()); + } + + private Claims getJwtBody(String token) { + + return Jwts.parser() + .setSigningKey(secretKey) + .parseClaimsJws(token) + .getBody(); + } +} diff --git a/day-13/api/src/main/java/com/bookstore/api/repositories/RefreshTokenRepository.java b/day-13/api/src/main/java/com/bookstore/api/repositories/RefreshTokenRepository.java new file mode 100644 index 0000000..e5e933e --- /dev/null +++ b/day-13/api/src/main/java/com/bookstore/api/repositories/RefreshTokenRepository.java @@ -0,0 +1,9 @@ +package com.bookstore.api.repositories; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.bookstore.api.entities.RefreshToken; + +public interface RefreshTokenRepository extends JpaRepository { + RefreshToken findByUserId(int userId); +} diff --git a/day-13/api/src/main/java/com/bookstore/api/repositories/RoleRepository.java b/day-13/api/src/main/java/com/bookstore/api/repositories/RoleRepository.java index 3c94059..b9b7705 100644 --- a/day-13/api/src/main/java/com/bookstore/api/repositories/RoleRepository.java +++ b/day-13/api/src/main/java/com/bookstore/api/repositories/RoleRepository.java @@ -1,9 +1,15 @@ package com.bookstore.api.repositories; +import java.util.Set; + import org.springframework.data.jpa.repository.JpaRepository; import com.bookstore.api.entities.Role; public interface RoleRepository extends JpaRepository { + + Role findByName(String string); + + Set findByIdIn(Set roles); } diff --git a/day-13/api/src/main/java/com/bookstore/api/repositories/UserRepository.java b/day-13/api/src/main/java/com/bookstore/api/repositories/UserRepository.java index c6bb255..eca1835 100644 --- a/day-13/api/src/main/java/com/bookstore/api/repositories/UserRepository.java +++ b/day-13/api/src/main/java/com/bookstore/api/repositories/UserRepository.java @@ -5,5 +5,7 @@ import com.bookstore.api.entities.User; public interface UserRepository extends JpaRepository { + + User findByUserName(String username); } diff --git a/day-13/api/src/main/java/com/bookstore/api/repositories/UserRoleRepository.java b/day-13/api/src/main/java/com/bookstore/api/repositories/UserRoleRepository.java index 7423f86..8d95b73 100644 --- a/day-13/api/src/main/java/com/bookstore/api/repositories/UserRoleRepository.java +++ b/day-13/api/src/main/java/com/bookstore/api/repositories/UserRoleRepository.java @@ -5,5 +5,4 @@ import com.bookstore.api.entities.UserRole; public interface UserRoleRepository extends JpaRepository { - } diff --git a/day-13/api/src/main/java/com/bookstore/api/services/Abstract/UserService.java b/day-13/api/src/main/java/com/bookstore/api/services/Abstract/UserService.java index dfc1dc2..0745a53 100644 --- a/day-13/api/src/main/java/com/bookstore/api/services/Abstract/UserService.java +++ b/day-13/api/src/main/java/com/bookstore/api/services/Abstract/UserService.java @@ -1,5 +1,24 @@ package com.bookstore.api.services.Abstract; +import java.util.List; + +import com.bookstore.api.entities.User; +import com.bookstore.api.entities.dto.UserDto; +import com.bookstore.api.entities.models.ApiResponse; + public interface UserService extends ApplicationUserDao { - + ApiResponse> getAllUsers(); + + ApiResponse getOneUser(int id); + + ApiResponse postOneUser(User user); + + ApiResponse putOneUser(int userId, User user); + + User getOneUserByUserName(String userName); + + void deleteOneUser(int userId); + + User saveOneUser(User user); + } diff --git a/day-13/api/src/main/java/com/bookstore/api/services/ApplicationUserService.java b/day-13/api/src/main/java/com/bookstore/api/services/ApplicationUserService.java index 44a1932..1401498 100644 --- a/day-13/api/src/main/java/com/bookstore/api/services/ApplicationUserService.java +++ b/day-13/api/src/main/java/com/bookstore/api/services/ApplicationUserService.java @@ -1,5 +1,6 @@ package com.bookstore.api.services; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; @@ -10,16 +11,19 @@ import lombok.RequiredArgsConstructor; @Service -@RequiredArgsConstructor public class ApplicationUserService implements UserDetailsService { private final ApplicationUserDao applicationUserDao; + public ApplicationUserService(@Qualifier("mysql") ApplicationUserDao applicationUserDao) { + this.applicationUserDao = applicationUserDao; + } + @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return applicationUserDao - .selectApplicationUserByUsername(username) - .orElseThrow(() -> new UsernameNotFoundException("Username could not found.")); + .selectApplicationUserByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("Username could not found.")); } - + } diff --git a/day-13/api/src/main/java/com/bookstore/api/services/FakeApplicationUserDaoService.java b/day-13/api/src/main/java/com/bookstore/api/services/FakeApplicationUserDaoService.java index 80cf6ba..aed4248 100644 --- a/day-13/api/src/main/java/com/bookstore/api/services/FakeApplicationUserDaoService.java +++ b/day-13/api/src/main/java/com/bookstore/api/services/FakeApplicationUserDaoService.java @@ -4,6 +4,7 @@ import java.util.Optional; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Repository; import org.springframework.stereotype.Service; import com.bookstore.api.security.ApplicationUser; @@ -15,45 +16,46 @@ @Service @RequiredArgsConstructor +@Repository("fake") public class FakeApplicationUserDaoService implements ApplicationUserDao { - private final PasswordEncoder passwordEncoder; - - @Override - public Optional selectApplicationUserByUsername(String username) { - - return getApplicationUsers() - .stream() - .filter(applicationUser -> username.equals(applicationUser.getUsername())) - .findFirst(); - } - - private List getApplicationUsers() { - List applicationUsers = Lists.newArrayList( - new ApplicationUser("admin", - passwordEncoder.encode("admin123456"), - ADMIN.getGrantedAuthorities(), - true, - true, - true, - true), - - new ApplicationUser("editor", - passwordEncoder.encode("editor123"), - EDITOR.getGrantedAuthorities(), - true, - true, - true, - true), - - new ApplicationUser("user", - passwordEncoder.encode("user123"), - USER.getGrantedAuthorities(), - true, - true, - true, - true)); - return applicationUsers; - } + private final PasswordEncoder passwordEncoder; + + @Override + public Optional selectApplicationUserByUsername(String username) { + + return getApplicationUsers() + .stream() + .filter(applicationUser -> username.equals(applicationUser.getUsername())) + .findFirst(); + } + + private List getApplicationUsers() { + List applicationUsers = Lists.newArrayList( + new ApplicationUser("admin", + passwordEncoder.encode("admin123"), + ADMIN.getGrantedAuthorities(), + true, + true, + true, + true), + + new ApplicationUser("editor", + passwordEncoder.encode("editor123"), + EDITOR.getGrantedAuthorities(), + true, + true, + true, + true), + + new ApplicationUser("user", + passwordEncoder.encode("user123"), + USER.getGrantedAuthorities(), + true, + true, + true, + true)); + return applicationUsers; + } } diff --git a/day-13/api/src/main/java/com/bookstore/api/services/RefreshTokenService.java b/day-13/api/src/main/java/com/bookstore/api/services/RefreshTokenService.java new file mode 100644 index 0000000..bf89ca7 --- /dev/null +++ b/day-13/api/src/main/java/com/bookstore/api/services/RefreshTokenService.java @@ -0,0 +1,46 @@ +package com.bookstore.api.services; + +import java.time.Instant; +import java.util.Date; +import java.util.UUID; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import com.bookstore.api.entities.RefreshToken; +import com.bookstore.api.entities.User; +import com.bookstore.api.repositories.RefreshTokenRepository; + +@Service +public class RefreshTokenService { + + @Value("${application.jwt.refresh.token.expires.in}") + Long expireSeconds; + + private RefreshTokenRepository refreshTokenRepository; + + public RefreshTokenService(RefreshTokenRepository refreshTokenRepository) { + this.refreshTokenRepository = refreshTokenRepository; + } + + public String createRefreshToken(User user) { + RefreshToken token = refreshTokenRepository.findByUserId(user.getId()); + if (token == null) { + token = new RefreshToken(); + token.setUser(user); + } + token.setToken(UUID.randomUUID().toString()); + token.setExpiryDate(Date.from(Instant.now().plusSeconds(expireSeconds))); + refreshTokenRepository.save(token); + return token.getToken(); + } + + public boolean isRefreshExpired(RefreshToken token) { + return token.getExpiryDate().before(new Date()); + } + + public RefreshToken getByUser(int userId) { + return refreshTokenRepository.findByUserId(userId); + } + +} diff --git a/day-13/api/src/main/java/com/bookstore/api/services/UserRoleService.java b/day-13/api/src/main/java/com/bookstore/api/services/UserRoleService.java new file mode 100644 index 0000000..560fe81 --- /dev/null +++ b/day-13/api/src/main/java/com/bookstore/api/services/UserRoleService.java @@ -0,0 +1,21 @@ +package com.bookstore.api.services; + +import org.springframework.stereotype.Service; + +import com.bookstore.api.entities.UserRole; +import com.bookstore.api.repositories.UserRoleRepository; + +@Service +public class UserRoleService { + private final UserRoleRepository userRoleRepository; + + public UserRoleService(UserRoleRepository userRoleRepository) { + this.userRoleRepository = userRoleRepository; + } + + public void Add(int userId, int roleid) { + UserRole userRole = new UserRole(userId, roleid); + userRoleRepository.save(userRole); + } + +} diff --git a/day-13/api/src/main/java/com/bookstore/api/services/UserServiceImp.java b/day-13/api/src/main/java/com/bookstore/api/services/UserServiceImp.java index b3e43c1..59ab4a7 100644 --- a/day-13/api/src/main/java/com/bookstore/api/services/UserServiceImp.java +++ b/day-13/api/src/main/java/com/bookstore/api/services/UserServiceImp.java @@ -1,22 +1,138 @@ package com.bookstore.api.services; +import java.util.HashSet; +import java.util.List; import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; -import org.springframework.stereotype.Service; - +import com.bookstore.api.entities.Role; +import com.bookstore.api.entities.User; +import com.bookstore.api.entities.dto.UserDto; +import com.bookstore.api.entities.models.ApiResponse; +import com.bookstore.api.exceptions.notFoundExceptions.UserNotFoundException; +import com.bookstore.api.repositories.RoleRepository; import com.bookstore.api.repositories.UserRepository; import com.bookstore.api.security.ApplicationUser; import com.bookstore.api.services.Abstract.UserService; import lombok.RequiredArgsConstructor; + +import org.modelmapper.ModelMapper; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Repository; +import org.springframework.stereotype.Service; + +import static com.bookstore.api.security.ApplicationUserRole.*; + @Service @RequiredArgsConstructor +@Repository("mysql") public class UserServiceImp implements UserService { private final UserRepository userRepository; + private final RoleRepository roleRepository; + private final PasswordEncoder passwordEncoder; + private final ModelMapper mapper; + + @Override + public ApiResponse> getAllUsers() { + List users = userRepository.findAll(); + + List list = users + .stream() + .map(user -> mapper.map(user, UserDto.class)) + .collect(Collectors.toList()); + + return ApiResponse.default_OK(list); + } + + @Override + public ApiResponse getOneUser(int userId) { + User user = userRepository + .findById(userId) + .orElseThrow(() -> new UserNotFoundException(userId)); + + UserDto userDto = mapper.map(user, UserDto.class); + + return ApiResponse.default_OK(userDto); + } + + @Override + public ApiResponse postOneUser(User user) { + Set roles = new HashSet<>(); + Role role = roleRepository.findByName("USER"); + if (role == null) { + throw new RuntimeException("USER role is not defined."); + } + roles.add(role); + user.setRoles(roles); + user.setPassword(passwordEncoder.encode(user.getPassword())); + userRepository.save(user); + return ApiResponse.default_CREATED(mapper.map(user, UserDto.class)); + } + + @Override + public ApiResponse putOneUser(int userId, User user) { + getOneUser(userId); + + Set roles = roleRepository.findByIdIn(user.getRoles()); + user.setId(userId); + user.setRoles(roles); + + userRepository.save(user); + return ApiResponse.default_ACCEPTED(mapper.map(user, UserDto.class)); + } + + @Override + public void deleteOneUser(int userId) { + userRepository.deleteById(userId); + } + + @Override + public User getOneUserByUserName(String userName) { + return userRepository.findByUserName(userName); + } + + @Override + public User saveOneUser(User newUser) { + newUser.setPassword(passwordEncoder.encode(newUser.getPassword())); + return userRepository.save(newUser); + } + @Override public Optional selectApplicationUserByUsername(String username) { - return Optional.empty(); + + User user = userRepository.findByUserName(username); + + Set grantedAuthorities = new HashSet<>(); + Set roles = user.getRoles(); + + for (Role role : roles) { + switch (role.getId()) { + case 1: + grantedAuthorities.addAll(ADMIN.getGrantedAuthorities()); + break; + case 2: + grantedAuthorities.addAll(EDITOR.getGrantedAuthorities()); + break; + case 3: + grantedAuthorities.addAll(USER.getGrantedAuthorities()); + break; + default: + break; + } + } + + Optional applicationUser = Optional.ofNullable(new ApplicationUser( + user.getUserName(), + user.getPassword(), + grantedAuthorities, + true, + true, + true, + true)); + return applicationUser; } - } diff --git a/day-13/api/src/main/resources/application.properties b/day-13/api/src/main/resources/application.properties index 985cf60..cc10898 100644 --- a/day-13/api/src/main/resources/application.properties +++ b/day-13/api/src/main/resources/application.properties @@ -5,4 +5,11 @@ spring.jpa.hibernate.ddl-auto=update spring.datasource.url=jdbc:mysql://localhost:3306/book-store-course?useUnicode=true&useLegacyDatetimeCode=false&serverTimezone=Turkey spring.datasource.username=root spring.datasource.password=123456 -spring.jpa.show-sql=true \ No newline at end of file +spring.jpa.show-sql=true + + +application.jwt.secretKey=springsecurityspringsecurityspringsecurityspringsecurityspringsecurityspringsecurityxxyy12345698 +application.jwt.expires.in=3600000 +application.jwt.tokenPrefix=Bearer +application.jwt.tokenExpirationAfterDays=10 +application.jwt.refresh.token.expires.in=604800 \ No newline at end of file