Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Learn Spring Security by baby steps from zero to pro!

License

NotificationsYou must be signed in to change notification settings

daggerok/spring-security-basics

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

29 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Learn Spring Security by baby steps from zero to pro! (Status: IN PROGRESS)

Table of Content

step: 0

let's use simple spring boot web app without security at all!

application

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

test application

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

step: 1

in this step we are going to implement simple authentication.it's mean everyone who logged in, can access all availableresources.

application

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()    ;  }}

test application

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

step: 2

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

step: 3

now let's add authorization, so we can distinguish that different usershave access to some resources where others are not!

application

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()    ;  }}

test application

@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();  }}

step: 4

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,/logoutallowed for admins only:/adminall other paths allowed only for authenticated users.

application

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>

test application

./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

step: 5.1

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

step: 5.2

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

step: 5.3

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

maven

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

resources


[8]ページ先頭

©2009-2025 Movatter.jp