auth service

This commit is contained in:
Struchkov Mark
2021-10-14 19:42:03 +03:00
parent 8afcbd2330
commit 6a3ea3f134
18 changed files with 552 additions and 17 deletions

16
pom.xml
View File

@@ -28,10 +28,20 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.4.0-b180830.0359</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
<build>

View File

@@ -0,0 +1,44 @@
package org.sadtech.example.jwt.server.config;
import lombok.RequiredArgsConstructor;
import org.sadtech.example.jwt.server.filter.JwtFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtFilter jwtFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic().disable()
.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/auth/login", "/api/auth/token").permitAll()
.anyRequest().authenticated()
.and()
.addFilterAfter(jwtFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

View File

@@ -0,0 +1,39 @@
package org.sadtech.example.jwt.server.controller;
import lombok.RequiredArgsConstructor;
import org.sadtech.example.jwt.server.domain.JwtRequest;
import org.sadtech.example.jwt.server.domain.JwtResponse;
import org.sadtech.example.jwt.server.domain.RefreshJwtRequest;
import org.sadtech.example.jwt.server.service.AuthService;
import org.springframework.http.ResponseEntity;
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;
@RestController
@RequestMapping("api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
@PostMapping("login")
public ResponseEntity<JwtResponse> login(@RequestBody JwtRequest authRequest) {
final JwtResponse token = authService.login(authRequest);
return ResponseEntity.ok(token);
}
@PostMapping("token")
public ResponseEntity<JwtResponse> getNewAccessToken(@RequestBody RefreshJwtRequest request) {
final JwtResponse token = authService.getAccessToken(request.getRefreshToken());
return ResponseEntity.ok(token);
}
@PostMapping("refresh")
public ResponseEntity<JwtResponse> getNewRefreshToken(@RequestBody RefreshJwtRequest request) {
final JwtResponse token = authService.refresh(request.getRefreshToken());
return ResponseEntity.ok(token);
}
}

View File

@@ -0,0 +1,34 @@
package org.sadtech.example.jwt.server.controller;
import lombok.RequiredArgsConstructor;
import org.sadtech.example.jwt.server.domain.JwtAuthentication;
import org.sadtech.example.jwt.server.service.AuthService;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("api")
@RequiredArgsConstructor
public class Controller {
private final AuthService authService;
@PreAuthorize("hasAuthority('USER')")
@GetMapping("hello/user")
public ResponseEntity<String> helloUser() {
final JwtAuthentication authInfo = authService.getAuthInfo();
return ResponseEntity.ok("Hello user " + authInfo.getPrincipal() + "!");
}
@PreAuthorize("hasAuthority('ADMIN')")
@GetMapping("hello/admin")
public ResponseEntity<String> helloAdmin() {
final JwtAuthentication authInfo = authService.getAuthInfo();
return ResponseEntity.ok("Hello admin " + authInfo.getPrincipal() + "!");
}
}

View File

@@ -0,0 +1,43 @@
package org.sadtech.example.jwt.server.domain;
import lombok.Getter;
import lombok.Setter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
import java.util.Set;
@Getter
@Setter
public class JwtAuthentication implements Authentication {
private boolean authenticated;
private String username;
private String firstName;
private Set<Role> roles;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() { return roles; }
@Override
public Object getCredentials() { return null; }
@Override
public Object getDetails() { return null; }
@Override
public Object getPrincipal() { return username; }
@Override
public boolean isAuthenticated() { return authenticated; }
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
this.authenticated = isAuthenticated;
}
@Override
public String getName() { return firstName; }
}

View File

@@ -0,0 +1,13 @@
package org.sadtech.example.jwt.server.domain;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
public class JwtRequest {
private String login;
private String password;
}

View File

@@ -0,0 +1,14 @@
package org.sadtech.example.jwt.server.domain;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class JwtResponse {
private final String type = "Bearer";
private String accessToken;
private String refreshToken;
}

View File

@@ -0,0 +1,12 @@
package org.sadtech.example.jwt.server.domain;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class RefreshJwtRequest {
public String refreshToken;
}

View File

@@ -0,0 +1,19 @@
package org.sadtech.example.jwt.server.domain;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
@RequiredArgsConstructor
public enum Role implements GrantedAuthority {
ADMIN("ADMIN"),
USER("USER");
private final String vale;
@Override
public String getAuthority() {
return vale;
}
}

View File

