diff --git a/frontend/src/components/Login.vue b/frontend/src/components/Login.vue
new file mode 100644
index 0000000..bd68adc
--- /dev/null
+++ b/frontend/src/components/Login.vue
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ error }}
+
+
+
+ Регистрация
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/components/Signup.vue b/frontend/src/components/Signup.vue
new file mode 100644
index 0000000..25b1a74
--- /dev/null
+++ b/frontend/src/components/Signup.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ error }}
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/main.js b/frontend/src/main.js
index aafcf65..1dad12d 100644
--- a/frontend/src/main.js
+++ b/frontend/src/main.js
@@ -3,20 +3,24 @@ import App from './App'
import { createRouter, createWebHistory } from "vue-router"
import Customers from './components/Customers'
-import Posts from './components/Posts'
+import Feed from './components/Feed'
+import Login from "@/components/Login.vue";
const routes = [
{
path: '/customers/:id?',
name: "Customers",
- component: Customers,
- props: true
+ component: Customers
},
{
- path: '/posts',
- name: "Posts",
- component: Posts,
- props: true
+ path: '/feed',
+ name: "Feed",
+ component: Feed
+ },
+ {
+ path: '/login',
+ name: "Login",
+ component: Login
}
]
diff --git a/src/main/java/np/something/Exceptions/JwtException.java b/src/main/java/np/something/Exceptions/JwtException.java
new file mode 100644
index 0000000..5d55c1e
--- /dev/null
+++ b/src/main/java/np/something/Exceptions/JwtException.java
@@ -0,0 +1,11 @@
+package np.something.Exceptions;
+
+public class JwtException extends RuntimeException {
+ public JwtException(Throwable throwable) {
+ super(throwable);
+ }
+
+ public JwtException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/np/something/OpenAPI30Configuration.java b/src/main/java/np/something/OpenAPI30Configuration.java
new file mode 100644
index 0000000..26bb817
--- /dev/null
+++ b/src/main/java/np/something/OpenAPI30Configuration.java
@@ -0,0 +1,28 @@
+package np.something;
+
+import io.swagger.v3.oas.models.Components;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.security.SecurityRequirement;
+import io.swagger.v3.oas.models.security.SecurityScheme;
+import np.something.security.JwtFilter;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class OpenAPI30Configuration {
+ public static final String API_PREFIX = "/api/1.0";
+
+ @Bean
+ public OpenAPI customizeOpenAPI() {
+ final String securitySchemeName = JwtFilter.TOKEN_BEGIN_STR;
+ return new OpenAPI()
+ .addSecurityItem(new SecurityRequirement()
+ .addList(securitySchemeName))
+ .components(new Components()
+ .addSecuritySchemes(securitySchemeName, new SecurityScheme()
+ .name(securitySchemeName)
+ .type(SecurityScheme.Type.HTTP)
+ .scheme("bearer")
+ .bearerFormat("JWT")));
+ }
+}
diff --git a/src/main/java/np/something/WebConfiguration.java b/src/main/java/np/something/WebConfiguration.java
index 3a1e562..8f62a14 100644
--- a/src/main/java/np/something/WebConfiguration.java
+++ b/src/main/java/np/something/WebConfiguration.java
@@ -1,18 +1,31 @@
package np.something;
+import np.something.security.SecurityConfiguration;
+import org.springframework.boot.web.server.ErrorPage;
+import org.springframework.boot.web.server.WebServerFactoryCustomizer;
+import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
+import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
- public static final String REST_API = "/api";
+ public static final String REST_API = OpenAPI30Configuration.API_PREFIX;
@Override
public void addViewControllers(ViewControllerRegistry registry) {
WebMvcConfigurer.super.addViewControllers(registry);
registry.addViewController("login");
+ registry.addViewController(SecurityConfiguration.SPA_URL_MASK).setViewName("forward:/");
+ registry.addViewController("/notFound").setViewName("forward:/");
+ }
+
+ @Bean
+ public WebServerFactoryCustomizer
containerCustomizer() {
+ return container -> container.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/notFound"));
}
@Override
diff --git a/src/main/java/np/something/controllers/CustomerController.java b/src/main/java/np/something/controllers/CustomerController.java
index 29e8bba..7d75805 100644
--- a/src/main/java/np/something/controllers/CustomerController.java
+++ b/src/main/java/np/something/controllers/CustomerController.java
@@ -5,6 +5,7 @@ import np.something.DTO.CustomerDto;
import np.something.WebConfiguration;
import np.something.model.Customer;
import np.something.services.*;
+import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@@ -12,6 +13,7 @@ import java.util.List;
@RestController
@RequestMapping(WebConfiguration.REST_API + "/customer")
public class CustomerController {
+ public static final String URL_LOGIN = "/jwt/login";
private final CustomerService customerService;
public CustomerController(CustomerService customerService) {
@@ -50,4 +52,31 @@ public class CustomerController {
public void deleteAllCustomers(){
customerService.deleteAllCustomers();
}
+
+ @GetMapping("/find/{username}")
+ public CustomerDto getCustomerByUsername(@PathVariable String username) {
+ return new CustomerDto(customerService.findByUsername(username));
+ }
+
+ @PostMapping(URL_LOGIN)
+ public String login(@RequestBody @Valid CustomerDto customerDto) {
+ return customerService.loginAndGetToken(customerDto);
+ }
+
+ @GetMapping ("/me")
+ CustomerDto getCurrentCustomer(@RequestHeader(HttpHeaders.AUTHORIZATION) String token) {
+ return new CustomerDto(customerService.findByUsername(customerService.loadUserByToken(token).getUsername()));
+ }
+
+ @GetMapping("role/{token}")
+ public String getRoleByToken(@PathVariable String token) {
+ var userDetails = customerService.loadUserByToken(token);
+ Customer customer = customerService.findByUsername(userDetails.getUsername());
+
+ if (customer != null) {
+ return customer.getRole().toString();
+ }
+
+ return null;
+ }
}
diff --git a/src/main/java/np/something/security/JwtFilter.java b/src/main/java/np/something/security/JwtFilter.java
new file mode 100644
index 0000000..bed1ee8
--- /dev/null
+++ b/src/main/java/np/something/security/JwtFilter.java
@@ -0,0 +1,73 @@
+package np.something.security;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import np.something.Exceptions.JwtException;
+import np.something.services.CustomerService;
+import org.springframework.http.MediaType;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.util.StringUtils;
+import org.springframework.web.filter.GenericFilterBean;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+public class JwtFilter extends GenericFilterBean {
+ private static final String AUTHORIZATION = "Authorization";
+ public static final String TOKEN_BEGIN_STR = "Bearer ";
+
+ private final CustomerService customerService;
+
+ public JwtFilter(CustomerService customerService) {
+ this.customerService = customerService;
+ }
+
+ private String getTokenFromRequest(HttpServletRequest request) {
+ String bearer = request.getHeader(AUTHORIZATION);
+ if (StringUtils.hasText(bearer) && bearer.startsWith(TOKEN_BEGIN_STR)) {
+ return bearer.substring(TOKEN_BEGIN_STR.length());
+ }
+ return null;
+ }
+
+ private void raiseException(ServletResponse response, int status, String message) throws IOException {
+ if (response instanceof final HttpServletResponse httpResponse) {
+ httpResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
+ httpResponse.setStatus(status);
+ final byte[] body = new ObjectMapper().writeValueAsBytes(message);
+ response.getOutputStream().write(body);
+ }
+ }
+
+ @Override
+ public void doFilter(ServletRequest request,
+ ServletResponse response,
+ FilterChain chain) throws IOException, ServletException {
+ if (request instanceof final HttpServletRequest httpRequest) {
+ final String token = getTokenFromRequest(httpRequest);
+ if (StringUtils.hasText(token)) {
+ try {
+ final UserDetails user = customerService.loadUserByToken(token);
+ final UsernamePasswordAuthenticationToken auth =
+ new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
+ SecurityContextHolder.getContext().setAuthentication(auth);
+ } catch (JwtException e) {
+ raiseException(response, HttpServletResponse.SC_UNAUTHORIZED, e.getMessage());
+ return;
+ } catch (Exception e) {
+ e.printStackTrace();
+ raiseException(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+ String.format("Internal error: %s", e.getMessage()));
+ return;
+ }
+ }
+ }
+ chain.doFilter(request, response);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/np/something/security/JwtProperties.java b/src/main/java/np/something/security/JwtProperties.java
new file mode 100644
index 0000000..cbb2497
--- /dev/null
+++ b/src/main/java/np/something/security/JwtProperties.java
@@ -0,0 +1,27 @@
+package np.something.security;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@ConfigurationProperties(prefix = "jwt", ignoreInvalidFields = true)
+public class JwtProperties {
+ private String devToken = "";
+ private Boolean isDev = true;
+
+ public String getDevToken() {
+ return devToken;
+ }
+
+ public void setDevToken(String devToken) {
+ this.devToken = devToken;
+ }
+
+ public Boolean isDev() {
+ return isDev;
+ }
+
+ public void setDev(Boolean dev) {
+ isDev = dev;
+ }
+}
diff --git a/src/main/java/np/something/security/JwtProvider.java b/src/main/java/np/something/security/JwtProvider.java
new file mode 100644
index 0000000..82a7443
--- /dev/null
+++ b/src/main/java/np/something/security/JwtProvider.java
@@ -0,0 +1,108 @@
+package np.something.security;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.algorithms.Algorithm;
+import com.auth0.jwt.exceptions.JWTVerificationException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+import com.auth0.jwt.interfaces.JWTVerifier;
+import np.something.Exceptions.JwtException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.Date;
+import java.util.Optional;
+import java.util.UUID;
+
+@Component
+public class JwtProvider {
+ private final static Logger LOG = LoggerFactory.getLogger(JwtProvider.class);
+
+ private final static byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII);
+ private final static String ISSUER = "auth0";
+
+ private final Algorithm algorithm;
+ private final JWTVerifier verifier;
+
+ public JwtProvider(JwtProperties jwtProperties) {
+ if (!jwtProperties.isDev()) {
+ LOG.info("Generate new JWT key for prod");
+ try {
+ final MessageDigest salt = MessageDigest.getInstance("SHA-256");
+ salt.update(UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8));
+ LOG.info("Use generated JWT key for prod \n{}", bytesToHex(salt.digest()));
+ algorithm = Algorithm.HMAC256(bytesToHex(salt.digest()));
+ } catch (NoSuchAlgorithmException e) {
+ throw new JwtException(e);
+ }
+ } else {
+ LOG.info("Use default JWT key for dev \n{}", jwtProperties.getDevToken());
+ algorithm = Algorithm.HMAC256(jwtProperties.getDevToken());
+ }
+ verifier = JWT.require(algorithm)
+ .withIssuer(ISSUER)
+ .build();
+ }
+
+ private static String bytesToHex(byte[] bytes) {
+ byte[] hexChars = new byte[bytes.length * 2];
+ for (int j = 0; j < bytes.length; j++) {
+ int v = bytes[j] & 0xFF;
+ hexChars[j * 2] = HEX_ARRAY[v >>> 4];
+ hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
+ }
+ return new String(hexChars, StandardCharsets.UTF_8);
+ }
+
+ public String generateToken(String login) {
+ final Date issueDate = Date.from(LocalDate.now()
+ .atStartOfDay(ZoneId.systemDefault())
+ .toInstant());
+ final Date expireDate = Date.from(LocalDate.now()
+ .plusDays(15)
+ .atStartOfDay(ZoneId.systemDefault())
+ .toInstant());
+ return JWT.create()
+ .withIssuer(ISSUER)
+ .withIssuedAt(issueDate)
+ .withExpiresAt(expireDate)
+ .withSubject(login)
+ .sign(algorithm);
+ }
+
+ private DecodedJWT validateToken(String token) {
+ try {
+ return verifier.verify(token);
+ } catch (JWTVerificationException e) {
+ throw new JwtException(String.format("Token verification error: %s", e.getMessage()));
+ }
+ }
+
+ public boolean isTokenValid(String token) {
+ if (!StringUtils.hasText(token)) {
+ return false;
+ }
+ try {
+ validateToken(token);
+ return true;
+ } catch (JwtException e) {
+ LOG.error(e.getMessage());
+ return false;
+ }
+ }
+
+ public Optional getLoginFromToken(String token) {
+ try {
+ return Optional.ofNullable(validateToken(token).getSubject());
+ } catch (JwtException e) {
+ LOG.error(e.getMessage());
+ return Optional.empty();
+ }
+ }
+}
diff --git a/src/main/java/np/something/security/SecurityConfiguration.java b/src/main/java/np/something/security/SecurityConfiguration.java
index ca63111..978bef6 100644
--- a/src/main/java/np/something/security/SecurityConfiguration.java
+++ b/src/main/java/np/something/security/SecurityConfiguration.java
@@ -1,5 +1,7 @@
package np.something.security;
+import np.something.WebConfiguration;
+import np.something.controllers.CustomerController;
import np.something.model.UserRole;
import np.something.mvc.UserSignUp;
import np.something.services.CustomerService;
@@ -13,6 +15,8 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
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.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@@ -20,10 +24,13 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final Logger log = LoggerFactory.getLogger(SecurityConfiguration.class);
private static final String LOGIN_URL = "/login";
+ public static final String SPA_URL_MASK = "/{path:[^\\.]*}";
private final CustomerService customerService;
+ private final JwtFilter jwtFilter;
public SecurityConfiguration(CustomerService customerService) {
this.customerService = customerService;
+ this.jwtFilter = new JwtFilter(customerService);
createAdminOnStartup();
}
@@ -35,20 +42,38 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
}
}
+// @Override
+// protected void configure(HttpSecurity http) throws Exception {
+// http.headers().frameOptions().sameOrigin().and()
+// .cors().and()
+// .csrf().disable()
+// .authorizeRequests()
+// .antMatchers(UserSignUp.SIGNUP_URL).permitAll()
+// .antMatchers(HttpMethod.GET, LOGIN_URL).permitAll()
+// .anyRequest().authenticated()
+// .and()
+// .formLogin()
+// .loginPage(LOGIN_URL).permitAll()
+// .and()
+// .logout().permitAll();
+// }
+
@Override
protected void configure(HttpSecurity http) throws Exception {
- http.headers().frameOptions().sameOrigin().and()
- .cors().and()
+ log.info("Creating security configuration");
+ http.cors()
+ .and()
.csrf().disable()
+ .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+ .and()
.authorizeRequests()
- .antMatchers(UserSignUp.SIGNUP_URL).permitAll()
- .antMatchers(HttpMethod.GET, LOGIN_URL).permitAll()
- .anyRequest().authenticated()
+ .antMatchers("/", SPA_URL_MASK).permitAll()
+ .antMatchers(HttpMethod.POST, WebConfiguration.REST_API + "/customer" + CustomerController.URL_LOGIN).permitAll()
+ .anyRequest()
+ .authenticated()
.and()
- .formLogin()
- .loginPage(LOGIN_URL).permitAll()
- .and()
- .logout().permitAll();
+ .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
+ .anonymous();
}
@Override
@@ -62,6 +87,8 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
.antMatchers("/css/**")
.antMatchers("/js/**")
.antMatchers("/templates/**")
- .antMatchers("/webjars/**");
+ .antMatchers("/webjars/**")
+ .antMatchers("/swagger-resources/**")
+ .antMatchers("/v3/api-docs/**");
}
}
\ No newline at end of file
diff --git a/src/main/java/np/something/services/CustomerService.java b/src/main/java/np/something/services/CustomerService.java
index 3027252..52f71b2 100644
--- a/src/main/java/np/something/services/CustomerService.java
+++ b/src/main/java/np/something/services/CustomerService.java
@@ -1,10 +1,14 @@
package np.something.services;
import javax.transaction.Transactional;
+
+import np.something.DTO.CustomerDto;
import np.something.Exceptions.CustomerNotFoundException;
+import np.something.Exceptions.JwtException;
import np.something.model.Customer;
import np.something.model.UserRole;
import np.something.repositories.CustomerRepository;
+import np.something.security.JwtProvider;
import np.something.util.validation.ValidatorUtil;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
@@ -20,12 +24,14 @@ public class CustomerService implements UserDetailsService {
private final CustomerRepository customerRepository;
private final ValidatorUtil validatorUtil;
private final PasswordEncoder passwordEncoder;
+ private final JwtProvider jwtProvider;
public CustomerService(CustomerRepository customerRepository,
- ValidatorUtil validatorUtil, PasswordEncoder passwordEncoder) {
+ ValidatorUtil validatorUtil, PasswordEncoder passwordEncoder, JwtProvider jwtProvider) {
this.customerRepository = customerRepository;
this.validatorUtil = validatorUtil;
this.passwordEncoder = passwordEncoder;
+ this.jwtProvider = jwtProvider;
}
@Transactional
@@ -86,4 +92,29 @@ public class CustomerService implements UserDetailsService {
return new org.springframework.security.core.userdetails.User(
customerEntity.getUsername(), customerEntity.getPassword(), Collections.singleton(customerEntity.getRole()));
}
+
+ public String loginAndGetToken(CustomerDto customerDto) {
+ try {
+ final Customer customer = findByUsername(customerDto.getUsername());
+ if (customer == null) {
+ throw new Exception("Login not found" + customerDto.getUsername());
+ }
+ if (!passwordEncoder.matches(customerDto.getPassword(), customer.getPassword())) {
+ throw new Exception("User not found" + customer.getUsername());
+ }
+ return jwtProvider.generateToken(customer.getUsername());
+ }
+ catch (Exception e) {
+ return null;
+ }
+ }
+
+ public UserDetails loadUserByToken(String token) throws UsernameNotFoundException {
+ if (!jwtProvider.isTokenValid(token)) {
+ throw new JwtException("Bad token");
+ }
+ final String userLogin = jwtProvider.getLoginFromToken(token)
+ .orElseThrow(() -> new JwtException("Token is not contain Login"));
+ return loadUserByUsername(userLogin);
+ }
}
diff --git a/src/main/resources/templates/customers.html b/src/main/resources/templates/customers.html
index cac7b24..58c4666 100644
--- a/src/main/resources/templates/customers.html
+++ b/src/main/resources/templates/customers.html
@@ -48,26 +48,6 @@