Last active
October 27, 2023 07:44
-
-
Save thomasdarimont/8d6bc243d3b504439e67d57cb0d0bb72 to your computer and use it in GitHub Desktop.
Secure REST API Example with Spring Security, Spring Session, Spring Boot
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package demo; | |
import java.io.Serializable; | |
import java.security.Principal; | |
import java.util.Collection; | |
import java.util.Collections; | |
import java.util.HashMap; | |
import java.util.Map; | |
import java.util.UUID; | |
import javax.servlet.http.HttpSession; | |
import org.springframework.beans.factory.annotation.Autowired; | |
import org.springframework.boot.SpringApplication; | |
import org.springframework.boot.autoconfigure.SpringBootApplication; | |
import org.springframework.context.ApplicationContext; | |
import org.springframework.context.annotation.Bean; | |
import org.springframework.context.annotation.Configuration; | |
import org.springframework.security.access.PermissionEvaluator; | |
import org.springframework.security.access.annotation.Secured; | |
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; | |
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; | |
import org.springframework.security.access.prepost.PreAuthorize; | |
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; | |
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; | |
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration; | |
import org.springframework.security.config.annotation.web.builders.HttpSecurity; | |
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; | |
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; | |
import org.springframework.security.core.Authentication; | |
import org.springframework.security.core.GrantedAuthority; | |
import org.springframework.security.core.annotation.AuthenticationPrincipal; | |
import org.springframework.security.web.savedrequest.NullRequestCache; | |
import org.springframework.session.ExpiringSession; | |
import org.springframework.session.MapSessionRepository; | |
import org.springframework.session.SessionRepository; | |
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession; | |
import org.springframework.session.web.http.HeaderHttpSessionStrategy; | |
import org.springframework.session.web.http.HttpSessionStrategy; | |
import org.springframework.stereotype.Component; | |
import org.springframework.util.CollectionUtils; | |
import org.springframework.web.bind.annotation.GetMapping; | |
import org.springframework.web.bind.annotation.PostMapping; | |
import org.springframework.web.bind.annotation.RequestMapping; | |
import org.springframework.web.bind.annotation.RestController; | |
import lombok.Data; | |
import lombok.Value; | |
import lombok.extern.slf4j.Slf4j; | |
/** | |
* <pre> | |
* {@code | |
* curl --noproxy localhost -u user:password -v http://localhost:8080/api/auth | |
* export AUTH_TOKEN=... | |
* curl --noproxy localhost -H "x-auth-token: $AUTH_TOKEN" -v http://localhost:8080/api/greet | |
* curl --noproxy localhost -H "x-auth-token: $AUTH_TOKEN" -v -d "amount=42.0" http://localhost:8080/api/order | |
* curl --noproxy localhost -H "x-auth-token: $AUTH_TOKEN" -v -d "amount=1000.0" http://localhost:8080/api/order | |
* | |
* curl --noproxy localhost -u admin:password -v http://localhost:8080/api/auth | |
* export AUTH_TOKEN=... | |
* curl --noproxy localhost -H "x-auth-token: $AUTH_TOKEN" -v -d "amount=1000.0" http://localhost:8080/api/order | |
* } | |
* </pre> | |
* | |
*/ | |
@SpringBootApplication | |
public class App { | |
public static void main(String[] args) { | |
SpringApplication.run(App.class, args); | |
} | |
} | |
@Configuration | |
@EnableSpringHttpSession | |
class HttpSessionConfig { | |
@Bean | |
SessionRepository<ExpiringSession> inmemorySessionRepository() { | |
return new MapSessionRepository(); | |
} | |
@Bean | |
HttpSessionStrategy httpSessionStrategy() { | |
return new HeaderHttpSessionStrategy(); | |
} | |
} | |
@Configuration | |
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) | |
class SecurityConfig extends GlobalMethodSecurityConfiguration { | |
@Autowired | |
private DomainAwarePermissionEvaluator permissionEvaluator; | |
@Autowired | |
private ApplicationContext applicationContext; | |
@Override | |
protected MethodSecurityExpressionHandler createExpressionHandler() { | |
DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); | |
expressionHandler.setPermissionEvaluator(permissionEvaluator); | |
expressionHandler.setApplicationContext(applicationContext); | |
return expressionHandler; | |
} | |
} | |
@EnableWebSecurity | |
class WebSecurityConfig extends WebSecurityConfigurerAdapter { | |
@Override | |
protected void configure(HttpSecurity http) throws Exception { | |
http.authorizeRequests() // | |
.anyRequest().authenticated() // | |
.and().requestCache().requestCache(new NullRequestCache()) // | |
.and().httpBasic() // | |
.and().csrf().disable(); | |
} | |
@Autowired | |
void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { | |
auth.inMemoryAuthentication() // | |
.withUser("user").password("password").authorities("ROLE_USER") // | |
.and() // | |
.withUser("admin").password("password").authorities("ROLE_USER", "ROLE_ADMIN"); | |
} | |
} | |
@Slf4j | |
@Component | |
class DomainAwarePermissionEvaluator implements PermissionEvaluator { | |
@Override | |
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) { | |
log.info("check permission '{}' for user '{}' for target '{}'", permission, authentication.getName(), | |
targetDomainObject); | |
if ("place-order".equals(permission)) { | |
Order order = (Order) targetDomainObject; | |
if (order.getAmount() > 500) { | |
return hasRole("ROLE_ADMIN", authentication); | |
} | |
} | |
return true; | |
} | |
@Override | |
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, | |
Object permission) { | |
return hasPermission(authentication, new DomainObjectReference(targetId, targetType), permission); | |
} | |
private boolean hasRole(String role, Authentication auth) { | |
if (auth == null || auth.getPrincipal() == null) { | |
return false; | |
} | |
Collection<? extends GrantedAuthority> authorities = auth.getAuthorities(); | |
if (CollectionUtils.isEmpty(authorities)) { | |
return false; | |
} | |
return authorities.stream().filter(ga -> role.equals(ga.getAuthority())).findAny().isPresent(); | |
} | |
@Value | |
static class DomainObjectReference { | |
private final Serializable targetId; | |
private final String targetType; | |
} | |
} | |
@RequestMapping("/api/auth") | |
@RestController | |
class AuthEndpoint { | |
@GetMapping | |
Map<String, Object> getToken(HttpSession session) { | |
return Collections.singletonMap("session", session.getId()); | |
} | |
} | |
@Secured("ROLE_USER") | |
@RequestMapping("/api/greet") | |
@RestController | |
class GreetingEndpoint { | |
@GetMapping | |
Map<String, Object> greet(@AuthenticationPrincipal Principal user) { | |
Map<String, Object> map = new HashMap<>(); | |
map.put("user", user.getName()); | |
return map; | |
} | |
} | |
@Secured("ROLE_USER") | |
@RequestMapping("/api/order") | |
@RestController | |
class OrderEndpoint { | |
@PostMapping | |
@PreAuthorize("hasPermission(#order, 'place-order')") | |
Map<String, Object> placeOrder(Order order) { | |
Map<String, Object> map = new HashMap<>(); | |
map.put("orderId", UUID.randomUUID()); | |
return map; | |
} | |
} | |
@Data | |
class Order { | |
double amount; | |
} | |
@RequestMapping("/api/admin") | |
@RestController | |
class AdminEndpoint { | |
@GetMapping | |
@Secured("ROLE_ADMIN") | |
Map<String, Object> manage(@AuthenticationPrincipal Principal user) { | |
return Collections.singletonMap("user", user.getName()); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="UTF-8"?> | |
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | |
<modelVersion>4.0.0</modelVersion> | |
<groupId>de.tdlabs.training</groupId> | |
<artifactId>spring-boot-secure-rest-api-example</artifactId> | |
<version>0.0.1-SNAPSHOT</version> | |
<packaging>jar</packaging> | |
<name>spring-boot-secure-rest-api-example</name> | |
<description>Demo project for Spring Boot</description> | |
<parent> | |
<groupId>org.springframework.boot</groupId> | |
<artifactId>spring-boot-starter-parent</artifactId> | |
<version>1.4.0.RELEASE</version> | |
<relativePath/> <!-- lookup parent from repository --> | |
</parent> | |
<properties> | |
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | |
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> | |
<java.version>1.8</java.version> | |
</properties> | |
<dependencies> | |
<dependency> | |
<groupId>org.springframework.boot</groupId> | |
<artifactId>spring-boot-starter-security</artifactId> | |
</dependency> | |
<dependency> | |
<groupId>org.springframework.boot</groupId> | |
<artifactId>spring-boot-starter-web</artifactId> | |
</dependency> | |
<dependency> | |
<groupId>org.springframework.boot</groupId> | |
<artifactId>spring-boot-devtools</artifactId> | |
</dependency> | |
<dependency> | |
<groupId>org.springframework.session</groupId> | |
<artifactId>spring-session</artifactId> | |
</dependency> | |
<dependency> | |
<groupId>org.projectlombok</groupId> | |
<artifactId>lombok</artifactId> | |
</dependency> | |
<dependency> | |
<groupId>org.springframework.boot</groupId> | |
<artifactId>spring-boot-starter-test</artifactId> | |
<scope>test</scope> | |
</dependency> | |
</dependencies> | |
<build> | |
<plugins> | |
<plugin> | |
<groupId>org.springframework.boot</groupId> | |
<artifactId>spring-boot-maven-plugin</artifactId> | |
</plugin> | |
</plugins> | |
</build> | |
</project> |
How did you handle the noproxy part?
curl --noproxy localhost -H "x-auth-token: $AUTH_TOKEN"
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
can you update with String boot 2.0 ?