@@ -0,0 +1,22 @@
package org.sadtech.example.jwt.server.domain;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.Set;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String login;
private String password;
private String firstName;
private String lastName;
private Set<Role> roles;
}

View File

@@ -0,0 +1,9 @@
package org.sadtech.example.jwt.server.exception;
public class AuthException extends RuntimeException {
public AuthException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,51 @@
package org.sadtech.example.jwt.server.filter;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.sadtech.example.jwt.server.domain.JwtAuthentication;
import org.sadtech.example.jwt.server.service.JwtProvider;
import org.sadtech.example.jwt.server.service.JwtUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
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 java.io.IOException;
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtFilter extends GenericFilterBean {
private static final String AUTHORIZATION = "Authorization";
private final JwtProvider jwtProvider;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain fc)
throws IOException, ServletException {
final String token = getTokenFromRequest((HttpServletRequest) request);
if (token != null && jwtProvider.validateAccessToken(token)) {
final Claims claims = jwtProvider.getAccessClaims(token);
final JwtAuthentication jwtInfoToken = JwtUtils.generate(claims);
jwtInfoToken.setAuthenticated(true);
SecurityContextHolder.getContext().setAuthentication(jwtInfoToken);
}
fc.doFilter(request, response);
}
private String getTokenFromRequest(HttpServletRequest request) {
final String bearer = request.getHeader(AUTHORIZATION);
if (StringUtils.hasText(bearer) && bearer.startsWith("Bearer ")) {
return bearer.substring(7);
}
return null;
}
}

View File

@@ -0,0 +1,74 @@
package org.sadtech.example.jwt.server.service;
import io.jsonwebtoken.Claims;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.sadtech.example.jwt.server.domain.JwtAuthentication;
import org.sadtech.example.jwt.server.domain.JwtRequest;
import org.sadtech.example.jwt.server.domain.JwtResponse;
import org.sadtech.example.jwt.server.domain.User;
import org.sadtech.example.jwt.server.exception.AuthException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
@RequiredArgsConstructor
public class AuthService {
private final UserService userService;
private final Map<String, String> refreshStorage = new HashMap<>();
private final JwtProvider jwtProvider;
public JwtResponse login(@NonNull JwtRequest authRequest) {
final User user = userService.getByLogin(authRequest.getLogin())
.orElseThrow(() -> new AuthException("Пользователь не найден"));
if (user.getPassword().equals(authRequest.getPassword())) {
final String accessToken = jwtProvider.generateAccessToken(user);
final String refreshToken = jwtProvider.generateRefreshToken(user);
refreshStorage.put(user.getLogin(), refreshToken);
return new JwtResponse(accessToken, refreshToken);
} else {
throw new AuthException("Неправильный пароль");
}
}
public JwtResponse getAccessToken(@NonNull String refreshToken) {
if (jwtProvider.validateRefreshToken(refreshToken)) {
final Claims claims = jwtProvider.getRefreshClaims(refreshToken);
final String login = claims.getSubject();
final String saveRefreshToken = refreshStorage.get(login);
if (saveRefreshToken != null && saveRefreshToken.equals(refreshToken)) {
final User user = userService.getByLogin(login)
.orElseThrow(() -> new AuthException("Пользователь не найден"));
final String accessToken = jwtProvider.generateAccessToken(user);
return new JwtResponse(accessToken, null);
}
}
return new JwtResponse(null, null);
}
public JwtResponse refresh(@NonNull String refreshToken) {
if (jwtProvider.validateRefreshToken(refreshToken)) {
final Claims claims = jwtProvider.getRefreshClaims(refreshToken);
final String login = claims.getSubject();
final String saveRefreshToken = refreshStorage.get(login);
if (saveRefreshToken != null && saveRefreshToken.equals(refreshToken)) {
final User user = userService.getByLogin(login)
.orElseThrow(() -> new AuthException("Пользователь не найден"));
final String accessToken = jwtProvider.generateAccessToken(user);
final String newRefreshToken = jwtProvider.generateRefreshToken(user);
refreshStorage.put(user.getLogin(), newRefreshToken);
return new JwtResponse(accessToken, newRefreshToken);
}
}
throw new AuthException("Невалидный JWT токен");
}
public JwtAuthentication getAuthInfo() {
return (JwtAuthentication) SecurityContextHolder.getContext().getAuthentication();
}
}

View File

