spring-backend-boilerplate

by melthaw

The modularized backend boilerplate based on Spring Boot Framework, easy to get started and add your...

131 Stars 17 Forks Last release: Not found Apache License 2.0 108 Commits 0 Releases

Available items

No Items, yet!

The developer of this repository has not created any items for sale yet. Need a bug fixed? Help with integration? A different license? Create a request here:

Overview

The quick development boilerplate based on Spring (Boot) Framework which covers the general case of Java backend application something like Account Module, Security Foundation, Audit System, File Upload/Download, Message Notification, Role Based Access Control etc.

And we also provide the CRUD sample to show how manage the item list , add new one item , update it or remove it.

We hope this boilerplate can help the users to focus on their business part . Before that , we will explain how it is designed and implemented.

Get Started

Please make sure the Java 8, Gradle 2.x and Mongodb are installed on your development machine.

We support Spring Cloud now, here is the User Guide - Spring Cloud Support

Build

Build using

Gradle
gradle clean build

Start up - by Docker

cd openapi
docker-compose build
docker-compose up -d

Start up - by Manual

Start API Server

And here is the minimized application.properties to start the boilerplate.

app.name=spring-backend-boilerplate
app.description=spring-backend-boilerplate

[email protected]outhink.in [email protected]le.com in.clouthink.daas.sbb.account.administrator.username=administrator in.clouthink.daas.sbb.account.administrator.cellphone=13000000000 in.clouthink.daas.sbb.account.administrator.password=Please_change_the_pwd

in.clouthink.daas.sbb.setting.system.name=spring-backend-boilerplate [email protected]xample.com in.clouthink.daas.sbb.setting.system.contactPhone=13000000000

logging.file=/var/sbb/log/server.log logging.level.*=INFO logging.level.in.clouthink.daas=DEBUG

server.port=8081 server.address=127.0.0.1 server.session-timeout=360000

spring.mvc.date-format=yyyy-MM-dd spring.mvc.favicon.enabled=false

multipart.enabled=true multipart.max-file-size=20Mb multipart.max-request-size=20Mb

spring.http.encoding.charset=UTF-8 spring.http.encoding.enabled=true spring.http.encoding.force=true

spring.jackson.date-format=yyyy-MM-dd'T'HH:mm:ss.sss'Z'

spring.data.mongodb.uri=mongodb://localhost:27017/spring-backend-boilerplate

Then we can start it with

> cd openapi/server
> gradle clean bootRun  -PjvmArgs="-Dspring.config.location=the_full_path_of_the_application.properties"

Start ApiDoc Server

And here is the minimized application.properties to start the boilerplate.

app.name=spring-backend-boilerplate-api-doc
app.description=spring backend boilerplate api doc

server.port=8082 server.address=127.0.0.1 server.session-timeout=360000

Then we can start it with

> cd openapi/doc
> gradle clean bootRun  -PjvmArgs="-Dspring.config.location=the_full_path_of_the_application.properties"

After the swagger doc server booted, open browser and visit the api doc at

http://127.0.0.1:8082/swagger-ui.html

Features

Modularization

First we design a modularized system (Thanks Spring Boot and Gradle), our goal is to simple add or remove one module without changing the Application , except the foundational modules.

All these come to reality are belongs to the features of Spring Boot Starter.

  • Provide the auto configuration for each module.
  • Tell spring how to enable the auto configuration.

Example

Message module for example

Auto Configuration

@Configuration
@Import({MockSmsModuleConfiguration.class, SmsHistoryModuleConfiguration.class})
public class DummySmsRestModuleConfiguration {

}

Starter by spring.factories

#message/sms/starter/src/main/resources/META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
in.clouthink.daas.sbb.sms.DummySmsRestModuleConfiguration

Practise

Remove the message module in the boilerplate won't break the application, because the message is event-driven , The other modules dispatch event to the event bus, the event bus simply discard it if the message module not found.

//openapi/server/build.gradle
dependencies {

...
compile project(':sample/setting/starter')

compile project(':message/sms/starter')

compile project(':storage/starter')
...

}

Simply remove the module configuration from the startup script.

//openapi/server/build.gradle
dependencies {

...
compile project(':sample/setting/starter')

//after
//compile project(':message/sms/starter')

compile project(':storage/starter')
...

}

Without Starter

And we also force the module convention that's separating the abstraction and implementation, then your can switch from one implementation to another easily.

For example, we provide more than one implementation module for the storage abstraction (:storage/core).

@Import({GridfsModuleConfiguration.class})
public class StorageRestModuleConfiguration {

}

