Примеры к занятию по ACL

This commit is contained in:
vitalykutsenko
2024-03-16 10:58:12 +03:00
parent 5a65a8aeda
commit 1b99e26b43
23 changed files with 791 additions and 0 deletions
@@ -0,0 +1,99 @@
# Программа JS Dev -> Java Dev
## Первые шаги в Java
### 1. Java HelloWorld
- История языка
- Основные концепции языка
- Экосистема, установка. Какую версию выбрать?
- Типы данных. Примитивы/классы
- Hello World. Сборка и запуск приложений на Java
### 2. ООП в Java
- ООП
- Классы, поля и методы
- Модификаторы доступа.
- Специализированные конструкции классов (интерфейс, enum)
- аритектура на основе ООП
- исключения
### 3. Практическая Java. ДЗ
- IDE
- Code Convention
- Инструменты сборки (maven)
- Тестирование
### 4. Generics. Collections
### 5. Аннотации. Lombok
### 6. Сборщик мусора, Java в Докер. ДЗ
- Управление памятью
- Основы Docker
- Java в Docker (особенно память)
### 7. Многопоточность в Java
- Проблематика
- Создание потоков
- JMM
- Управление потоками
### 8. Функциональное программирование в Java
- Лямбда функции
- Функциональное программирование
- Streams
- Optional
## SQL БД
### 9. Основы реляционных БД. SQL
- Концепция и архитектура реляционных БД
- SQL. Основные приемы
### 10. Java-SQLDB. ORM. ДЗ
- SQL запросы в java (JDBC)
- ORM. Hibernate
## Прикладная Java
### 11. Архитектура приложение (SOLID, Dependency Injection)
- понятия Inversion of Control (IoC) и Dependency Injection (DI);
- Что такое Spring
### 12. Spring Framework. Основы
- Создание приложений с помощью Spring
- Spring Boot
- Конфигурация
### 13. Spring Framework. DB
- Spring JDBC
- Spring JPA
- Магия Spring Data JPA
- Тестирование
### 14. Spring WEB (REST). ДЗ
- Spring WEB
- REST API
- OpenAPI
- Тестирование
### 15. Spring Security
- Архитектура
- Аутентификация
- Авторизация
- Тестирование
### 16. Spring Kafka
- Архитектура Kafka
- Интеграция Spring-приложения и Kafka
- Тестирование
### 17. Интеграционное тестирование ДЗ
- Идеология
- Gherkin, Cucumber
- Selenium
- Test Containers
+83
View File
@@ -0,0 +1,83 @@
<?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>ru.otus</groupId>
<artifactId>spring-framework-26-acl</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.10</version>
</parent>
<properties>
<ehcache-core.version>2.6.11</ehcache-core.version>
<swagger.version>3.0.0</swagger.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Spring Security ACL и зависимости-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-acl</artifactId>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>${ehcache-core.version}</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<!-- <version>1.4.198</version>-->
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.6.9</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,14 @@
package ru.otus.spring;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@SpringBootApplication
public class Main {
public static void main(String[] args) {
SpringApplication.run(Main.class);
}
}
@@ -0,0 +1,33 @@
package ru.otus.spring.model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "system_message")
public class NoticeMessage {
@Id
@Column
private Integer id;
@Column
private String content;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
@@ -0,0 +1,4 @@
package ru.otus.spring.model;
public class Pack {
}
@@ -0,0 +1,25 @@
package ru.otus.spring.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.access.prepost.PreAuthorize;
import ru.otus.spring.model.NoticeMessage;
import java.util.List;
import java.util.Optional;
public interface NoticeMessageRepository extends JpaRepository<NoticeMessage, Integer> {
@PostFilter("hasPermission(filterObject, 'READ')")
List<NoticeMessage> findAll();
@PostAuthorize("hasPermission(returnObject, 'READ')")
NoticeMessage getById(Integer id);
@SuppressWarnings("unchecked")
@PreAuthorize("hasPermission(#noticeMessage, 'WRITE')")
NoticeMessage save(@Param("noticeMessage") NoticeMessage noticeMessage);
}
@@ -0,0 +1,36 @@
package ru.otus.spring.rest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RestController;
import ru.otus.spring.model.NoticeMessage;
import ru.otus.spring.repository.NoticeMessageRepository;
import java.util.List;
@RestController
public class NoticeMessageController {
private final NoticeMessageRepository repository;
public NoticeMessageController(NoticeMessageRepository repository) {
this.repository = repository;
}
@GetMapping("/message")
public List<NoticeMessage> getAll() {
return repository.findAll();
}
@GetMapping("/message/{id}")
public NoticeMessage getById(@PathVariable("id") Integer id) {
var result = repository.getById( id );
return result;
}
@PutMapping("/message")
public NoticeMessage getById(NoticeMessage message) {
return repository.save(message);
}
}
@@ -0,0 +1,79 @@
package ru.otus.spring.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.ehcache.EhCacheFactoryBean;
import org.springframework.cache.ehcache.EhCacheManagerFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.acls.AclPermissionCacheOptimizer;
import org.springframework.security.acls.AclPermissionEvaluator;
import org.springframework.security.acls.domain.*;
import org.springframework.security.acls.jdbc.BasicLookupStrategy;
import org.springframework.security.acls.jdbc.JdbcMutableAclService;
import org.springframework.security.acls.jdbc.LookupStrategy;
import org.springframework.security.acls.model.PermissionGrantingStrategy;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import javax.sql.DataSource;
import java.util.Objects;
@Configuration
public class AclConfig {
@SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
@Autowired
private DataSource dataSource;
@Bean
public EhCacheBasedAclCache aclCache() {
return new EhCacheBasedAclCache(
Objects.requireNonNull(aclEhCacheFactoryBean().getObject()),
permissionGrantingStrategy(),
aclAuthorizationStrategy()
);
}
@Bean
public EhCacheFactoryBean aclEhCacheFactoryBean() {
EhCacheFactoryBean ehCacheFactoryBean = new EhCacheFactoryBean();
ehCacheFactoryBean.setCacheManager(aclCacheManager().getObject());
ehCacheFactoryBean.setCacheName("aclCache");
return ehCacheFactoryBean;
}
@Bean
public EhCacheManagerFactoryBean aclCacheManager() {
return new EhCacheManagerFactoryBean();
}
@Bean
public PermissionGrantingStrategy permissionGrantingStrategy() {
return new DefaultPermissionGrantingStrategy(new ConsoleAuditLogger());
}
@Bean
public AclAuthorizationStrategy aclAuthorizationStrategy() {
return new AclAuthorizationStrategyImpl(new SimpleGrantedAuthority("ROLE_EDITOR"));
}
@Bean
public MethodSecurityExpressionHandler defaultMethodSecurityExpressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
AclPermissionEvaluator permissionEvaluator = new AclPermissionEvaluator(aclService());
expressionHandler.setPermissionEvaluator(permissionEvaluator);
expressionHandler.setPermissionCacheOptimizer(new AclPermissionCacheOptimizer(aclService()));
return expressionHandler;
}
@Bean
public LookupStrategy lookupStrategy() {
return new BasicLookupStrategy(dataSource, aclCache(), aclAuthorizationStrategy(), new ConsoleAuditLogger());
}
@Bean
public JdbcMutableAclService aclService() {
return new JdbcMutableAclService(dataSource, lookupStrategy(), aclCache());
}
}
@@ -0,0 +1,22 @@
package ru.otus.spring.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class AclMethodSecurityConfiguration extends GlobalMethodSecurityConfiguration {
@SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
@Autowired
MethodSecurityExpressionHandler defaultMethodSecurityExpressionHandler;
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
return defaultMethodSecurityExpressionHandler;
}
}
@@ -0,0 +1,42 @@
package ru.otus.spring.security;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import java.util.ArrayList;
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain( HttpSecurity http ) throws Exception {
http
.csrf().disable()
.authorizeHttpRequests( ( authorize ) -> authorize
.antMatchers( "/**", "/" ).permitAll()
)
.formLogin();
return http.build();
}
@Bean
public InMemoryUserDetailsManager userDetailsService() {
var users = new ArrayList<UserDetails>();
users.add( User
.withDefaultPasswordEncoder().username( "admin" ).password( "password" ).roles( "EDITOR" )
.build() );
users.add( User
.withDefaultPasswordEncoder().username( "user" ).password( "password" ).roles( "USER" )
.build() );
users.add( User
.withDefaultPasswordEncoder().username( "someone" ).password( "password" ).roles( "SOMEONE" )
.build() );
return new InMemoryUserDetailsManager( users );
}
}
@@ -0,0 +1,44 @@
package ru.otus.spring.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.acls.domain.BasePermission;
import org.springframework.security.acls.domain.GrantedAuthoritySid;
import org.springframework.security.acls.domain.ObjectIdentityImpl;
import org.springframework.security.acls.domain.PrincipalSid;
import org.springframework.security.acls.model.MutableAcl;
import org.springframework.security.acls.model.MutableAclService;
import org.springframework.security.acls.model.ObjectIdentity;
import org.springframework.security.acls.model.Sid;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.otus.spring.model.NoticeMessage;
import ru.otus.spring.repository.NoticeMessageRepository;
@Service
public class NoticeService {
@Autowired
protected MutableAclService mutableAclService;
@Autowired
private NoticeMessageRepository repository;
public void add( NoticeMessage noticeMessage ) {
repository.save( noticeMessage );
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
final Sid owner = new PrincipalSid( authentication );
ObjectIdentity oid = new ObjectIdentityImpl( noticeMessage );
final Sid admin = new GrantedAuthoritySid("ROLE_EDITOR");
MutableAcl acl = mutableAclService.createAcl( oid );
acl.setOwner( owner );
acl.insertAce( acl.getEntries().size(), BasePermission.ADMINISTRATION, admin, true );
mutableAclService.updateAcl( acl );
}
}
@@ -0,0 +1,13 @@
spring:
h2:
console:
enabled: true
jpa:
hibernate:
ddl-auto: none
datasource:
url: jdbc:h2:mem:testdb
springdoc:
packages-to-scan: ru.otus.spring.rest
paths-to-match: /**
@@ -0,0 +1,28 @@
INSERT INTO system_message(id,content) VALUES
(1,'First Level Message'),
(2,'Second Level Message'),
(3,'Third Level Message');
INSERT INTO acl_sid (id, principal, sid) VALUES
(1, 1, 'admin'),
(2, 1, 'user'),
(3, 0, 'ROLE_EDITOR');
INSERT INTO acl_class (id, class) VALUES
(1, 'ru.otus.spring.model.NoticeMessage');
INSERT INTO acl_object_identity (id, object_id_class, object_id_identity, parent_object, owner_sid, entries_inheriting) VALUES
(1, 1, 1, NULL, 3, 0),
(2, 1, 2, NULL, 3, 0),
(3, 1, 3, NULL, 3, 0);
INSERT INTO acl_entry (id, acl_object_identity, ace_order, sid, mask,
granting, audit_success, audit_failure) VALUES
(1, 1, 1, 1, 1, 1, 1, 1),
(2, 1, 2, 1, 2, 1, 1, 1),
(3, 1, 3, 3, 1, 1, 1, 1),
(4, 2, 1, 2, 1, 1, 1, 1),
(5, 2, 2, 3, 1, 1, 1, 1),
(6, 3, 1, 3, 1, 1, 1, 1),
(7, 3, 2, 3, 2, 1, 1, 1);
@@ -0,0 +1,58 @@
create table IF NOT EXISTS system_message (id integer not null, content varchar(255), primary key (id));
CREATE TABLE IF NOT EXISTS acl_sid (
id bigint(20) NOT NULL AUTO_INCREMENT,
principal tinyint(1) NOT NULL,
sid varchar(100) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY unique_uk_1 (sid,principal)
);
CREATE TABLE IF NOT EXISTS acl_class (
id bigint(20) NOT NULL AUTO_INCREMENT,
class varchar(255) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY unique_uk_2 (class)
);
CREATE TABLE IF NOT EXISTS acl_entry (
id bigint(20) NOT NULL AUTO_INCREMENT,
acl_object_identity bigint(20) NOT NULL,
ace_order int(11) NOT NULL,
sid bigint(20) NOT NULL,
mask int(11) NOT NULL,
granting tinyint(1) NOT NULL,
audit_success tinyint(1) NOT NULL,
audit_failure tinyint(1) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY unique_uk_4 (acl_object_identity,ace_order)
);
CREATE TABLE IF NOT EXISTS acl_object_identity (
id bigint(20) NOT NULL AUTO_INCREMENT,
object_id_class bigint(20) NOT NULL,
object_id_identity bigint(20) NOT NULL,
parent_object bigint(20) DEFAULT NULL,
owner_sid bigint(20) DEFAULT NULL,
entries_inheriting tinyint(1) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY unique_uk_3 (object_id_class,object_id_identity)
);
ALTER TABLE acl_entry
ADD FOREIGN KEY (acl_object_identity) REFERENCES acl_object_identity(id);
ALTER TABLE acl_entry
ADD FOREIGN KEY (sid) REFERENCES acl_sid(id);
--
-- Constraints for table acl_object_identity
--
ALTER TABLE acl_object_identity
ADD FOREIGN KEY (parent_object) REFERENCES acl_object_identity (id);
ALTER TABLE acl_object_identity
ADD FOREIGN KEY (object_id_class) REFERENCES acl_class (id);
ALTER TABLE acl_object_identity
ADD FOREIGN KEY (owner_sid) REFERENCES acl_sid (id);
@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
</head>
<body>
Вам доступ запрещён!
</body>
</html>
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
</head>
<body>
<a th:href="@{login}">login</a>
<br>
<a th:href="@{logout}">logout</a>
<br>
<a th:href="@{h2-console}">h2-console</a>
<br>
<a th:href="@{swagger-ui.html}">/swagger</a>
</body>
</html>
+87
View File
@@ -0,0 +1,87 @@
<?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>ru.otus</groupId>
<artifactId>spring-framework-27-oauth</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.10</version>
</parent>
<properties>
<ehcache-core.version>2.6.11</ehcache-core.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.6.9</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>4.3.1</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator-core</artifactId>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>js-cookie</artifactId>
<version>2.1.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,13 @@
package ru.otus.spring.sso;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class GithubApplication {
public static void main(String[] args) {
SpringApplication.run( GithubApplication.class, args);
}
}
@@ -0,0 +1,13 @@
package ru.otus.spring.sso.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class IndexController {
@GetMapping("/")
public String indexPage() {
return "index";
}
}
@@ -0,0 +1,17 @@
package ru.otus.spring.sso.controller;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collections;
import java.util.Map;
@RestController
public class UserController {
@GetMapping("/user")
public Map<String, Object> user( @AuthenticationPrincipal OAuth2User principal) {
return Collections.singletonMap("name", principal.getAttribute("name"));
}
}
@@ -0,0 +1,26 @@
package ru.otus.spring.sso.security;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure( HttpSecurity http ) throws Exception {
http
.authorizeRequests( a -> a
.antMatchers( "/", "/error", "/webjars/**" ).permitAll()
.anyRequest().authenticated()
)
.exceptionHandling( e -> e
.authenticationEntryPoint( new HttpStatusEntryPoint( HttpStatus.UNAUTHORIZED ) )
)
.csrf().disable()
.logout( l -> l
.logoutSuccessUrl( "/" ).permitAll()
)
.oauth2Login();
}
}
@@ -0,0 +1,13 @@
spring:
security:
oauth2:
client:
registration:
github:
clientId: b08e2b651985f2440b7f
clientSecret: 891acf5785a5af14ebe4827c0d9606b3385304cd
logging:
level:
root: error
org.springframework.security: DEBUG
@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<title>Demo</title>
<meta name="description" content=""/>
<meta name="viewport" content="width=device-width"/>
<base href="/"/>
<link rel="stylesheet" type="text/css" href="/webjars/bootstrap/css/bootstrap.min.css"/>
<script type="text/javascript" src="/webjars/jquery/jquery.min.js"></script>
<script type="text/javascript" src="/webjars/bootstrap/js/bootstrap.min.js"></script>
</head>
<body>
<h1>Demo</h1>
<div class="container"></div>
</body>
</html>