implement authentication and authorization

This commit is contained in:
ard333
2018-05-13 21:11:22 +07:00
parent 2973078771
commit b5ddc1c6d9
17 changed files with 675 additions and 1 deletions

12
pom.xml
View File

@@ -14,7 +14,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
<version>2.0.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
@@ -33,6 +33,16 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@@ -0,0 +1,21 @@
/*
* Ardiansyah | http://ard.web.id
*
*/
package id.web.ard.springbootwebfluxjjwt.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
/**
*
* @author ardiansyah
*/
@Data @NoArgsConstructor @AllArgsConstructor @ToString
public class Message {
private String content;
}

View File

@@ -0,0 +1,103 @@
/*
* Ardiansyah | http://ard.web.id
*
*/
package id.web.ard.springbootwebfluxjjwt.entity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import id.web.ard.springbootwebfluxjjwt.security.model.Role;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.ToString;
/**
*
* @author ardiansyah
*/
@ToString @AllArgsConstructor @NoArgsConstructor
public class User implements UserDetails {
private String username;
private String password;
private Boolean enabled;
private List<Role> roles;
public User(String username) {
this.username = username;
}
@Override
public String getUsername() {
return username;
}
//==============================
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return this.enabled;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.roles.stream().map(authority -> new SimpleGrantedAuthority(authority.name())).collect(Collectors.toList());
}
//==============================
@JsonIgnore
@Override
public String getPassword() {
return password;
}
@JsonProperty
public void setPassword(String password) {
this.password = password;
}
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
}

View File

@@ -0,0 +1,49 @@
/*
* Ardiansyah | http://ard.web.id
*
*/
package id.web.ard.springbootwebfluxjjwt.rest;
import id.web.ard.springbootwebfluxjjwt.security.JWTUtil;
import id.web.ard.springbootwebfluxjjwt.security.PBKDF2Encoder;
import id.web.ard.springbootwebfluxjjwt.security.model.AuthRequest;
import id.web.ard.springbootwebfluxjjwt.security.model.AuthResponse;
import id.web.ard.springbootwebfluxjjwt.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
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 reactor.core.publisher.Mono;
/**
*
* @author ardiansyah
*/
@RestController
public class AuthenticationREST {
@Autowired
private JWTUtil jwtTokenUtil;
@Autowired
private PBKDF2Encoder passwordEncoder;
@Autowired
private UserService userRepository;
@RequestMapping(value = "auth", method = RequestMethod.POST)
public Mono<ResponseEntity<AuthResponse>> auth(@RequestBody AuthRequest ar) {
return userRepository.findByUsername(ar.getUsername()).map((userDetails) -> {
if (passwordEncoder.encode(ar.getPassword()).equals(userDetails.getPassword())) {
return ResponseEntity.ok(new AuthResponse(jwtTokenUtil.generateToken(userDetails)));
} else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
});
}
}

View File

@@ -0,0 +1,39 @@
/*
* Ardiansyah | http://ard.web.id
*
*/
package id.web.ard.springbootwebfluxjjwt.rest;
import id.web.ard.springbootwebfluxjjwt.entity.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.RestController;
import reactor.core.publisher.Mono;
/**
*
* @author ardiansyah
*/
@RestController
public class ResourceREST {
@RequestMapping(value = "resource/user", method = RequestMethod.GET)
@PreAuthorize("hasRole('USER')")
public Mono<ResponseEntity<?>> user() {
return Mono.just(ResponseEntity.ok(new Message("Content for user")));
}
@RequestMapping(value = "resource/admin", method = RequestMethod.GET)
@PreAuthorize("hasRole('ADMIN')")
public Mono<ResponseEntity<?>> admin() {
return Mono.just(ResponseEntity.ok(new Message("Content for admin")));
}
@RequestMapping(value = "resource/user-or-admin", method = RequestMethod.GET)
@PreAuthorize("hasRole('USER') or hasRole('ADMIN')")
public Mono<ResponseEntity<?>> userOrAdmin() {
return Mono.just(ResponseEntity.ok(new Message("Content for user or admin")));
}
}

View File

