- Notifications
You must be signed in to change notification settings - Fork2
Learn Spring Security by baby steps from zero to pro!
License
daggerok/spring-security-basics
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Learn Spring Security by baby steps from zero to pro! (Status: IN PROGRESS)
- Step 0: No security
- Step 1: Add authentication
- Step 2: Custom authentication
- Step 3: Add authorization
- Step 4: JavaEE and Spring Security
- Step 5.1: JDBC authentication
- Step 5.2: Spring Data JDBC authentication
- Step 5.3: Spring Data JPA authentication
- Step 6: Spring LDAP Security
- Versioning and releasing
- Resources and used links
let's use simple spring boot web app without security at all!
use needed dependencies inpom.xml
file:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency></dependencies>
add inApplication.java
file controller for index page:
@ControllerclassIndexPage {@GetMapping("/")Stringindex() {return"index.html"; }}
do not forget aboutsrc/main/resources/static/index.html
template file:
<!doctype html><htmllang="en"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><metahttp-equiv="X-UA-Compatible"content="ie=edge"><title>spring-security baby-steps</title></head><body><h1>Hello!</h1></body></html>
finally, to gracefully shutdown application under test on CI builds,add actuator dependency:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency></dependencies>
with according configurations inapplication.yaml
file:
spring:output:ansi:enabled:always---spring:profiles:cimanagement:endpoint:shutdown:enabled:trueendpoints:web:exposure:include:> shutdown
so, you can start application which is supports shutdown, like so:
java -jar /path/to/jar --spring.profiles.active=ci
use required dependencies:
<dependencies> <dependency> <groupId>com.codeborne</groupId> <artifactId>selenide</artifactId> <scope>test</scope> </dependency></dependencies>
implement Selenide test:
@Log4j2@AllArgsConstructorclassApplicationTestextendsAbstractTest {@Testvoidtest() {open("http://127.0.0.1:8080");// open home page...varh1 =$("h1");// find there <h1> tag...log.info("h1 html: {}",h1);h1.shouldBe(exist,visible)// element should be inside DOM .shouldHave(text("hello"));// textContent of the tag should// contains expected content... }}
see sources for implementation details.
build, run test and cleanup:
./mvnw -f step-0-application-without-securityjava -jar ./step-0-application-without-security/target/*jar --spring.profiles.active=ci&./mvnw -Dgroups=e2e -f step-0-test-application-without-securityhttp post :8080/actuator/shutdown
in this step we are going to implement simple authentication.it's mean everyone who logged in, can access all availableresources.
add required dependencies:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency></dependencies>
updateapplication.yaml
configuration with desired user password:
spring:security:user:password:pwd
tune little bit security config to bein able shutdown application with POST:we have to permit it and disable CSRF:
@EnableWebSecurityclassMyWebSecurityextendsWebSecurityConfigurerAdapter {@Overrideprotectedvoidconfigure(HttpSecurityhttp)throwsException {http.authorizeRequests() .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll() .anyRequest().authenticated() .and() .csrf().disable() .formLogin() ; }}
now, let's update test according to configured security as follows:
@Log4j2@AllArgsConstructorclassApplicationTestextendsAbstractTest {@Testvoidtest() {open("http://127.0.0.1:8080");// we should be redirected to login page, so lets authenticate!$("#username").setValue("user");$("#password").setValue("pwd").submit();// everything else is with no changes...varh1 =$("h1");log.info("h1 html: {}",h1);h1.shouldBe(exist,visible) .shouldHave(text("hello")); }}
build, run test and cleanup:
./mvnw -f step-0-application-without-securitySPRING_PROFILES_ACTIVE=ci java -jar ./step-0-application-without-security/target/*jar&./mvnw -Dgroups=e2e -f step-0-test-application-without-securityhttp post :8080/actuator/shutdown
let's add few users for authorization:
@EnableWebSecurityclassMyWebSecurityextendsWebSecurityConfigurerAdapter {@BeanPasswordEncoderpasswordEncoder() {returnPasswordEncoderFactories.createDelegatingPasswordEncoder(); }@Overrideprotectedvoidconfigure(AuthenticationManagerBuilderauth)throwsException {auth.inMemoryAuthentication() .withUser("user") .password(passwordEncoder().encode("password")) .roles("USER") .and() .withUser("admin") .password(passwordEncoder().encode("admin")) .roles("USER","ADMIN") ; }// ...}
now we can authenticate withusers
/password
oradmin
/admin
now let's add authorization, so we can distinguish that different usershave access to some resources where others are not!
in next configuration access to/admin
path:
@ControllerclassAdminPage {@GetMapping("admin")Stringindex() {return"admin/index.html"; }}
addadmin/index.html
file:
<!doctype html><htmllang="en"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><metahttp-equiv="X-UA-Compatible"content="ie=edge"><title>Admin Page | spring-security baby-steps</title></head><body><h2>Administration page</h2></body></html>
we can allow to users with admin role:
@EnableWebSecurityclassMyWebSecurityextendsWebSecurityConfigurerAdapter {@Overrideprotectedvoidconfigure(HttpSecurityhttp)throwsException {http.authorizeRequests() .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll() .antMatchers("/admin/**").hasRole("ADMIN") .anyRequest().authenticated() .and() .csrf().disable() .formLogin() ; }}
@Value@ConstructorBinding@ConfigurationProperties("test-application-props")classTestApplicationProps {StringbaseUrl;Useradmin;Useruser;@Value@ConstructorBindingstaticclassUser {Stringusername;Stringpassword; }}@Log4j2@Tag("e2e")@AllArgsConstructor@SpringBootTest(properties = {"test-application-props.user.username=user","test-application-props.user.password=password","test-application-props.admin.username=admin","test-application-props.admin.password=admin","test-application-props.base-url=http://127.0.0.1:8080",})classApplicationTest {ApplicationContextcontext;@Testvoidadmin_should_authorize() {varprops =context.getBean(TestApplicationProps.class);open(String.format("%s/admin",props.getBaseUrl()));$("#username").setValue(props.getAdmin().getUsername());$("#password").setValue(props.getAdmin().getPassword()).submit();varh2 =$("h2");log.info("h2 html: {}",h2);h2.shouldBe(exist,visible) .shouldHave(text("administration")); }@Testvoidtest_forbidden_403() {varprops =context.getBean(TestApplicationProps.class);open(String.format("%s/admin",props.getBaseUrl()));$("#username").setValue(props.getUser().getUsername());$("#password").setValue(props.getUser().getPassword()).submit();$(withText("403")).shouldBe(exist,visible);$(withText("Forbidden")).shouldBe(exist,visible); }@AfterEachvoidafter() {closeWebDriver(); }}
let's try use Spring Security together with JavaEE!NOTE: use spring version 4.x, not 5!
in this step we will configure JavaEE app for nextsets of security rules:
allowed for all:/
,/favicon.ico
,/api/health
,/login
,/logout
allowed for admins only:/admin
all other paths allowed only for authenticated users.
dependencies:
<dependencies> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-taglibs</artifactId> </dependency></dependencies>
JAX-RS application:
@ApplicationScoped@ApplicationPath("api")publicclassConfigextendsApplication { }@Path("")@RequestScoped@Produces(APPLICATION_JSON)publicclassHealthResource {@GET@Path("health")publicJsonObjecthello() {returnJson.createObjectBuilder() .add("status","UP") .build(); }}@Path("v1")@RequestScoped@Produces(APPLICATION_JSON)publicclassMyResource {@GET@Path("hello")publicJsonObjecthello() {returnJson.createObjectBuilder() .add("hello","world!") .build(); }}
Spring Security configuration:
@Configuration@EnableWebSecuritypublicclassSpringSecurityConfigextendsWebSecurityConfigurerAdapter {@Overrideprotectedvoidconfigure(AuthenticationManagerBuilderauth)throwsException {// @formatter:offauth.inMemoryAuthentication() .withUser("user") .password("password") .roles("USER") .and() .withUser("admin") .password("admin") .roles("ADMIN")// @formatter:on ; }@Overrideprotectedvoidconfigure(HttpSecurityhttp)throwsException {// @formatter:offhttp.authorizeRequests() .antMatchers("/","/favicon.ico","/api/health").permitAll() .antMatchers("/admin/**").hasRole("ADMIN") .anyRequest().authenticated() .and() .formLogin() .and() .logout() .logoutSuccessUrl("/") .clearAuthentication(true) .invalidateHttpSession(true) .deleteCookies("JSESSIONID") .and() .csrf().disable() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);// @formatter:on ; }}publicclassSecurityWebApplicationInitializerextendsAbstractSecurityWebApplicationInitializer {publicSecurityWebApplicationInitializer() {super(SpringSecurityConfig.class); }}
addsrc/main/resources/META-INF/beans.xml
file:
<?xml version="1.0" encoding="UTF-8"?><beansxmlns="http://xmlns.jcp.org/xml/ns/javaee"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"bean-discovery-mode="annotated"></beans>
finally, add HTML pages:
filesrc/main/webapp/index.html
:
<!doctype html><htmllang="en"><head><title>Hello!</title></head><body><h1>Hello!</h1></body></html>
filesrc/main/webapp/admin/index.html
:
<!doctype html><htmllang="en"><head><title>Admin</title></head><body><h1>Admin page</h1></body></html>
./mvnw -f step-4-java-ee-jaxrs-jboss-spring-security./mvnw -f step-4-java-ee-jaxrs-jboss-spring-security docker:build docker:start./mvnw -f step-4-test-java-ee-jboss-spring-security -Dgroups=e2e./mvnw -f step-4-java-ee-jaxrs-jboss-spring-security docker:stop docker:remove
let's use jdbc database as users / roles store.
security config:
@EnableWebSecurity@RequiredArgsConstructorclassMyWebSecurityextendsWebSecurityConfigurerAdapter {finalDataSourcedataSource;@Overrideprotectedvoidconfigure(AuthenticationManagerBuilderauth)throwsException {auth.jdbcAuthentication() .dataSource(dataSource) .usersByUsernameQuery(" select sec_username, sec_password, sec_enabled " +" from sec_users where sec_username=? " ) .authoritiesByUsernameQuery(" select sec_username, sec_authority " +" from sec_authorities where sec_username=? " ); ; }// ...}
sql schema and data:
dropindex if exists sec_authorities_idx;droptable if exists sec_authorities;droptable if exists sec_users;dropschema if exists"public";createschema "public";createtablesec_users ( sec_usernamevarchar(255)not nullprimary key, sec_passwordvarchar(1024)not null, sec_enabledbooleannot null);createtablesec_authorities ( sec_usernamevarchar(255)not null, sec_authorityvarchar(255)not null,constraint sec_authorities_fkforeign key (sec_username)references sec_users (sec_username));createunique indexsec_authorities_idxon sec_authorities (sec_username, sec_authority);insert into sec_users (sec_username, sec_password, sec_enabled)values ('user','{bcrypt}$2a$10$OlBp2JOK0/8xDjiVqh4OYOggr3tHTKfBcv82dso4fsnUPo66f5Ury', true),-- password ('admin','{bcrypt}$2a$10$OKPak8tw3jYSyqil/eNKz.U1nF/HtabOotUqi2ceeLuWdBsejH9yS', true);-- admininsert into sec_authorities (sec_username, sec_authority)values ('user','ROLE_USER'), ('admin','ROLE_ADMIN');
testing:
./mvnw -f step-5-jdbc-authentication clean package spring-boot:build-image docker-compose:upwhile! [[`curl -s -o /dev/null -w"%{http_code}" 0:8080/actuator/health`-eq 200 ]];do sleep 1s;echo -n'.';done./mvnw -f step-5-test-jdbc -Dgroups=e2e ./mvnw -f step-5-jdbc-authentication docker-compose:down
let's use spring-data-jdbc database as users / roles store.
add security entity, repository and service:
@With@Value@Table("sec_users")classSecurity {@Id@Column("sec_username")Stringusername;@Column("sec_password")Stringpassword;@Column("sec_enabled")booleanactive;@Column("sec_authority")Stringauthority;publicUserDetailstoUserDetails() {returnUser.builder() .username(username) .password(password) .disabled(!active) .accountExpired(!active) .credentialsExpired(!active) .authorities(AuthorityUtils.createAuthorityList(authority)) .build(); }}interfaceSecurityRepositoryextendsCrudRepository<Security,String> {@Query("select * from sec_users where sec_username = :username limit 1")Optional<Security>findFirstByUsername(@Param("username")Stringusername);}@Service@RequiredArgsConstructorclassSecurityServiceimplementsUserDetailsService {finalSecurityRepositorysecurityRepository;@OverridepublicUserDetailsloadUserByUsername(Stringusername)throwsUsernameNotFoundException {returnsecurityRepository.findFirstByUsername(username) .map(Security::toUserDetails) .orElseThrow(() ->newUsernameNotFoundException(String.format("User %s not found.",username))); }}
security config:
@EnableWebSecurity@RequiredArgsConstructorclassMyWebSecurityextendsWebSecurityConfigurerAdapter {finalSecurityServicesecurityService;@Overrideprotectedvoidconfigure(AuthenticationManagerBuilderauth)throwsException {auth.userDetailsService(securityService); }// ...}
sql schema and data:
dropindex if exists sec_users_authorities_idx;droptable if exists sec_users;dropschema if exists"public";createschema "public";createtablesec_users ( sec_usernamevarchar(255)not nullprimary key, sec_passwordvarchar(1024)not null, sec_enabledbooleannot null, sec_authorityvarchar(255)not null);createunique indexsec_users_authorities_idxon sec_users (sec_username, sec_authority);insert into sec_users (sec_username, sec_password, sec_enabled, sec_authority)values ('user','{bcrypt}$2a$10$OlBp2JOK0/8xDjiVqh4OYOggr3tHTKfBcv82dso4fsnUPo66f5Ury', true,'ROLE_USER'), ('admin','{bcrypt}$2a$10$OKPak8tw3jYSyqil/eNKz.U1nF/HtabOotUqi2ceeLuWdBsejH9yS', true,'ROLE_ADMIN');
testing:
./mvnw -f step-5-spring-data-jdbc-authentication clean package spring-boot:build-image docker-compose:up./mvnw -f step-5-test-jdbc -Dgroups=e2e ./mvnw -f step-5-spring-data-jdbc-authentication docker-compose:down
let's use spring-data-jpa this time.
required changes:
@Data@Entity@Setter(PROTECTED)@NoArgsConstructor(access =PROTECTED)@AllArgsConstructor(staticName ="of")@Table(name ="sec_users")classSecurity {@Id@Column(nullable =false,name ="sec_username")privateStringusername;@Column(nullable =false,name ="sec_password")privateStringpassword;@Column(nullable =false,name ="sec_enabled")privatebooleanactive;@Column(nullable =false,name ="sec_authority")privateStringauthority;publicUserDetailstoUserDetails() {returnUser.builder() .username(username) .password(password) .disabled(!active) .accountExpired(!active) .credentialsExpired(!active) .authorities(AuthorityUtils.createAuthorityList(authority)) .build(); }}interfaceSecurityRepositoryextendsCrudRepository<Security,String> {@QueryOptional<Security>findFirstByUsername(@Param("username")Stringusername);}@Service@RequiredArgsConstructorclassSecurityServiceimplementsUserDetailsService {finalSecurityRepositorysecurityRepository;@OverridepublicUserDetailsloadUserByUsername(Stringusername)throwsUsernameNotFoundException {returnsecurityRepository.findFirstByUsername(username) .map(Security::toUserDetails) .orElseThrow(() ->newUsernameNotFoundException(String.format("User %s not found.",username))); }}@EnableWebSecurity@RequiredArgsConstructorclassMyWebSecurityextendsWebSecurityConfigurerAdapter {finalSecurityServicesecurityService;@BeanPasswordEncoderpasswordEncoder() {returnPasswordEncoderFactories.createDelegatingPasswordEncoder(); }@Overrideprotectedvoidconfigure(AuthenticationManagerBuilderauth)throwsException {auth.userDetailsService(securityService); }@Overrideprotectedvoidconfigure(HttpSecurityhttp)throwsException {http.authorizeRequests() .requestMatchers(EndpointRequest.to("health","shutdown")).permitAll() .antMatchers("/","/favicon.ico","/assets/**").permitAll() .antMatchers("/admin/**").hasRole("ADMIN") .anyRequest().authenticated() .and() .csrf().disable() .formLogin() .and() .httpBasic() ; }}
_application.yaml` file:
spring:datasource:driver-class-name:org.postgresql.Driverurl:jdbc:postgresql://${POSTGRES_HOST:127.0.0.1}:${POSTGRES_PORT:5432}/${POSTGRES_DB:postgres}username:${POSTGRES_USER:postgres}password:${POSTGRES_PASSWORD:postgres}flyway:enabled:truejpa:database:postgresqlgenerate-ddl:falseshow-sql:truehibernate:ddl-auto:validateproperties:hibernate:temp:use_jdbc_metadata_defaults:false
db/migration
scripts:
createtablesec_users ( sec_usernamevarchar(255)not nullprimary key, sec_passwordvarchar(1024)not null, sec_enabledbooleannot null, sec_authorityvarchar(255)not null);createunique indexsec_users_authorities_idxon sec_users (sec_username, sec_authority);insert into sec_users (sec_username, sec_password, sec_enabled, sec_authority)values ('user','{bcrypt}$2a$10$OlBp2JOK0/8xDjiVqh4OYOggr3tHTKfBcv82dso4fsnUPo66f5Ury', true,'ROLE_USER'), ('admin','{bcrypt}$2a$10$OKPak8tw3jYSyqil/eNKz.U1nF/HtabOotUqi2ceeLuWdBsejH9yS', true,'ROLE_ADMIN');
testing:
# docker-compose -f step-5-spring-data-jpa-authentication/docker-compose.yaml up postgres./mvnw -f step-5-spring-data-jpa-authentication clean package spring-boot:build-image docker-compose:up./mvnw -f step-5-test-jdbc -Dgroups=e2e ./mvnw -f step-5-spring-data-jpa-authentication docker-compose:down
we will be releasing after each important step! so it will be easy simply checkout needed version from git tag.
release version without maven-release-plugin (when you aren't using *-SNAPSHOT version for development):
currentVersion=`./mvnw -q --non-recursive exec:exec -Dexec.executable=echo -Dexec.args='${project.version}'`git tag"v$currentVersion"./mvnw build-helper:parse-version -DgenerateBackupPoms=false -DgenerateBackupPoms=false versions:set \ -DnewVersion=\${parsedVersion.majorVersion}.\${parsedVersion.minorVersion}.\${parsedVersion.nextIncrementalVersion} \ -f step-4-java-ee-jaxrs-jboss-spring-security./mvnw build-helper:parse-version -DgenerateBackupPoms=false versions:set -DgenerateBackupPoms=false \ -DnewVersion=\${parsedVersion.majorVersion}.\${parsedVersion.minorVersion}.\${parsedVersion.nextIncrementalVersion}nextVersion=`./mvnw -q --non-recursive exec:exec -Dexec.executable=echo -Dexec.args='${project.version}'`git add.; git commit -am"v$currentVersion release."; git push --tags
increment version:
1.1.1?->1.1.2./mvnw build-helper:parse-version -DgenerateBackupPoms=false versions:set -DgenerateBackupPoms=false -DnewVersion=\${parsedVersion.majorVersion}.\${parsedVersion.minorVersion}.\${parsedVersion.nextIncrementalVersion}
current release version:
# 1.2.3-SNAPSHOT -> 1.2.3./mvnw build-helper:parse-version -DgenerateBackupPoms=false versions:set -DgenerateBackupPoms=false -DnewVersion=\${parsedVersion.majorVersion}.\${parsedVersion.minorVersion}.\${parsedVersion.incrementalVersion}
next snapshot version:
# 1.2.3? -> 1.2.4-SNAPSHOT./mvnw build-helper:parse-version -DgenerateBackupPoms=false versions:set -DgenerateBackupPoms=false -DnewVersion=\${parsedVersion.majorVersion}.\${parsedVersion.minorVersion}.\${parsedVersion.nextIncrementalVersion}-SNAPSHOT
About
Learn Spring Security by baby steps from zero to pro!
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Uh oh!
There was an error while loading.Please reload this page.