diff --git a/src/main/java/com/ard333/springbootwebfluxjjwt/model/Message.java b/src/main/java/com/ard333/springbootwebfluxjjwt/model/Message.java index 1927ada..2933a34 100644 --- a/src/main/java/com/ard333/springbootwebfluxjjwt/model/Message.java +++ b/src/main/java/com/ard333/springbootwebfluxjjwt/model/Message.java @@ -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; } diff --git a/src/main/java/com/ard333/springbootwebfluxjjwt/model/User.java b/src/main/java/com/ard333/springbootwebfluxjjwt/model/User.java index ee7d127..a3f1ee0 100644 --- a/src/main/java/com/ard333/springbootwebfluxjjwt/model/User.java +++ b/src/main/java/com/ard333/springbootwebfluxjjwt/model/User.java @@ -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 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; diff --git a/src/main/java/com/ard333/springbootwebfluxjjwt/model/security/AuthRequest.java b/src/main/java/com/ard333/springbootwebfluxjjwt/model/security/AuthRequest.java new file mode 100644 index 0000000..bc34916 --- /dev/null +++ b/src/main/java/com/ard333/springbootwebfluxjjwt/model/security/AuthRequest.java @@ -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; + +} diff --git a/src/main/java/com/ard333/springbootwebfluxjjwt/model/security/AuthResponse.java b/src/main/java/com/ard333/springbootwebfluxjjwt/model/security/AuthResponse.java new file mode 100644 index 0000000..20611a2 --- /dev/null +++ b/src/main/java/com/ard333/springbootwebfluxjjwt/model/security/AuthResponse.java @@ -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; + +} diff --git a/src/main/java/com/ard333/springbootwebfluxjjwt/model/security/Role.java b/src/main/java/com/ard333/springbootwebfluxjjwt/model/security/Role.java new file mode 100644 index 0000000..a5d31b9 --- /dev/null +++ b/src/main/java/com/ard333/springbootwebfluxjjwt/model/security/Role.java @@ -0,0 +1,5 @@ +package com.ard333.springbootwebfluxjjwt.model.security; + +public enum Role { + ROLE_USER, ROLE_ADMIN +} \ No newline at end of file diff --git a/src/main/java/com/ard333/springbootwebfluxjjwt/rest/AuthenticationREST.java b/src/main/java/com/ard333/springbootwebfluxjjwt/rest/AuthenticationREST.java index 4652554..77cc869 100644 --- a/src/main/java/com/ard333/springbootwebfluxjjwt/rest/AuthenticationREST.java +++ b/src/main/java/com/ard333/springbootwebfluxjjwt/rest/AuthenticationREST.java @@ -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> 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> 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())); } } diff --git a/src/main/java/com/ard333/springbootwebfluxjjwt/rest/ResourceREST.java b/src/main/java/com/ard333/springbootwebfluxjjwt/rest/ResourceREST.java index cc8f443..96c4407 100644 --- a/src/main/java/com/ard333/springbootwebfluxjjwt/rest/ResourceREST.java +++ b/src/main/java/com/ard333/springbootwebfluxjjwt/rest/ResourceREST.java @@ -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> user() { + public Mono> 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> admin() { + public Mono> 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> userOrAdmin() { + public Mono> userOrAdmin() { return Mono.just(ResponseEntity.ok(new Message("Content for user or admin"))); } } diff --git a/src/main/java/com/ard333/springbootwebfluxjjwt/security/AuthenticationManager.java b/src/main/java/com/ard333/springbootwebfluxjjwt/security/AuthenticationManager.java index 8018bc3..6d653ba 100644 --- a/src/main/java/com/ard333/springbootwebfluxjjwt/security/AuthenticationManager.java +++ b/src/main/java/com/ard333/springbootwebfluxjjwt/security/AuthenticationManager.java @@ -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 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 rolesMap = claims.get("role", List.class); - List 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 rolesMap = claims.get("role", List.class); + return new UsernamePasswordAuthenticationToken( + username, + null, + rolesMap.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()) + ); + }); } } diff --git a/src/main/java/com/ard333/springbootwebfluxjjwt/security/CORSFilter.java b/src/main/java/com/ard333/springbootwebfluxjjwt/security/CORSFilter.java index a557f1b..9d5fbdc 100644 --- a/src/main/java/com/ard333/springbootwebfluxjjwt/security/CORSFilter.java +++ b/src/main/java/com/ard333/springbootwebfluxjjwt/security/CORSFilter.java @@ -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 { diff --git a/src/main/java/com/ard333/springbootwebfluxjjwt/security/JWTUtil.java b/src/main/java/com/ard333/springbootwebfluxjjwt/security/JWTUtil.java index 58b39ed..41f08ac 100644 --- a/src/main/java/com/ard333/springbootwebfluxjjwt/security/JWTUtil.java +++ b/src/main/java/com/ard333/springbootwebfluxjjwt/security/JWTUtil.java @@ -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 claims = new HashMap<>(); claims.put("role", user.getRoles()); @@ -54,7 +56,7 @@ public class JWTUtil { private String doGenerateToken(Map 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); } diff --git a/src/main/java/com/ard333/springbootwebfluxjjwt/security/PBKDF2Encoder.java b/src/main/java/com/ard333/springbootwebfluxjjwt/security/PBKDF2Encoder.java index b8d3377..53d5e30 100644 --- a/src/main/java/com/ard333/springbootwebfluxjjwt/security/PBKDF2Encoder.java +++ b/src/main/java/com/ard333/springbootwebfluxjjwt/security/PBKDF2Encoder.java @@ -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 diff --git a/src/main/java/com/ard333/springbootwebfluxjjwt/security/SecurityContextRepository.java b/src/main/java/com/ard333/springbootwebfluxjjwt/security/SecurityContextRepository.java index 1f266ac..988b15a 100644 --- a/src/main/java/com/ard333/springbootwebfluxjjwt/security/SecurityContextRepository.java +++ b/src/main/java/com/ard333/springbootwebfluxjjwt/security/SecurityContextRepository.java @@ -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 save(ServerWebExchange swe, SecurityContext sc) { throw new UnsupportedOperationException("Not supported yet."); @@ -28,17 +26,12 @@ public class SecurityContextRepository implements ServerSecurityContextRepositor @Override public Mono 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(); - } } } diff --git a/src/main/java/com/ard333/springbootwebfluxjjwt/security/WebSecurityConfig.java b/src/main/java/com/ard333/springbootwebfluxjjwt/security/WebSecurityConfig.java index 00bb20a..41c4667 100644 --- a/src/main/java/com/ard333/springbootwebfluxjjwt/security/WebSecurityConfig.java +++ b/src/main/java/com/ard333/springbootwebfluxjjwt/security/WebSecurityConfig.java @@ -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() diff --git a/src/main/java/com/ard333/springbootwebfluxjjwt/service/UserService.java b/src/main/java/com/ard333/springbootwebfluxjjwt/service/UserService.java index 58fd102..38a43ca 100644 --- a/src/main/java/com/ard333/springbootwebfluxjjwt/service/UserService.java +++ b/src/main/java/com/ard333/springbootwebfluxjjwt/service/UserService.java @@ -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 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 findByUsername(String username) { - if (data.containsKey(username)) { - return Mono.just(data.get(username)); - } else { - return Mono.empty(); - } + return Mono.justOrEmpty(data.get(username)); } }