post-image

JWT trong bảo mật ứng dụng Spring Boot

REST API

JWT là một phương tiện đại diện cho các yêu cầu chuyển giao giữa hai bên Client – Server , các thông tin trong chuỗi JWT được định dạng bằng JSON . Trong đó chuỗi Token phải có 3 phần là header , phần payload và phần signature được ngăn bằng dấu “.” Vây nên theo lý thuyết chuỗi token của chúng ta sẽ có dạng như sau:

header.payload.signature
Code language: CSS (css)

Bài viết này mình sẽ hướng dẫn mọi người cách bảo mật ứng dụng Spring Boot với JWT.

>> Xem ngay Tài liệu Java Core giúp bạn “Nâng Cấp” kỹ năng lập trình

Cài đặt thư viện

Chúng ta tạo một ứng dụng Spring Boot với những thư viện như sau:

implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' runtimeOnly 'mysql:mysql-connector-java' annotationProcessor 'org.projectlombok:lombok' testImplementation('org.springframework.boot:spring-boot-starter-test') { exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' } testImplementation 'org.springframework.security:spring-security-test'
Code language: JavaScript (javascript)

Ở đây mình sử dụng thêm thư viện io.jsonwebtoken để tạo ra JWT:

compile group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.1'
Code language: JavaScript (javascript)

Cấu hình file application.properties

Chúng ta truy cập vào file application.properties và cấu hình đường dẫn tới cơ sở dữ liệu và cấu hình Hibernate như sau:

server.port=${port:8080} #Database spring.datasource.url=jdbc:mysql://localhost:3306/demo-jwt spring.datasource.username=root spring.datasource.password=123456 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver #Hibernate spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect spring.jpa.properties.hibernate.id.new_generator_mappings = true spring.jpa.properties.hibernate.show.sql = true spring.jpa.properties.hibernate.format_sql = true spring.jpa.generate-ddl=true spring.jpa.properties.hibernate.hb2dll.auto = update logging.level.org.hibernate.SQL=DEBUG logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
Code language: PHP (php)

Chuẩn bị

Chúng ta sẽ viết lần lượt Model, Repository, Service cho 2 class User và Role

Model

  • Tạo model User ở đây chúng ta sẽ để mối quan hệ giữa User và Role là nhiều nhiều (1 user sẽ có nhiều role và 1 role có nhiều user).
package com.example.demo.model; import lombok.Data; import javax.persistence.*; import java.util.Set; @Entity @Data @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false) private String username; @Column(nullable = false) private String password; @Column(nullable = false) private String fullName; @ManyToMany(fetch = FetchType.EAGER) @JoinTable(name = "users_roles", joinColumns = {@JoinColumn(name = "user_id")}, inverseJoinColumns = {@JoinColumn(name = "role_id")}) private Set<Role> roles; }
Code language: CSS (css)
  • Tạo model Role
package com.example.demo.model; import lombok.Data; import javax.persistence.*; @Entity @Data @Table(name = "roles") public class Role { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; }
Code language: CSS (css)
  • Ở đây mình sẽ tạo thêm một class là UserPrincipal để có thể custom lại các thuộc tính của lớp UserDetails có sẵn trong Spring.
package com.example.demo.model; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; public class UserPrinciple implements UserDetails { private static final long serialVersionUID = 1L; private Long id; private String email; private String password; private Collection<? extends GrantedAuthority> roles; public UserPrinciple(Long id, String email, String password, Collection<? extends GrantedAuthority> roles) { this.id = id; this.email = email; this.password = password; this.roles = roles; } public static UserPrinciple build(User user) { List<GrantedAuthority> authorities = user.getRoles().stream().map(role -> new SimpleGrantedAuthority(role.getName()) ).collect(Collectors.toList()); return new UserPrinciple( user.getId(), user.getEmail(), user.getPassword(), authorities ); } public Long getId() { return id; } @Override public String getUsername() { return email; } @Override public String getPassword() { return password; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return roles; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; UserPrinciple user = (UserPrinciple) o; return Objects.equals(id, user.id); } @Override public int hashCode() { return super.hashCode(); } }
Code language: PHP (php)

Repository