@@ -0,0 +1,62 @@
/*
* Ardiansyah | http://ard.web.id
*
*/
package id.web.ard.springbootwebfluxjjwt.security;
import id.web.ard.springbootwebfluxjjwt.security.model.Role;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
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;
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
/**
*
* @author ardiansyah
*/
@Component
public class AuthenticationManager implements ReactiveAuthenticationManager {
@Autowired
private JWTUtil jwtUtil;
@Override
public Mono<Authentication> authenticate(Authentication authentication) {
String authToken = authentication.getCredentials().toString();
String username = null;
try {
username = jwtUtil.getUsernameFromToken(authToken);
} catch (Exception e) {
username = null;
}
if (username != null) {
Claims claims = jwtUtil.getAllClaimsFromToken(authToken);
if (jwtUtil.validateToken(authToken)) {
List<String> rolesMap = claims.get("role", List.class);
List<Role> roles = new ArrayList<>();
for (String rolemap : rolesMap) {
roles.add(Role.valueOf(rolemap));
}
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
username,
null,
roles.stream().map(authority -> new SimpleGrantedAuthority(authority.name())).collect(Collectors.toList())
);
return Mono.just(auth);
} else {
return Mono.empty();
}
} else {
return Mono.empty();
}
}
}

View File

@@ -0,0 +1,35 @@
/*
* Ardiansyah | http://ard.web.id
*
*/
package id.web.ard.springbootwebfluxjjwt.security;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
/**
*
* @author ardiansyah
*/
@Component
public class CORSFilter implements WebFilter{
@Override
public Mono<Void> filter(ServerWebExchange swe, WebFilterChain wfc) {
//CORS
swe.getResponse().getHeaders().add("Access-Control-Allow-Origin", "*");
if (swe.getRequest().getHeaders().get("Access-Control-Request-Method") != null && "OPTIONS".equalsIgnoreCase(swe.getRequest().getMethod().toString())) {
swe.getResponse().getHeaders().add("Access-Control-Allow-Headers", "Authorization");
swe.getResponse().getHeaders().add("Access-Control-Allow-Headers", "Content-Type");
swe.getResponse().getHeaders().add("Access-Control-Max-Age", "1");
swe.getResponse().getHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
}
return wfc.filter(swe);
}
}

View File

@@ -0,0 +1,75 @@
/*
* Ardiansyah | http://ard.web.id
*
*/
package id.web.ard.springbootwebfluxjjwt.security;
import id.web.ard.springbootwebfluxjjwt.entity.User;
import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
*
* @author ardiansyah
*/
@Component
public class JWTUtil implements Serializable {
private static final long serialVersionUID = 1L;
@Value("${springbootwebfluxjjwt.jjwt.secret}")
private String secret;
@Value("${springbootwebfluxjjwt.jjwt.expiration}")
private String expirationTime;
public Claims getAllClaimsFromToken(String token) {
return Jwts.parser().setSigningKey(secret).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());
claims.put("enable", user.getEnabled());
return doGenerateToken(claims, user.getUsername());
}
private String doGenerateToken(Map<String, Object> claims, String username) {
Long expirationTimeLong = Long.parseLong(expirationTime); //in second
final Date createdDate = new Date();
final Date expirationDate = new Date(createdDate.getTime() + expirationTimeLong * 1000);
return Jwts.builder()
.setClaims(claims)
.setSubject(username)
.setIssuedAt(createdDate)
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public Boolean validateToken(String token) {
return !isTokenExpired(token);
}
}

View File

@@ -0,0 +1,48 @@
/*
* Ardiansyah | http://ard.web.id
*
*/
package id.web.ard.springbootwebfluxjjwt.security;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.Base64;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
/**
*
* @author ardiansyah
*/
@Component
public class PBKDF2Encoder implements PasswordEncoder{
@Value("${springbootwebfluxjjwt.password.secret}")
private String secret;
/**
* More info (https://www.owasp.org/index.php/Hashing_Java)
* @param cs password
* @return encoded password
*/
@Override
public String encode(CharSequence cs) {
try {
byte[] result = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512")
.generateSecret(new PBEKeySpec(cs.toString().toCharArray(), secret.getBytes(), 33, 256))
.getEncoded();
return Base64.getEncoder().encodeToString(result);
} catch (NoSuchAlgorithmException | InvalidKeySpecException ex) {
throw new RuntimeException(ex);
}
}
@Override
public boolean matches(CharSequence cs, String string) {
return encode(cs).equals(string);
}
}

View File