@@ -0,0 +1,100 @@
package org.sadtech.example.jwt.server.service;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.sadtech.example.jwt.server.domain.User;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
@Slf4j
@Component
public class JwtProvider {
private final String jwtAccessSecret;
private final String jwtRefreshSecret;
public JwtProvider(
@Value("${jwt.secret.access}") String jwtAccessSecret,
@Value("${jwt.secret.refresh}") String jwtRefreshSecret
) {
this.jwtAccessSecret = jwtAccessSecret;
this.jwtRefreshSecret = jwtRefreshSecret;
}
public String generateAccessToken(@NonNull User user) {
final LocalDateTime now = LocalDateTime.now();
final Instant accessExpirationInstant = now.plusMinutes(5).atZone(ZoneId.systemDefault()).toInstant();
final Date accessExpiration = Date.from(accessExpirationInstant);
final String accessToken = Jwts.builder()
.setSubject(user.getLogin())
.setExpiration(accessExpiration)
.signWith(SignatureAlgorithm.HS512, jwtAccessSecret)
.claim("roles", user.getRoles())
.claim("firstName", user.getFirstName())
.compact();
return accessToken;
}
public String generateRefreshToken(@NonNull User user) {
final LocalDateTime now = LocalDateTime.now();
final Instant refreshExpirationInstant = now.plusDays(30).atZone(ZoneId.systemDefault()).toInstant();
final Date refreshExpiration = Date.from(refreshExpirationInstant);
final String refreshToken = Jwts.builder()
.setSubject(user.getLogin())
.setExpiration(refreshExpiration)
.signWith(SignatureAlgorithm.HS512, jwtRefreshSecret)
.compact();
return refreshToken;
}
public boolean validateAccessToken(@NonNull String token) {
return validateToken(token, jwtAccessSecret);
}
public boolean validateRefreshToken(@NonNull String token) {
return validateToken(token, jwtRefreshSecret);
}
private boolean validateToken(@NonNull String token, @NonNull String secret) {
try {
Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
return true;
} catch (ExpiredJwtException expEx) {
log.error("Token expired", expEx);
} catch (UnsupportedJwtException unsEx) {
log.error("Unsupported jwt", unsEx);
} catch (MalformedJwtException mjEx) {
log.error("Malformed jwt", mjEx);
} catch (SignatureException sEx) {
log.error("Invalid signature", sEx);
} catch (Exception e) {
log.error("invalid token", e);
}
return false;
}
public Claims getAccessClaims(@NonNull String token) {
return getClaims(token, jwtAccessSecret);
}
public Claims getRefreshClaims(@NonNull String token) {
return getClaims(token, jwtRefreshSecret);
}
private Claims getClaims(@NonNull String token, @NonNull String secret) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
}

View File

@@ -0,0 +1,31 @@
package org.sadtech.example.jwt.server.service;
import io.jsonwebtoken.Claims;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.sadtech.example.jwt.server.domain.JwtAuthentication;
import org.sadtech.example.jwt.server.domain.Role;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class JwtUtils {
public static JwtAuthentication generate(Claims claims) {
final JwtAuthentication jwtInfoToken = new JwtAuthentication();
jwtInfoToken.setRoles(getRoles(claims));
jwtInfoToken.setFirstName(claims.get("firstName", String.class));
jwtInfoToken.setUsername(claims.getSubject());
return jwtInfoToken;
}
private static Set<Role> getRoles(Claims claims) {
final List<String> roles = claims.get("roles", List.class);
return roles.stream()
.map(Role::valueOf)
.collect(Collectors.toSet());
}
}

View File

@@ -0,0 +1,32 @@
package org.sadtech.example.jwt.server.service;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.sadtech.example.jwt.server.domain.Role;
import org.sadtech.example.jwt.server.domain.User;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@Service
@RequiredArgsConstructor
public class UserService {
private final List<User> users;
public UserService() {
this.users = List.of(
new User("anton", "1234", "Антон", "Иванов", Collections.singleton(Role.USER)),
new User("ivan", "12345", "Сергей", "Петров", Collections.singleton(Role.ADMIN))
);
}
public Optional<User> getByLogin(@NonNull String login) {
return users.stream()
.filter(user -> login.equals(user.getLogin()))
.findFirst();
}
}

View File

@@ -1 +1,2 @@
jwt.secret.access=supermegasecret
jwt.secret.refresh=supermegarefreshsecret

View File

@@ -1,13 +0,0 @@
package org.sadtech.example.jwt.server;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class ServerJwtApplicationTests {
@Test
void contextLoads() {
}
}