Chúng ta sẽ tạo lần lượt 2 interface đó là UserRepository và RoleRepository kế thừa từ interface có sẵn của Jpa là JpaRepository.

IUserRepository.java

package com.example.demo.repository; import com.example.demo.model.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.util.Optional; @Repository public interface IUserRepository extends JpaRepository<User, Long> { Optional<User> findByUsername(String username); }
Code language: CSS (css)

IRoleRepository.java

package com.example.demo.repository; import com.example.demo.model.Role; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface IRoleRepository extends JpaRepository<Role, Long> { Role findByName(String name); }
Code language: CSS (css)

Service

  • Chúng ta một interface GeneralService chứa các phương thức CRUD để dùng chung 
package com.example.demo.service; import java.util.Optional; public interface IGeneralService<T> { Iterable<T> findAll(); Optional<T> findById(Long id); T save(T t); void remove(Long id); }
Code language: HTML, XML (xml)
  • Sau đó chúng ta sẽ tạo interface IUserService kế thừa từ interface GeneralService trên và kế thừa interface UserDetailService có sẵn của Spring
package com.example.demo.service.user; import com.example.demo.model.User; import com.example.demo.service.IGeneralService; import org.springframework.security.core.userdetails.UserDetailsService; import java.util.Optional; public interface IUserService extends IGeneralService<User>, UserDetailsService { Optional<User> findByUsername(String username); }
Code language: JavaScript (javascript)

UserService.java

package com.example.demo.service.user; import com.example.demo.model.User; import com.example.demo.model.UserPrinciple; import com.example.demo.repository.IUserRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import java.util.Optional; @Service public class UserService implements IUserService { @Autowired private IUserRepository userRepository; @Autowired private PasswordEncoder passwordEncoder; @Override public Iterable<User> findAll() { return userRepository.findAll(); } @Override public Optional<User> findById(Long id) { return userRepository.findById(id); } @Override public User save(User user) { user.setPassword(passwordEncoder.encode(user.getPassword())); return userRepository.save(user); } @Override public void remove(Long id) { userRepository.deleteById(id); } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Optional<User> userOptional = userRepository.findByUsername(username); if (!userOptional.isPresent()) { throw new UsernameNotFoundException(username); } return UserPrinciple.build(userOptional.get()); } @Override public Optional<User> findByUsername(String username) { return userRepository.findByUsername(username); } }
Code language: JavaScript (javascript)

IRoleService.java

package com.example.demo.service.role; import com.example.demo.model.Role; import com.example.demo.service.IGeneralService; public interface IRoleService extends IGeneralService<Role> { Role findByName(String name); }
Code language: PHP (php)

RoleService.java

package com.example.demo.service.role; import com.example.demo.model.Role; import com.example.demo.repository.IRoleRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Optional; @Service public class RoleService implements IRoleService { @Autowired private IRoleRepository roleRepository; @Override public Iterable<Role> findAll() { return roleRepository.findAll(); } @Override public Optional<Role> findById(Long id) { return roleRepository.findById(id); } @Override public Role save(Role role) { return roleRepository.save(role); } @Override public void remove(Long id) { roleRepository.deleteById(id); } @Override public Role findByName(String name) { return roleRepository.findByName(name); } }
Code language: CSS (css)

Các file Configure

  • CustomAccessDeniedHandler: Tùy chỉnh trạng thái và thông điệp thông báo khi không có quyền truy cập
package com.example.demo.configuration.custom; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class CustomAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.getWriter().write("Access Denied!"); } }
Code language: JavaScript (javascript)
  • RestAuthenticationEntryPoint: Cấu hình lại thông điệp khi chưa được xác thực
