refactoring

This commit is contained in:
Ardiansyah
2021-05-27 22:33:19 +07:00
parent f3fd409bdc
commit 8a9dabc6e1
14 changed files with 127 additions and 130 deletions

View File

@@ -3,13 +3,10 @@ package com.ard333.springbootwebfluxjjwt.model;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.ToString;
/** @Data
* @NoArgsConstructor
* @author ard333 @AllArgsConstructor
*/
@Data @NoArgsConstructor @AllArgsConstructor @ToString
public class Message { public class Message {
private String content; private String content;
} }

View File

@@ -1,8 +1,9 @@
package com.ard333.springbootwebfluxjjwt.model; package com.ard333.springbootwebfluxjjwt.model;
import com.ard333.springbootwebfluxjjwt.model.security.Role;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.ard333.springbootwebfluxjjwt.security.model.Role;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
@@ -15,24 +16,27 @@ import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import lombok.ToString; import lombok.ToString;
/** @ToString
* @NoArgsConstructor
* @author ard333 @AllArgsConstructor
*/
@ToString @AllArgsConstructor @NoArgsConstructor
public class User implements UserDetails { public class User implements UserDetails {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private String username; private String username;
private String password; private String password;
@Getter @Setter @Getter @Setter
private Boolean enabled; private Boolean enabled;
@Getter @Setter @Getter @Setter
private List<Role> roles; private List<Role> roles;
@Override @Override
public String getUsername() { public String getUsername() {
return username; return username;
} }
public void setUsername(String username) { public void setUsername(String username) {
this.username = username; this.username = username;
} }
@@ -67,6 +71,7 @@ public class User implements UserDetails {
public String getPassword() { public String getPassword() {
return password; return password;
} }
@JsonProperty @JsonProperty
public void setPassword(String password) { public void setPassword(String password) {
this.password = password; this.password = password;

View File

@@ -0,0 +1,14 @@
package com.ard333.springbootwebfluxjjwt.model.security;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AuthRequest {
private String username;
private String password;
}

View File

@@ -0,0 +1,13 @@
package com.ard333.springbootwebfluxjjwt.model.security;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AuthResponse {
private String token;
}

View File

@@ -0,0 +1,5 @@
package com.ard333.springbootwebfluxjjwt.model.security;
public enum Role {
ROLE_USER, ROLE_ADMIN
}

View File

@@ -1,43 +1,33 @@
package com.ard333.springbootwebfluxjjwt.rest; package com.ard333.springbootwebfluxjjwt.rest;
import com.ard333.springbootwebfluxjjwt.model.security.AuthRequest;
import com.ard333.springbootwebfluxjjwt.model.security.AuthResponse;
import com.ard333.springbootwebfluxjjwt.security.JWTUtil; import com.ard333.springbootwebfluxjjwt.security.JWTUtil;
import com.ard333.springbootwebfluxjjwt.security.PBKDF2Encoder; import com.ard333.springbootwebfluxjjwt.security.PBKDF2Encoder;
import com.ard333.springbootwebfluxjjwt.security.model.AuthRequest;
import com.ard333.springbootwebfluxjjwt.security.model.AuthResponse;
import com.ard333.springbootwebfluxjjwt.service.UserService; import com.ard333.springbootwebfluxjjwt.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; 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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import lombok.AllArgsConstructor;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
/** @AllArgsConstructor
*
* @author ard333
*/
@RestController @RestController
public class AuthenticationREST { public class AuthenticationREST {
@Autowired
private JWTUtil jwtUtil; private JWTUtil jwtUtil;
@Autowired
private PBKDF2Encoder passwordEncoder; private PBKDF2Encoder passwordEncoder;
@Autowired
private UserService userService; private UserService userService;
@RequestMapping(value = "/login", method = RequestMethod.POST) @PostMapping("/login")
public Mono<ResponseEntity<?>> login(@RequestBody AuthRequest ar) { public Mono<ResponseEntity<AuthResponse>> login(@RequestBody AuthRequest ar) {
return userService.findByUsername(ar.getUsername()).map((userDetails) -> { return userService.findByUsername(ar.getUsername())
if (passwordEncoder.encode(ar.getPassword()).equals(userDetails.getPassword())) { .filter(userDetails -> passwordEncoder.encode(ar.getPassword()).equals(userDetails.getPassword()))
return ResponseEntity.ok(new AuthResponse(jwtUtil.generateToken(userDetails))); .map(userDetails -> ResponseEntity.ok(new AuthResponse(jwtUtil.generateToken(userDetails))))
} else { .switchIfEmpty(Mono.just(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()));
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}).defaultIfEmpty(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build());
} }
} }

View File

@@ -3,30 +3,28 @@ package com.ard333.springbootwebfluxjjwt.rest;
import com.ard333.springbootwebfluxjjwt.model.Message; import com.ard333.springbootwebfluxjjwt.model.Message;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
/**
*
* @author ardiansyah
*/
@RestController @RestController
public class ResourceREST { public class ResourceREST {
@RequestMapping(value = "/resource/user", method = RequestMethod.GET)
@GetMapping("/resource/user")
@PreAuthorize("hasRole('USER')") @PreAuthorize("hasRole('USER')")
public Mono<ResponseEntity<?>> user() { public Mono<ResponseEntity<Message>> user() {
return Mono.just(ResponseEntity.ok(new Message("Content for user"))); return Mono.just(ResponseEntity.ok(new Message("Content for user")));
} }
@RequestMapping(value = "/resource/admin", method = RequestMethod.GET)
@GetMapping("/resource/admin")
@PreAuthorize("hasRole('ADMIN')") @PreAuthorize("hasRole('ADMIN')")
public Mono<ResponseEntity<?>> admin() { public Mono<ResponseEntity<Message>> admin() {
return Mono.just(ResponseEntity.ok(new Message("Content for admin"))); return Mono.just(ResponseEntity.ok(new Message("Content for admin")));
} }
@RequestMapping(value = "/resource/user-or-admin", method = RequestMethod.GET)
@GetMapping("/resource/user-or-admin")
@PreAuthorize("hasRole('USER') or hasRole('ADMIN')") @PreAuthorize("hasRole('USER') or hasRole('ADMIN')")
public Mono<ResponseEntity<?>> userOrAdmin() { public Mono<ResponseEntity<Message>> userOrAdmin() {
return Mono.just(ResponseEntity.ok(new Message("Content for user or admin"))); return Mono.just(ResponseEntity.ok(new Message("Content for user or admin")));
} }
} }

View File

@@ -1,44 +1,39 @@
package com.ard333.springbootwebfluxjjwt.security; package com.ard333.springbootwebfluxjjwt.security;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired; import lombok.AllArgsConstructor;
import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
/**
*
* @author ard333
*/
@Component @Component
@AllArgsConstructor
public class AuthenticationManager implements ReactiveAuthenticationManager { public class AuthenticationManager implements ReactiveAuthenticationManager {
@Autowired
private JWTUtil jwtUtil; private JWTUtil jwtUtil;
@Override @Override
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public Mono<Authentication> authenticate(Authentication authentication) { public Mono<Authentication> authenticate(Authentication authentication) {
String authToken = authentication.getCredentials().toString(); String authToken = authentication.getCredentials().toString();
try { String username = jwtUtil.getUsernameFromToken(authToken);
String username = jwtUtil.getUsernameFromToken(authToken); return Mono.just(jwtUtil.validateToken(authToken))
if (!jwtUtil.validateToken(authToken)) { .filter(valid -> valid)
return Mono.empty(); .switchIfEmpty(Mono.empty())
} .map(valid -> {
Claims claims = jwtUtil.getAllClaimsFromToken(authToken); Claims claims = jwtUtil.getAllClaimsFromToken(authToken);
List<String> rolesMap = claims.get("role", List.class); List<String> rolesMap = claims.get("role", List.class);
List<GrantedAuthority> authorities = new ArrayList<>(); return new UsernamePasswordAuthenticationToken(
for (String rolemap : rolesMap) { username,
authorities.add(new SimpleGrantedAuthority(rolemap)); null,
} rolesMap.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList())
return Mono.just(new UsernamePasswordAuthenticationToken(username, null, authorities)); );
} catch (Exception e) { });
return Mono.empty();
}
} }
} }

View File

@@ -5,10 +5,6 @@ import org.springframework.web.reactive.config.CorsRegistry;
import org.springframework.web.reactive.config.EnableWebFlux; import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.config.WebFluxConfigurer; import org.springframework.web.reactive.config.WebFluxConfigurer;
/**
*
* @author ard333
*/
@Configuration @Configuration
@EnableWebFlux @EnableWebFlux
public class CORSFilter implements WebFluxConfigurer { public class CORSFilter implements WebFluxConfigurer {

View File

@@ -15,37 +15,39 @@ import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
/**
*
* @author ard333
*/
@Component @Component
public class JWTUtil { public class JWTUtil {
@Value("${springbootwebfluxjjwt.jjwt.secret}") @Value("${springbootwebfluxjjwt.jjwt.secret}")
private String secret; private String secret;
@Value("${springbootwebfluxjjwt.jjwt.expiration}") @Value("${springbootwebfluxjjwt.jjwt.expiration}")
private String expirationTime; private String expirationTime;
private Key key; private Key key;
@PostConstruct @PostConstruct
public void init(){ public void init() {
this.key = Keys.hmacShaKeyFor(secret.getBytes()); this.key = Keys.hmacShaKeyFor(secret.getBytes());
} }
public Claims getAllClaimsFromToken(String token) { public Claims getAllClaimsFromToken(String token) {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody(); return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
} }
public String getUsernameFromToken(String token) { public String getUsernameFromToken(String token) {
return getAllClaimsFromToken(token).getSubject(); return getAllClaimsFromToken(token).getSubject();
} }
public Date getExpirationDateFromToken(String token) { public Date getExpirationDateFromToken(String token) {
return getAllClaimsFromToken(token).getExpiration(); return getAllClaimsFromToken(token).getExpiration();
} }
private Boolean isTokenExpired(String token) { private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token); final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date()); return expiration.before(new Date());
} }
public String generateToken(User user) { public String generateToken(User user) {
Map<String, Object> claims = new HashMap<>(); Map<String, Object> claims = new HashMap<>();
claims.put("role", user.getRoles()); claims.put("role", user.getRoles());
@@ -54,7 +56,7 @@ public class JWTUtil {
private String doGenerateToken(Map<String, Object> claims, String username) { private String doGenerateToken(Map<String, Object> claims, String username) {
Long expirationTimeLong = Long.parseLong(expirationTime); //in second Long expirationTimeLong = Long.parseLong(expirationTime); //in second
final Date createdDate = new Date(); final Date createdDate = new Date();
final Date expirationDate = new Date(createdDate.getTime() + expirationTimeLong * 1000); final Date expirationDate = new Date(createdDate.getTime() + expirationTimeLong * 1000);
return Jwts.builder() return Jwts.builder()
@@ -65,6 +67,7 @@ public class JWTUtil {
.signWith(key) .signWith(key)
.compact(); .compact();
} }
public Boolean validateToken(String token) { public Boolean validateToken(String token) {
return !isTokenExpired(token); return !isTokenExpired(token);
} }

View File

@@ -9,12 +9,9 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
/**
*
* @author ard333
*/
@Component @Component
public class PBKDF2Encoder implements PasswordEncoder{ public class PBKDF2Encoder implements PasswordEncoder {
@Value("${springbootwebfluxjjwt.password.encoder.secret}") @Value("${springbootwebfluxjjwt.password.encoder.secret}")
private String secret; private String secret;
@@ -23,6 +20,7 @@ public class PBKDF2Encoder implements PasswordEncoder{
@Value("${springbootwebfluxjjwt.password.encoder.keylength}") @Value("${springbootwebfluxjjwt.password.encoder.keylength}")
private Integer keylength; private Integer keylength;
/** /**
* More info (https://www.owasp.org/index.php/Hashing_Java) 404 :( * More info (https://www.owasp.org/index.php/Hashing_Java) 404 :(
* @param cs password * @param cs password

View File

@@ -1,8 +1,6 @@
package com.ard333.springbootwebfluxjjwt.security; package com.ard333.springbootwebfluxjjwt.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContext;
@@ -12,15 +10,15 @@ import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
/**
*
* @author ard333
*/
@Component @Component
public class SecurityContextRepository implements ServerSecurityContextRepository{ public class SecurityContextRepository implements ServerSecurityContextRepository {
@Autowired
private AuthenticationManager authenticationManager; private AuthenticationManager authenticationManager;
public SecurityContextRepository(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@Override @Override
public Mono<Void> save(ServerWebExchange swe, SecurityContext sc) { public Mono<Void> save(ServerWebExchange swe, SecurityContext sc) {
throw new UnsupportedOperationException("Not supported yet."); throw new UnsupportedOperationException("Not supported yet.");
@@ -28,17 +26,12 @@ public class SecurityContextRepository implements ServerSecurityContextRepositor
@Override @Override
public Mono<SecurityContext> load(ServerWebExchange swe) { public Mono<SecurityContext> load(ServerWebExchange swe) {
ServerHttpRequest request = swe.getRequest(); return Mono.justOrEmpty(swe.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION))
String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION); .filter(authHeader -> authHeader.startsWith("Bearer "))
.flatMap(authHeader -> {
if (authHeader != null && authHeader.startsWith("Bearer ")) { String authToken = authHeader.substring(7);
String authToken = authHeader.substring(7); Authentication auth = new UsernamePasswordAuthenticationToken(authToken, authToken);
Authentication auth = new UsernamePasswordAuthenticationToken(authToken, authToken); return this.authenticationManager.authenticate(auth).map(SecurityContextImpl::new);
return this.authenticationManager.authenticate(auth).map((authentication) -> {
return new SecurityContextImpl(authentication);
}); });
} else {
return Mono.empty();
}
} }
} }

View File

@@ -1,6 +1,5 @@
package com.ard333.springbootwebfluxjjwt.security; package com.ard333.springbootwebfluxjjwt.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@@ -9,34 +8,26 @@ import org.springframework.security.config.annotation.web.reactive.EnableWebFlux
import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.SecurityWebFilterChain;
import lombok.AllArgsConstructor;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
/** @AllArgsConstructor
*
* @author ard333
*/
@EnableWebFluxSecurity @EnableWebFluxSecurity
@EnableReactiveMethodSecurity @EnableReactiveMethodSecurity
public class WebSecurityConfig { public class WebSecurityConfig {
@Autowired
private AuthenticationManager authenticationManager; private AuthenticationManager authenticationManager;
@Autowired
private SecurityContextRepository securityContextRepository; private SecurityContextRepository securityContextRepository;
@Bean @Bean
public SecurityWebFilterChain securitygWebFilterChain(ServerHttpSecurity http) { public SecurityWebFilterChain securitygWebFilterChain(ServerHttpSecurity http) {
return http return http
.exceptionHandling() .exceptionHandling()
.authenticationEntryPoint((swe, e) -> { .authenticationEntryPoint((swe, e) ->
return Mono.fromRunnable(() -> { Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED))
swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); ).accessDeniedHandler((swe, e) ->
}); Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN))
}).accessDeniedHandler((swe, e) -> { ).and()
return Mono.fromRunnable(() -> {
swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
});
}).and()
.csrf().disable() .csrf().disable()
.formLogin().disable() .formLogin().disable()
.httpBasic().disable() .httpBasic().disable()

View File

@@ -1,7 +1,8 @@
package com.ard333.springbootwebfluxjjwt.service; package com.ard333.springbootwebfluxjjwt.service;
import com.ard333.springbootwebfluxjjwt.model.User; import com.ard333.springbootwebfluxjjwt.model.User;
import com.ard333.springbootwebfluxjjwt.security.model.Role; import com.ard333.springbootwebfluxjjwt.model.security.Role;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@@ -12,28 +13,26 @@ import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
/** /**
* * This is just an example, you can load the user from the database from the repository.
* @author ard333 *
*/ */
@Service @Service
public class UserService { public class UserService {
// this is just an example, you can load the user from the database from the repository
private Map<String, User> data; private Map<String, User> data;
@PostConstruct @PostConstruct
public void init(){ public void init() {
data = new HashMap<>(); data = new HashMap<>();
//username:passwowrd -> user:user
//username:passwowrd -> user:user
data.put("user", new User("user", "cBrlgyL2GI2GINuLUUwgojITuIufFycpLG4490dhGtY=", true, Arrays.asList(Role.ROLE_USER))); data.put("user", new User("user", "cBrlgyL2GI2GINuLUUwgojITuIufFycpLG4490dhGtY=", true, Arrays.asList(Role.ROLE_USER)));
//username:passwowrd -> admin:admin //username:passwowrd -> admin:admin
data.put("admin", new User("admin", "dQNjUIMorJb8Ubj2+wVGYp6eAeYkdekqAcnYp+aRq5w=", true, Arrays.asList(Role.ROLE_ADMIN))); data.put("admin", new User("admin", "dQNjUIMorJb8Ubj2+wVGYp6eAeYkdekqAcnYp+aRq5w=", true, Arrays.asList(Role.ROLE_ADMIN)));
} }
public Mono<User> findByUsername(String username) { public Mono<User> findByUsername(String username) {
if (data.containsKey(username)) { return Mono.justOrEmpty(data.get(username));
return Mono.just(data.get(username));
} else {
return Mono.empty();
}
} }
} }