If you want upload the file and save it to the local storage ( your runtime server's file system ), just import another one and replace it as follow

@Import({LocalStorageModuleConfiguration.class})
public class StorageRestModuleConfiguration {

}

Security

Foundation

Spring Security is a powerful security framework , it's easy to customize and extend . Based on the flexibility provided by Spring Security , we add more interesting feature to it like multi-factor authentication, user device, audit and pluggable account system.

Context

Yes, Spring Security provides the context named

org.springframework.security.core.context.SecurityContext
and the corresponding helper class
org.springframework.security.core.context.SecurityContextHolder
.

Why we need another one?

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
    throw some exception here?
}

Every time we retrieve the authentication , we must check the authentication , duplicated code everywhere.

What we supposed is that an exception is thrown automatically if no authentication found in the context,
not check it and throw it by manual.

So we provide one new generic interface in module(:security/core)

public interface SecurityContext {

/**
 * @return current user , or null if not authenticated
 */
T currentUser();

/**
 * @return the current user
 * @throw AuthenticationRequiredException if not authenticated
 */
T requireUser();

}

And we provide one helper named

SecurityContexts
to load the implementation. Here is the sample:
User user = (User)SecurityContexts.getContext().requireUser();

As you see,

requireUser()
is invoked which means if no authenticated user found in the context, it will throw one AuthenticationRequiredException.

The

SecurityContexts
requires that the implementation must follow the Java SPI Spec and provide it in the file as follow
META-INF/services/in.clouthink.daas.sbb.security.SecurityContext

Authentication & Authorization

First let's list the extension points what we implemented for Spring Security.

User
  • org.springframework.security.authentication.AuthenticationProvider
    • org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider
  • org.springframework.security.core.userdetails.User
  • org.springframework.security.core.userdetails.UserDetailsService

Login & Logout
  • org.springframework.security.web.AuthenticationEntryPoint
  • org.springframework.security.web.authentication.AuthenticationFailureHandler
  • org.springframework.security.web.authentication.AuthenticationSuccessHandler
  • org.springframework.security.web.authentication.logout.LogoutSuccessHandler

Access
  • org.springframework.security.web.access.AccessDeniedHandler
    • org.springframework.security.web.access.AccessDeniedHandlerImpl
  • org.springframework.security.access.expression.SecurityExpressionHandler
    • org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler
  • org.springframework.security.web.access.expression.WebSecurityExpressionRoot

The backend is designed as a restful service provider, so the security foundation must has the ability to process the restful request and response , not only the web page request and response.

And our implementations are listed as follow:

User
* in.clouthink.daas.sbb.security.impl.spring.UserDetailsAuthenticationProviderImpl * in.clouthink.daas.sbb.security.impl.spring.UserDetails * in.clouthink.daas.sbb.security.impl.spring.UserDetailsServiceImpl

Login & Logout
* in.clouthink.daas.sbb.security.impl.spring.rest.AuthenticationEntryPointRestImpl * in.clouthink.daas.sbb.security.impl.spring.rest.AuthenticationFailureHandlerRestImpl * in.clouthink.daas.sbb.security.impl.spring.rest.AuthenticationSuccessHandlerRestImpl * in.clouthink.daas.sbb.security.impl.spring.rest.LogoutSuccessHandlerRestImpl

Access
* in.clouthink.daas.sbb.security.impl.spring.rest.AccessDeniedHandlerRestImpl * in.clouthink.daas.sbb.rbac.impl.spring.security.RbacWebSecurityExpressionHandler * in.clouthink.daas.sbb.rbac.impl.spring.security.RbacWebSecurityExpressionRoot

How to configure Spring Security

Please refer to

in.clouthink.daas.sbb.openapi.OpenApiSecurityConfigurer
.

First , export the Spring Security extension points implementation.

    @Bean
    public AuthenticationProvider authenticationProvider() {
        return new UserDetailsAuthenticationProviderImpl();
    }

@Bean
public UserDetailsService userDetailsService() {
    return new UserDetailsServiceImpl();
}

@Bean
public AuthenticationSuccessHandler authenticationSuccessHandlerImpl() {
    return new AuthenticationSuccessHandlerRestImpl();
}

@Bean
public AuthenticationFailureHandler authenticationFailureHandlerImpl() {
    return new AuthenticationFailureHandlerRestImpl();
}

@Bean
public AccessDeniedHandler accessDeniedHandlerImpl() {
    return new AccessDeniedHandlerRestImpl();
}

@Bean
public LogoutSuccessHandler logoutSuccessHandlerImpl() {
    return new LogoutSuccessHandlerRestImpl();
}

@Bean
public AuthenticationEntryPoint authenticationEntryPointImpl() {
    return new AuthenticationEntryPointRestImpl();
}