package com.example.demo.configuration.custom; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { private static final Logger logger = LoggerFactory.getLogger(RestAuthenticationEntryPoint.class); @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException { logger.error("Unauthorized error. Message - {}", e.getMessage()); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error -> Unauthorized"); } }
Code language: JavaScript (javascript)
  • MethodSecurityConfigurer.java
package com.example.market.configuration.security; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration; @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class MethodSecurityConfigurer extends GlobalMethodSecurityConfiguration { }
Code language: CSS (css)
  • SecurityConfig: Cấu hình phân quyền cho ứng dụng và khai báo một số Bean để custom lại thông điệp
package com.example.demo.configuration.security; import com.example.demo.configuration.custom.CustomAccessDeniedHandler; import com.example.demo.configuration.custom.RestAuthenticationEntryPoint; import com.example.demo.service.user.IUserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.BeanIds; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 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.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private IUserService userService; @Bean(BeanIds.AUTHENTICATION_MANAGER) @Override public AuthenticationManager authenticationManager() throws Exception { return super.authenticationManager(); } @Bean public RestAuthenticationEntryPoint restServicesEntryPoint() { return new RestAuthenticationEntryPoint(); } @Bean public CustomAccessDeniedHandler customAccessDeniedHandler() { return new CustomAccessDeniedHandler(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(10); } @Autowired public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService).passwordEncoder(passwordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().ignoringAntMatchers("/**"); http.httpBasic().authenticationEntryPoint(restServicesEntryPoint()); http.authorizeRequests() .antMatchers("/", "/login").permitAll() .anyRequest().authenticated() .and().csrf().disable(); http.sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.cors(); } }
Code language: CSS (css)

Sử dụng JWT trong Spring Boot

  • Tạo class JwtResponse để cấu hình lại thông điệp trả về sau khi đăng nhập thành công.
package com.example.demo.model; import org.springframework.security.core.GrantedAuthority; import java.util.Collection; public class JwtResponse { private Long id; private String token; private String type = "Bearer"; private String username; private String name; private Collection<? extends GrantedAuthority> roles; public JwtResponse(String accessToken, Long id, String username, String name, Collection<? extends GrantedAuthority> roles) { this.token = accessToken; this.username = username; this.roles = roles; this.name = name; this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getAccessToken() { return token; } public void setAccessToken(String accessToken) { this.token = accessToken; } public String getTokenType() { return type; } public void setTokenType(String tokenType) { this.type = tokenType; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public Collection<? extends GrantedAuthority> getRoles() { return roles; } }
Code language: JavaScript (javascript)
  • Tạo file JwtService để tạo ra token sau khi đăng nhập thành công
package com.example.demo.service; import com.example.demo.model.UserPrinciple; import io.jsonwebtoken.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; import java.util.Date; @Component @Service public class JwtService { private static final String SECRET_KEY = "123456789"; private static final long EXPIRE_TIME = 86400000000L; private static final Logger logger = LoggerFactory.getLogger(JwtService.class.getName()); public String generateTokenLogin(Authentication authentication) { UserPrinciple userPrincipal = (UserPrinciple) authentication.getPrincipal(); return Jwts.builder() .setSubject((userPrincipal.getUsername())) .setIssuedAt(new Date()) .setExpiration(new Date((new Date()).getTime() + EXPIRE_TIME * 1000)) .signWith(SignatureAlgorithm.HS512, SECRET_KEY) .compact(); } public boolean validateJwtToken(String authToken) { try { Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(authToken); return true; } catch (SignatureException e) { logger.error("Invalid JWT signature -> Message: {} ", e); } catch (MalformedJwtException e) { logger.error("Invalid JWT token -> Message: {}", e); } catch (ExpiredJwtException e) { logger.error("Expired JWT token -> Message: {}", e); } catch (UnsupportedJwtException e) { logger.error("Unsupported JWT token -> Message: {}", e); } catch (IllegalArgumentException e) { logger.error("JWT claims string is empty -> Message: {}", e); } return false; } public String getUserNameFromJwtToken(String token) { String userName = Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody().getSubject(); return userName; } }
Code language: JavaScript (javascript)
  • JwtAuthenticationFilter.java
package com.example.demo.configuration.filter; import com.example.demo.service.JwtService; import com.example.demo.service.user.IUserService; 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.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private JwtService jwtService; @Autowired private IUserService userService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { String jwt = getJwtFromRequest(request); if (jwt != null && jwtService.validateJwtToken(jwt)) { String username = jwtService.getUserNameFromJwtToken(jwt); UserDetails userDetails = userService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (Exception e) { logger.error("Can NOT set user authentication -> Message: {}", e); } filterChain.doFilter(request, response); } private String getJwtFromRequest(HttpServletRequest request) { String authHeader = request.getHeader("Authorization"); if (authHeader != null && authHeader.startsWith("Bearer ")) { return authHeader.replace("Bearer ", ""); } return null; } }
Code language: JavaScript (javascript)
  • Cấu hình lại file SecurityConfigure sau khi đủ các thành phần như sau:
package com.example.demo.configuration.security; import com.example.demo.configuration.custom.CustomAccessDeniedHandler; import com.example.demo.configuration.custom.RestAuthenticationEntryPoint; import com.example.demo.configuration.filter.JwtAuthenticationFilter; import com.example.demo.service.user.IUserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.BeanIds; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 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.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private IUserService userService; @Bean public JwtAuthenticationFilter jwtAuthenticationFilter() { return new JwtAuthenticationFilter(); } @Bean(BeanIds.AUTHENTICATION_MANAGER) @Override public AuthenticationManager authenticationManager() throws Exception { return super.authenticationManager(); } @Bean public RestAuthenticationEntryPoint restServicesEntryPoint() { return new RestAuthenticationEntryPoint(); } @Bean public CustomAccessDeniedHandler customAccessDeniedHandler() { return new CustomAccessDeniedHandler(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(10); } @Autowired public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService).passwordEncoder(passwordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().ignoringAntMatchers("/**"); http.httpBasic().authenticationEntryPoint(restServicesEntryPoint()); http.authorizeRequests() .antMatchers("/", "/login").permitAll() .anyRequest().authenticated() .and().csrf().disable(); http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) .exceptionHandling().accessDeniedHandler(customAccessDeniedHandler()); http.sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.cors(); } }
Code language: CSS (css)
  • Tạo class AuthController chưa phương thức POST để đăng nhập
package com.example.demo.controller; import com.example.demo.model.JwtResponse; import com.example.demo.model.User; import com.example.demo.service.JwtService; import com.example.demo.service.user.IUserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @CrossOrigin("*") @RestController public class AuthController { @Autowired private AuthenticationManager authenticationManager; @Autowired private JwtService jwtService; @Autowired private IUserService userService; @PostMapping("/login") public ResponseEntity<?> login(@RequestBody User user) { Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword())); SecurityContextHolder.getContext().setAuthentication(authentication); String jwt = jwtService.generateTokenLogin(authentication); UserDetails userDetails = (UserDetails) authentication.getPrincipal(); User currentUser = userService.findByUsername(user.getUsername()).get(); return ResponseEntity.ok(new JwtResponse(jwt, currentUser.getId(), userDetails.getUsername(), currentUser.getFullName(), userDetails.getAuthorities())); } }
Code language: JavaScript (javascript)
  • Ở class DemoApplication.java mình sẽ viết phương thức init() để tạo sẵn tài khoản trong cơ sở dữ liệu và sử dụng annotation @PostContruct để phương thức này được chạy đầu tiên khi chạy dự án.
