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.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
/**
*
* @author ard333
*/
@Data @NoArgsConstructor @AllArgsConstructor @ToString
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Message {
private String content;
}

View File

@@ -1,8 +1,9 @@
package com.ard333.springbootwebfluxjjwt.model;
import com.ard333.springbootwebfluxjjwt.model.security.Role;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.ard333.springbootwebfluxjjwt.security.model.Role;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
@@ -15,24 +16,27 @@ import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
/**
*
* @author ard333
*/
@ToString @AllArgsConstructor @NoArgsConstructor
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class User implements UserDetails {
private static final long serialVersionUID = 1L;
private String username;
private String password;
@Getter @Setter
private Boolean enabled;
@Getter @Setter
private List<Role> roles;
@Override
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@@ -67,6 +71,7 @@ public class User implements UserDetails {
public String getPassword() {
return password;
}
@JsonProperty
public void setPassword(String 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;
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.PBKDF2Encoder;
import com.ard333.springbootwebfluxjjwt.security.model.AuthRequest;
import com.ard333.springbootwebfluxjjwt.security.model.AuthResponse;
import com.ard333.springbootwebfluxjjwt.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
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.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import lombok.AllArgsConstructor;
import reactor.core.publisher.Mono;
/**
*
* @author ard333
*/
@AllArgsConstructor
@RestController
public class AuthenticationREST {
@Autowired
private JWTUtil jwtUtil;
@Autowired
private PBKDF2Encoder passwordEncoder;
@Autowired
private UserService userService;
@RequestMapping(value = "/login", method = RequestMethod.POST)
public Mono<ResponseEntity<?>> login(@RequestBody AuthRequest ar) {
return userService.findByUsername(ar.getUsername()).map((userDetails) -> {
if (passwordEncoder.encode(ar.getPassword()).equals(userDetails.getPassword())) {
return ResponseEntity.ok(new AuthResponse(jwtUtil.generateToken(userDetails)));
} else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}).defaultIfEmpty(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build());
@PostMapping("/login")
public Mono<ResponseEntity<AuthResponse>> login(@RequestBody AuthRequest ar) {
return userService.findByUsername(ar.getUsername())
.filter(userDetails -> passwordEncoder.encode(ar.getPassword()).equals(userDetails.getPassword()))
.map(userDetails -> ResponseEntity.ok(new AuthResponse(jwtUtil.generateToken(userDetails))))
.switchIfEmpty(Mono.just(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()));
}
}

View File

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

View File

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

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.WebFluxConfigurer;
/**
*
* @author ard333
*/
@Configuration
@EnableWebFlux
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.stereotype.Component;
/**
*
* @author ard333
*/
@Component
public class JWTUtil {
@Value("${springbootwebfluxjjwt.jjwt.secret}")
private String secret;
@Value("${springbootwebfluxjjwt.jjwt.expiration}")
private String expirationTime;
private Key key;
@PostConstruct
public void init(){
public void init() {
this.key = Keys.hmacShaKeyFor(secret.getBytes());
}
public Claims getAllClaimsFromToken(String token) {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
public String getUsernameFromToken(String token) {
return getAllClaimsFromToken(token).getSubject();
}
public Date getExpirationDateFromToken(String token) {
return getAllClaimsFromToken(token).getExpiration();
}
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
public String generateToken(User user) {
Map<String, Object> claims = new HashMap<>();
claims.put("role", user.getRoles());
@@ -54,7 +56,7 @@ public class JWTUtil {
private String doGenerateToken(Map<String, Object> claims, String username) {
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);
return Jwts.builder()
@@ -65,6 +67,7 @@ public class JWTUtil {
.signWith(key)
.compact();
}
public Boolean validateToken(String 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.stereotype.Component;
/**
*
* @author ard333
*/
@Component
public class PBKDF2Encoder implements PasswordEncoder{
public class PBKDF2Encoder implements PasswordEncoder {
@Value("${springbootwebfluxjjwt.password.encoder.secret}")
private String secret;
@@ -23,6 +20,7 @@ public class PBKDF2Encoder implements PasswordEncoder{
@Value("${springbootwebfluxjjwt.password.encoder.keylength}")
private Integer keylength;
/**
* More info (https://www.owasp.org/index.php/Hashing_Java) 404 :(
* @param cs password

View File

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

View File

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

View File

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