@Bean
public AccessDecisionManager accessDecisionManager() {
    List<accessdecisionvoter extends object>&gt; decisionVoters = new ArrayList&lt;&gt;();
    decisionVoters.add(new RoleVoter());
    decisionVoters.add(new AuthenticatedVoter());
    decisionVoters.add(webExpressionVoter());
    return new AffirmativeBased(decisionVoters);
}

@Bean
public WebExpressionVoter webExpressionVoter() {
    WebExpressionVoter result = new WebExpressionVoter();
    result.setExpressionHandler(rbacWebSecurityExpressionHandler());
    return result;
}

@Bean
public SecurityExpressionHandler rbacWebSecurityExpressionHandler() {
    return new RbacWebSecurityExpressionHandler();
}

@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.authenticationProvider(authenticationProvider())
        .eraseCredentials(true)
        .userDetailsService(userDetailsService());
}

Then configure the authentication part

    private void configLogin(HttpSecurity http) throws Exception {
        http.csrf()
            .disable()
            .formLogin()
            .loginPage("/login")
            .permitAll()
            .successHandler(authenticationSuccessHandlerImpl())
            .failureHandler(authenticationFailureHandlerImpl())
            .loginProcessingUrl("/login")
            .usernameParameter("username")
            .passwordParameter("password")
            .and()
            .logout()
            .logoutUrl("/logout")
            .logoutSuccessHandler(logoutSuccessHandlerImpl())
            .invalidateHttpSession(true)
            .deleteCookies("JSESSIONID")
            .permitAll()
            .and()
            .rememberMe()
            .key("PLEASE_CHANGE_THIS");
    }

Finally configure the authorization part

    private void configAccess(HttpSecurity http) throws Exception {
        http.headers().frameOptions().disable();

    http.authorizeRequests()
        .accessDecisionManager(accessDecisionManager())
        .antMatchers("/", "/static/**", "/login**", "/guest/**")
        .permitAll()
        .antMatchers("/api/shared/**")
        .hasRole("USER")
        .antMatchers("/api/_devops_/**")
        .hasRole("ADMIN")
        .antMatchers("/api/**")
        .access("passRbacCheck")
        .and()
        .exceptionHandling()
        .authenticationEntryPoint(authenticationEntryPointImpl())
        .accessDeniedHandler(accessDeniedHandlerImpl());
}

Audit

The daas-audit is a simple and quick audit abstraction lib for spring mvc http request. Please go https://github.com/melthaw/spring-mvc-audit to get more detail about it. Here we only explain what we extended and customized.

The module(:audit/impl) implements the daas-audit's AuditEvent SPI including:

  • in.clouthink.daas.audit.core.MutableAuditEvent
    • in.clouthink.daas.sbb.audit.domain.model.AuditEvent
  • in.clouthink.daas.audit.spi.AuditEventPersister
    • in.clouthink.daas.sbb.audit.spiImpl.AuditEventPersisterImpl

And the login history is supported which is not covered in daas-audit.

  • in.clouthink.daas.sbb.audit.domain.model.AuthEvent
  • in.clouthink.daas.sbb.audit.service.AuthEventService

Here is the configuration to enable the audit module

@EnableAudit
public class SpringBootApplication extends SpringBootServletInitializer {

@Bean
public AuditEventPersister auditEventPersisterImpl() {
    return new AuditEventPersisterImpl();
}

@Bean
public AuditConfigurer auditConfigurer() {
    return result -&gt; {
        result.setSecurityContext(new SecurityContextAuditImpl());
        result.setAuditEventPersister(auditEventPersisterImpl());
        result.setErrorDetailRequired(true);
    };
}

public static void main(String[] args) { 
    AuditRestModuleConfiguration.class,
    ...
    SpringBootApplication.class
}

}

File Storage

The daas-fss is APIs which make storing the blob file easy and simple. Please go https://github.com/melthaw/spring-file-storage-service to get more detail about it. Here we only explain what we extended and customized.

Now we provide three file storage implementation * aliyun oss (:storage/alioss) * mongodb gridfs (:storage/gridfs) * local file system (:storage/localfs)

Because different storage service stores the file in different system , the download url goes to different as well.

Here is the abstraction we supplied to extend.

  • in.clouthink.daas.sbb.storage.spi.DownloadUrlProvider

For example, if you choose the :storage/localfs , the download url goes to:

public class LocalfsDownloadUrlProvider implements DownloadUrlProvider {

@Autowired
private LocalfsConfigureProperties localfsConfigureProperties;

@Autowired
private FileObjectService fileObjectService;

@Override
public String getDownloadUrl(String id) {
    FileObject fileObject = fileObjectService.findById(id);
    if (fileObject == null) {
        throw new FileNotFoundException(id);
    }

    return localfsConfigureProperties.getDowloadUrlPrefix() + fileObject.getFinalFilename();
}

}

