refactoring
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.ard333.springbootwebfluxjjwt.model.security;
|
||||
|
||||
public enum Role {
|
||||
ROLE_USER, ROLE_ADMIN
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* This is just an example, you can load the user from the database from the repository.
|
||||
*
|
||||
* @author ard333
|
||||
*/
|
||||
@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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user