diff --git a/api-security.properties b/api-security.properties new file mode 100644 index 000000000..2e2e4c5e3 --- /dev/null +++ b/api-security.properties @@ -0,0 +1,2 @@ +jwt.token.secret=jwtcrmsecret +jwt.token.expired=3600000 \ No newline at end of file diff --git a/pom.xml b/pom.xml index 0654a9cc2..6c6ecab6e 100644 --- a/pom.xml +++ b/pom.xml @@ -46,6 +46,7 @@ 4.1.0 1.7 5.3.2 + 0.8.0 @@ -356,6 +357,13 @@ test + + + io.jsonwebtoken + jjwt + ${jsonwebtoken.version} + + diff --git a/src/main/java/com/ewp/crm/controllers/rest/api/AuthApiController.java b/src/main/java/com/ewp/crm/controllers/rest/api/AuthApiController.java new file mode 100644 index 000000000..ed04fbb56 --- /dev/null +++ b/src/main/java/com/ewp/crm/controllers/rest/api/AuthApiController.java @@ -0,0 +1,56 @@ +package com.ewp.crm.controllers.rest.api; + +/** + * Подробное описание доступа к api см. в классе ApiSecurityConfig + */ + +import com.ewp.crm.models.User; +import com.ewp.crm.models.dto.AuthenticationRequestDto; +import com.ewp.crm.security.jwt.JwtTokenProvider; +import com.ewp.crm.service.interfaces.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.AuthenticationException; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping(value = "/rest/api") +public class AuthApiController { + private final UserService userService; + private final JwtTokenProvider jwtTokenProvider; + private final AuthenticationManager authenticationManager; + + @Autowired + public AuthApiController(UserService userService, JwtTokenProvider jwtTokenProvider, AuthenticationManager authenticationManager) { + this.userService = userService; + this.jwtTokenProvider = jwtTokenProvider; + this.authenticationManager = authenticationManager; + } + + @PostMapping("/login") + public ResponseEntity login(@RequestBody AuthenticationRequestDto authReqDto) { + + String email = authReqDto.getEmail(); + + try { + authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(email, authReqDto.getPassword())); + User user = userService.getUserByEmail(email).get(); + + String token = jwtTokenProvider.createToken(email, user.getRole()); + Map response = new HashMap<>(); + response.put("email", email); + response.put("token", token); + + return ResponseEntity.ok(response); + + } catch (AuthenticationException e) { + throw new BadCredentialsException("Invalid username or password"); + } + } +} diff --git a/src/main/java/com/ewp/crm/controllers/rest/api/StatusApiRestController.java b/src/main/java/com/ewp/crm/controllers/rest/api/StatusApiRestController.java index 8faf56470..30c3ed962 100644 --- a/src/main/java/com/ewp/crm/controllers/rest/api/StatusApiRestController.java +++ b/src/main/java/com/ewp/crm/controllers/rest/api/StatusApiRestController.java @@ -31,7 +31,6 @@ public ResponseEntity addStatus(@PathVariable String statusName) { } //TODO Понять откуда сюда приходит запрос - System.out.println("Я запрос! Я пришел!"); Status status = new Status(statusName, 1L); statusService.add(status); diff --git a/src/main/java/com/ewp/crm/models/dto/AuthenticationRequestDto.java b/src/main/java/com/ewp/crm/models/dto/AuthenticationRequestDto.java new file mode 100644 index 000000000..f48587fa9 --- /dev/null +++ b/src/main/java/com/ewp/crm/models/dto/AuthenticationRequestDto.java @@ -0,0 +1,35 @@ +package com.ewp.crm.models.dto; + +public class AuthenticationRequestDto { + private String email; + private String password; + + public AuthenticationRequestDto() { + } + + public AuthenticationRequestDto(String email, String password) { + this.email = email; + this.password = password; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + @Override + public String toString() { + return "Login: " + email + " Password: " + password; + } +} diff --git a/src/main/java/com/ewp/crm/security/config/ApiSecurityConfig.java b/src/main/java/com/ewp/crm/security/config/ApiSecurityConfig.java new file mode 100644 index 000000000..8f838c712 --- /dev/null +++ b/src/main/java/com/ewp/crm/security/config/ApiSecurityConfig.java @@ -0,0 +1,64 @@ +package com.ewp.crm.security.config; + +/** + * Доступ к api сделан на основе jwt. + * Для того чтобы получить доступ к данным необходимо обратиться на /rest/api/login (см. AuthApiController) + * и передать в теле запроса данные пользователя и пароль: + * { + * "email":"user_email@gmail.com", + * "password":"123456" + * } + * В ответ придет токен доступа, которым необходимо подписать каждый запрос к /rest/api/**. В заголовок запроса + * нужно вставить поле Authorization со значением Bearer_T (вместо Т подставить значение токена) + */ + +import com.ewp.crm.security.jwt.JwtTokenFilter; +import com.ewp.crm.security.jwt.JwtTokenProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.security.authentication.AuthenticationManager; +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.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@Order(1) +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class ApiSecurityConfig extends WebSecurityConfigurerAdapter { + private final JwtTokenProvider jwtTokenProvider; + + @Bean + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + @Autowired + public ApiSecurityConfig(JwtTokenProvider jwtTokenProvider) { + this.jwtTokenProvider = jwtTokenProvider; + } + + protected void configure(HttpSecurity http) throws Exception { + JwtTokenFilter jwtTokenFilter = new JwtTokenFilter(jwtTokenProvider); + + http + .antMatcher("/rest/api/**") + .authorizeRequests() + .antMatchers("/rest/api/login").permitAll() + .antMatchers("/rest/api/**").hasAnyAuthority("ADMIN", "OWNER") + .and() + .httpBasic().disable() + .csrf().disable() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class); + + } + +} diff --git a/src/main/java/com/ewp/crm/security/jwt/JwtAuthException.java b/src/main/java/com/ewp/crm/security/jwt/JwtAuthException.java new file mode 100644 index 000000000..66fe1ca9f --- /dev/null +++ b/src/main/java/com/ewp/crm/security/jwt/JwtAuthException.java @@ -0,0 +1,14 @@ +package com.ewp.crm.security.jwt; + + +import org.springframework.security.core.AuthenticationException; + +public class JwtAuthException extends AuthenticationException { + public JwtAuthException(String msg) { + super(msg); + } + + public JwtAuthException(String msg, Throwable t) { + super(msg, t); + } +} diff --git a/src/main/java/com/ewp/crm/security/jwt/JwtTokenFilter.java b/src/main/java/com/ewp/crm/security/jwt/JwtTokenFilter.java new file mode 100644 index 000000000..886db69ec --- /dev/null +++ b/src/main/java/com/ewp/crm/security/jwt/JwtTokenFilter.java @@ -0,0 +1,36 @@ +package com.ewp.crm.security.jwt; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +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 java.io.IOException; + +public class JwtTokenFilter extends GenericFilterBean { + + private JwtTokenProvider jwtTokenProvider; + + @Autowired + public JwtTokenFilter(JwtTokenProvider jwtTokenProvider) { + this.jwtTokenProvider = jwtTokenProvider; + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + + String token = jwtTokenProvider.resolveToken((HttpServletRequest) servletRequest); + if(token != null && jwtTokenProvider.validateToken(token)){ + Authentication authentication = jwtTokenProvider.getAuthentication(token); + if(authentication != null) { + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } + filterChain.doFilter(servletRequest, servletResponse); + } +} diff --git a/src/main/java/com/ewp/crm/security/jwt/JwtTokenProvider.java b/src/main/java/com/ewp/crm/security/jwt/JwtTokenProvider.java new file mode 100644 index 000000000..35ba84717 --- /dev/null +++ b/src/main/java/com/ewp/crm/security/jwt/JwtTokenProvider.java @@ -0,0 +1,102 @@ +package com.ewp.crm.security.jwt; + +import com.ewp.crm.models.Role; +import com.ewp.crm.security.service.AuthenticationService; +import io.jsonwebtoken.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.PropertySource; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.servlet.http.HttpServletRequest; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Date; +import java.util.List; + +@Component +@PropertySource("file:./api-security.properties") +public class JwtTokenProvider { + + @Value("${jwt.token.secret}") + private String secret; + + @Value("${jwt.token.expired}") + private Long validityInMilliSeconds; + + private AuthenticationService userDetailsService; + + @Autowired + public JwtTokenProvider(AuthenticationService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + @Bean + public BCryptPasswordEncoder passwordEncoder() { + BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); + return bCryptPasswordEncoder; + } + + @PostConstruct + protected void init() { + secret = Base64.getEncoder().encodeToString(secret.getBytes()); + } + + public String createToken(String userName, List roles){ + Claims claims = Jwts.claims().setSubject(userName); + claims.put("roles", getRoleNames(roles)); + + Date now = new Date(); + Date validity = new Date(now.getTime() + validityInMilliSeconds); + + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(validity) + .signWith(SignatureAlgorithm.HS256, secret) + .compact(); + } + + public Authentication getAuthentication(String token) { + UserDetails userDetails = this.userDetailsService.loadUserByUsername(getUserName(token)); + return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); + } + + public String getUserName(String token) { + return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody().getSubject(); + } + + public String resolveToken(HttpServletRequest req) { + String bearerToken = req.getHeader("Authorization"); + if (bearerToken != null && bearerToken.startsWith("Bearer_")) { + return bearerToken.substring(7); + } + return null; + } + + public boolean validateToken(String token){ + try { + Jws claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token); + + if (claims.getBody().getExpiration().before(new Date())) { + return false; + } + + return true; + } catch (JwtException | IllegalArgumentException e) { + throw new JwtAuthException("JWT token is expired or invalid"); + } + } + + private List getRoleNames(List userRoles) { + List result = new ArrayList<>(); + userRoles.forEach(role -> result.add(role.getRoleName())); + return result; + } +}