Here is the configuration to enable the

Storage
module
public class SpringBootApplication extends SpringBootServletInitializer {

public static void main(String[] args) { 
    StorageModuleConfiguration.class,
    ...
    SpringBootApplication.class
}

}

Message

The daas-edm is a lightweight event driven message framework based on reactor. Please go https://github.com/melthaw/spring-event-driven-message to get more detail about it. Here we only explain what we extended and customized.

SMS

In the boilerplate we choose the aliyun SMS as example . It's simple and easy to integrate your SMS provider , just implement the following interface.

in.clouthink.daas.edm.sms.SmsSender

We also supply one dummy module(:message/sms/mock) which can be used in the development ENV.

//for development
@Import({SmsAliyunModuleConfiguration.class})
//for production
@Import({DummySmsModuleConfiguration.class})

One more thing, the SMS history can be saved if you import the history module (:message/sms/history). By default we enable the SMS history in the boilerplate, if you don't like it, simple remove the import part to disable this feature.

@Configuration
@Import({SmsHistoryModuleConfiguration.class})
public class SmsRestModuleConfiguration {

}

Here is the configuration to enable the

Message
module
public class SpringBootApplication extends SpringBootServletInitializer {

public static void main(String[] args) { 
    SmsRestModuleConfiguration.class,
    ...
    SpringBootApplication.class
}

}

Configuration

application.properties

account

in.clouthink.daas.sbb.account.password.salt=
in.clouthink.daas.sbb.account.administrator.email=
in.clouthink.daas.sbb.account.administrator.username=
in.clouthink.daas.sbb.account.administrator.cellphone=
in.clouthink.daas.sbb.account.administrator.password=

storage

#alioss
in.clouthink.daas.sbb.storage.alioss.keyId=
in.clouthink.daas.sbb.storage.alioss.secret=
in.clouthink.daas.sbb.storage.alioss.ossDomain=
in.clouthink.daas.sbb.storage.alioss.imgDomain=
in.clouthink.daas.sbb.storage.alioss.defaultBucket=
in.clouthink.daas.sbb.storage.alioss.buckets.key1=
in.clouthink.daas.sbb.storage.alioss.buckets.key2=

sms

in.clouthink.daas.sbb.sms.aliyun.area=
in.clouthink.daas.sbb.sms.aliyun.accessKey=
in.clouthink.daas.sbb.sms.aliyun.accessSecret=
in.clouthink.daas.sbb.sms.aliyun.signature=
in.clouthink.daas.sbb.sms.aliyun.smsEndpoint=
in.clouthink.daas.sbb.sms.aliyun.templateId=

setting

in.clouthink.daas.sbb.setting.system.name=
in.clouthink.daas.sbb.setting.system.contactEmail=
in.clouthink.daas.sbb.setting.system.contactPhone=

resource

The resource is the term we called in RBAC, which is protected by authorization system. The resource should be a rest endpoint,or the visible menu item in the GUI , even a Create button in a page.

We design a resource registry SPI as

  • in.clouthink.daas.sbb.rbac.spi.ResourceProvider

All the resource provider implementation only required to implement it as a spring bean. The boilerplate scans and discovers it , and register the resources automatically.

The classic resource is the menu and action which are granted to the role and accessed control by role permission. We design the pluggable menu & action module , and supply the annotation to make it easy to use.

Here is the example

@EnableMenu(pluginId = "plugin:menu:sample",
            extensionPointId = Menus.ROOT_EXTENSION_POINT_ID,
            menu = {@Menu(virtual = true,
                          code = "menu:dashboard:sample",
                          name = "sample",
                          order = 100,
                          metadata = {@Metadata(key = "icon", value = "fa fa-gear")},
                          extensionPoint = {@ExtensionPoint(id = "extension:menu:sample")}),

                @Menu(virtual = true,
                      code = "menu:dashboard:system",
                      name = "system",
                      order = 200,
                      metadata = {@Metadata(key = "icon", value = "fa fa-gear")},
                      extensionPoint = {@ExtensionPoint(id = "extension:menu:system")}),

        })

More detail about the usage please check out the description of the Java file.

Sample - new business module

TODO

Appendix - Development ENV

IDEA - how to import the project to IDEA IDE

> gradle cleanIdea
> gradle idea

IDEA - how to debug in IDEA IDE

Create new debug configuration (type of gradle), and pop it with following value.

name

value
Gradle Project spring-backend-boilerplate:openapi:server
Tasks clean bootRun
VM Options
Script parameters -PjvmArgs="-Dspring.config.location=/var/sbb/etc/openapi/application.properties"

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.