@@ -0,0 +1,50 @@
/*
* Ardiansyah | http://ard.web.id
*
*/
package id.web.ard.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;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
*
* @author ardiansyah
*/
@Component
public class SecurityContextRepository implements ServerSecurityContextRepository{
@Autowired
private AuthenticationManager authenticationManager;
@Override
public Mono<Void> save(ServerWebExchange swe, SecurityContext sc) {
throw new UnsupportedOperationException("Not supported yet.");
}
@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);
});
} else {
return Mono.empty();
}
}
}

View File

@@ -0,0 +1,29 @@
/*
* Ardiansyah | http://ard.web.id
*
*/
package id.web.ard.springbootwebfluxjjwt.security;
import id.web.ard.springbootwebfluxjjwt.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
/**
*
* @author ardiansyah
*/
@Service
public class UserDetailsServiceImpl implements ReactiveUserDetailsService {
@Autowired
private UserService userRepository;
@Override
public Mono<UserDetails> findByUsername(String username) {
return userRepository.findUserDetailsByUsername(username);
}
}

View File

@@ -0,0 +1,39 @@
package id.web.ard.springbootwebfluxjjwt.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
/**
*
* @author ardiansyah
*/
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class WebSecurityConfig {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private SecurityContextRepository securityContextRepository;
@Bean
public SecurityWebFilterChain securitygWebFilterChain(ServerHttpSecurity http) {
return http.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.authenticationManager(authenticationManager)
.securityContextRepository(securityContextRepository)
//.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.HTTP_BASIC)
.authorizeExchange()
.pathMatchers("/auth").permitAll()
.anyExchange().authenticated()
.and().build();
}
}

View File

@@ -0,0 +1,23 @@
/*
* Ardiansyah | http://ard.web.id
*
*/
package id.web.ard.springbootwebfluxjjwt.security.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
/**
*
* @author ardiansyah
*/
@Data @NoArgsConstructor @AllArgsConstructor @ToString
public class AuthRequest {
private String username;
private String password;
}

View File

@@ -0,0 +1,21 @@
/*
* Ardiansyah | http://ard.web.id
*
*/
package id.web.ard.springbootwebfluxjjwt.security.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
/**
*
* @author ardiansyah
*/
@Data @NoArgsConstructor @AllArgsConstructor @ToString
public class AuthResponse {
private String token;
}

View File

@@ -0,0 +1,13 @@
/*
* Ardiansyah | http://ard.web.id
*
*/
package id.web.ard.springbootwebfluxjjwt.security.model;
/**
*
* @author ardiansyah
*/
public enum Role {
ROLE_USER, ROLE_ADMIN
}

View File

@@ -0,0 +1,54 @@
/*
* Ardiansyah | http://ard.web.id
*
*/
package id.web.ard.springbootwebfluxjjwt.service;
import id.web.ard.springbootwebfluxjjwt.entity.User;
import id.web.ard.springbootwebfluxjjwt.security.model.Role;
import java.util.Arrays;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
/**
*
* @author ardiansyah
*/
@Service
public class UserService {
// this is just an example, you can load the user from the database from the repository
//username:passwowrd -> user:user
private final String userUsername = "user";// password: user
private final User user = new User(userUsername, "cBrlgyL2GI2GINuLUUwgojITuIufFycpLG4490dhGtY=", true, Arrays.asList(Role.ROLE_USER));
//username:passwowrd -> admin:admin
private final String adminUsername = "admin";// password: admin
private final User admin = new User(adminUsername, "dQNjUIMorJb8Ubj2+wVGYp6eAeYkdekqAcnYp+aRq5w=", true, Arrays.asList(Role.ROLE_ADMIN));
public Mono<UserDetails> findUserDetailsByUsername(String username) {
if (username.equals(userUsername)) {
return Mono.just(user);
} else if (username.equals(adminUsername)) {
return Mono.just(admin);
} else {
return Mono.empty();
}
}
public Mono<User> findByUsername(String username) {
if (username.equals(userUsername)) {
return Mono.just(user);
} else if (username.equals(adminUsername)) {
return Mono.just(admin);
} else {
return Mono.empty();
}
}
}

View File

@@ -0,0 +1,3 @@
springbootwebfluxjjwt.password.secret=mysecret
springbootwebfluxjjwt.jjwt.secret=mysecret
springbootwebfluxjjwt.jjwt.expiration=28800