package com.example.demo; import com.example.demo.model.Role; import com.example.demo.model.User; import com.example.demo.service.role.IRoleService; import com.example.demo.service.user.IUserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import javax.annotation.PostConstruct; import java.util.HashSet; import java.util.List; import java.util.Set; @SpringBootApplication public class DemoApplication { @Autowired private IUserService userService; @Autowired private IRoleService roleService; public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @PostConstruct public void init() { List<User> users = (List<User>) userService.findAll(); List<Role> roleList = (List<Role>) roleService.findAll(); if (roleList.isEmpty()) { Role roleAdmin = new Role(); roleAdmin.setId(1L); roleAdmin.setName("ROLE_ADMIN"); roleService.save(roleAdmin); Role roleUser = new Role(); roleUser.setId(2L); roleUser.setName("ROLE_USER"); roleService.save(roleUser); } if (users.isEmpty()) { User admin = new User(); Set<Role> roles = new HashSet<>(); Role roleAdmin = new Role(); roleAdmin.setId(1L); roleAdmin.setName("ROLE_ADMIN"); roles.add(roleAdmin); admin.setUsername("admin"); admin.setPassword("123456"); admin.setRoles(roles); userService.save(admin); } } }
Code language: JavaScript (javascript)
  • Chạy dự án và sử dụng POSTMAN để đăng nhập sau khi đăng nhập thành công chúng ta sẽ thu được màn hình như sau trên POSTMAN
JWT

Leave a Reply

Your email address will not be published. Required fields are marked *