01 - Introduction
git
02 - Introduction to Spring Security
002 Application Security 101
003 Introducing Spring Security
004 OWASP Common Web Vulnerabilities
005 Cross Site Scripting - XSS
006 Cross Site Forgery
03 - HTTP Basic Auth
002 Overview of HTTP Basic Authentication
003 SFG Brewery Code Review
004 Spring Security Default Basic Auth
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
项目先 package 然后再 run springboot 启动类
/logout 就是退出
005 Customizing User Name and Password
# ...
spring.security.user.name=spring
spring.security.user.password=123
006 Testing Spring Security with JUnit 5
// BeerControllerT.java
package guru.sfg.brewery.web.controllers;
import guru.sfg.brewery.repositories.BeerInventoryRepository;
import guru.sfg.brewery.repositories.BeerRepository;
import guru.sfg.brewery.repositories.BreweryRepository;
import guru.sfg.brewery.repositories.CustomerRepository;
import guru.sfg.brewery.services.BeerService;
import guru.sfg.brewery.services.BreweryService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest
public class BeerControllerT {
@Autowired
WebApplicationContext wac;
MockMvc mockMvc;
@MockBean
BeerRepository beerRepository;
@MockBean
BeerInventoryRepository beerInventoryRepository;
@MockBean
BreweryRepository breweryRepository;
@MockBean
CustomerRepository customerRepository;
@MockBean
BreweryService breweryService;
@MockBean
BeerService beerService;
@BeforeEach
public void setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(wac).apply(springSecurity()).build();
}
@WithMockUser("spring") // 不加这个测试就会提示未认证
@Test
public void findBeers() throws Exception {
mockMvc.perform(get("/beers/find")).andExpect(status().isOk()).andExpect(view().name("beers/findBeers")).andExpect(model().attributeExists("beer"));
}
}
007 Testing HTTP Basic Auth
// BeerControllerT.java
package guru.sfg.brewery.web.controllers;
import guru.sfg.brewery.repositories.BeerInventoryRepository;
import guru.sfg.brewery.repositories.BeerRepository;
import guru.sfg.brewery.repositories.BreweryRepository;
import guru.sfg.brewery.repositories.CustomerRepository;
import guru.sfg.brewery.services.BeerService;
import guru.sfg.brewery.services.BreweryService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest
public class BeerControllerT {
@Autowired
WebApplicationContext wac;
MockMvc mockMvc;
@MockBean
BeerRepository beerRepository;
@MockBean
BeerInventoryRepository beerInventoryRepository;
@MockBean
BreweryRepository breweryRepository;
@MockBean
CustomerRepository customerRepository;
@MockBean
BreweryService breweryService;
@MockBean
BeerService beerService;
@BeforeEach
public void setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(wac).apply(springSecurity()).build();
}
@WithMockUser("spring") // 不加这个测试就会提示未认证
@Test
public void findBeers() throws Exception {
mockMvc.perform(get("/beers/find"))
.andExpect(status().isOk())
.andExpect(view().name("beers/findBeers"))
.andExpect(model().attributeExists("beer"));
}
@Test
public void findBeersWithHttpBasic() throws Exception {
mockMvc.perform(get("/beers/find").with(httpBasic("foo", "bar")))
.andExpect(status().isOk())
.andExpect(view().name("beers/findBeers"))
.andExpect(model().attributeExists("beer"));
}
@Test
public void findBeersWithHttpBasicAuth() throws Exception {
mockMvc.perform(get("/beers/find").with(httpBasic("spring", "123")))
.andExpect(status().isOk())
.andExpect(view().name("beers/findBeers"))
.andExpect(model().attributeExists("beer"));
}
}
008 Spring Security Filter Chain
spring security 提供了很多现成的过滤器链来进行安全防护
04 - Spring Security Java Configuration
002 Permit All with URL Pattern Matching
// IndexControllerT
package guru.sfg.brewery.web.controllers;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest
public class IndexControllerT extends BaseIt {
@Test
public void testGetIndexSlash() throws Exception {
super.mockMvc.perform(get("/"))
.andExpect(status().isOk());
}
}
// BaseIt
package guru.sfg.brewery.web.controllers;
import guru.sfg.brewery.repositories.BeerInventoryRepository;
import guru.sfg.brewery.repositories.BeerRepository;
import guru.sfg.brewery.repositories.BreweryRepository;
import guru.sfg.brewery.repositories.CustomerRepository;
import guru.sfg.brewery.services.BeerService;
import guru.sfg.brewery.services.BreweryService;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
public abstract class BaseIt {
@Autowired
WebApplicationContext wac;
MockMvc mockMvc;
@MockBean
BeerRepository beerRepository;
@MockBean
BeerInventoryRepository beerInventoryRepository;
@MockBean
BreweryRepository breweryRepository;
@MockBean
CustomerRepository customerRepository;
@MockBean
BreweryService breweryService;
@MockBean
BeerService beerService;
@BeforeEach
public void setUp() {
this.mockMvc = MockMvcBuilders
.webAppContextSetup(wac)
.apply(springSecurity())
.build();
}
}
这里配置了 http 的安全配置
设置访问 / 路径的所有请求不需要认证
package guru.sfg.brewery.web.controllers;
import org.springframework.context.annotation.Configuration;
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;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorize -> {
// 允许所有访问 / 路径的请求
authorize.antMatchers("/").permitAll();
})
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().and()
.httpBasic();
}
}
此时再运行测试(indexController),会通过
如果静态资源被拦截了
003 HTTP Method Matching
package guru.sfg.brewery.web.controllers;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@WebMvcTest
public class BeerRestControllerIT extends BaseIt {
@Test
public void findBeers() throws Exception {
mockMvc.perform(get("/api/v1/beer/"))
.andExpect(status().isOk());
}
@Test
public void findBeerById() throws Exception {
mockMvc.perform(get("/api/v1/beer/97df8c39-98c4-4ae8-b663-453e8e19c311"))
.andExpect(status().isOk());
}
}
package guru.sfg.brewery.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorize -> {
// 允许所有访问 / 路径的请求
authorize.antMatchers("/", "/webjars/**").permitAll()
.antMatchers("/beers/find", "/beers*").permitAll()
.antMatchers(HttpMethod.GET, "/api/v1/beer/**").permitAll();
})
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().and()
.httpBasic();
}
}
004 Spring MVC Path Matchers
package guru.sfg.brewery.web.controllers.api;
import guru.sfg.brewery.web.controllers.BaseIt;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@WebMvcTest
public class BeerRestControllerIT extends BaseIt {
@Test
public void findBeers() throws Exception {
// ...
}
@Test
public void findBeerById() throws Exception {
// ...
}
@Test
public void findBeerByUpc() throws Exception {
mockMvc.perform(get("/api/v1/beerUpc/863123420036"))
.andExpect(status().isOk());
}
}
package guru.sfg.brewery.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorize -> {
// 允许所有访问 / 路径的请求
authorize.antMatchers("/", "/webjars/**").permitAll()
.antMatchers("/beers/find", "/beers*").permitAll()
.antMatchers(HttpMethod.GET, "/api/v1/beer/**").permitAll()
.mvcMatchers(HttpMethod.GET, "/api/v1/beerUpc/{upc}").permitAll();
})
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().and()
.httpBasic();
}
}
05 - In Memory Authentication Provider
002 Spring Security Authentication Process
003 User Details Service
package guru.sfg.brewery.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
// ...
}
@Bean
@Override
public UserDetailsService userDetailsServiceBean() throws Exception {
// 创建两个用户
UserDetails admin = User.withDefaultPasswordEncoder()
.username("spring")
.password("123")
.roles("ADMIN")
.build();
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("pass")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(admin, user);
}
}
package guru.sfg.brewery.web.controllers;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.security.test.context.support.WithMockUser;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest
public class BeerControllerT extends BaseIt {
@Test
public void initCreationForm() throws Exception {
mockMvc.perform(get("/beers/new").with(httpBasic("user", "pass")))
.andExpect(status().isOk())
.andExpect(view().name("beers/createBeer"))
.andExpect(model().attributeExists("beer"));
}
@WithMockUser("spring") // 不加这个测试就会提示未认证
@Test
public void findBeers() throws Exception {
// ...
}
@Test
public void findBeersWithHttpBasic() throws Exception {
// ...
}
@Test
public void findBeersWithHttpBasicAuth() throws Exception {
// ...
}
}
004 In Memory Authentication Fluent API
package guru.sfg.brewery.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
// ...
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("spring")
.password("123")
.roles("ADMIN")
.and()
.withUser("user")
.password("pass")
.roles("USER");
}
}
重新运行之前的测试,会报错,我们需要在密码前面加上加密策略
package guru.sfg.brewery.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
// ...
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("spring")
.password("{noop}123")
.roles("ADMIN")
.and()
.withUser("user")
.password("{noop}pass")
.roles("USER");
}
}
06 - Password Security
002 Password Encoding
003 MD5 Hash and Password Salt
package guru.sfg.brewery.web.controllers;
import org.junit.jupiter.api.Test;
import org.springframework.util.DigestUtils;
public class PasswordEncodingTests {
static final String PASSWORD = "pass";
@Test
void hashingExample() {
// 哈希的结果是相同的
// 1a1dc91c907325c69271ddf0c944bc72
System.out.println(DigestUtils.md5DigestAsHex(PASSWORD.getBytes()));
System.out.println(DigestUtils.md5DigestAsHex(PASSWORD.getBytes()));
// 我之前每次都加上当前分钟单位的时间戳的想法就是动态加盐
String salted = PASSWORD + "ThisIsMySaltValue";
// 5ab82dc28edd60cc752e1b10cca0f96f
System.out.println(DigestUtils.md5DigestAsHex(salted.getBytes()));
}
}
004 NoOp Password Encoder
package guru.sfg.brewery.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 指定使用什么密码加密编码器
@Bean
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorize -> {
// 允许所有访问 / 路径的请求
authorize.antMatchers("/", "/webjars/**").permitAll()
.antMatchers("/beers/find", "/beers*").permitAll()
.antMatchers(HttpMethod.GET, "/api/v1/beer/**").permitAll()
.mvcMatchers(HttpMethod.GET, "/api/v1/beerUpc/{upc}").permitAll();
})
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().and()
.httpBasic();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("spring")
// .password("{noop}123")
.password("123")
.roles("ADMIN")
.and()
.withUser("user")
.password("pass")
// .password("{noop}pass")
.roles("USER");
auth.inMemoryAuthentication().withUser("scott").password("tiger").roles("CUSTOMER");
}
}
package guru.sfg.brewery.web.controllers;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.DigestUtils;
public class PasswordEncodingTests {
static final String PASSWORD = "pass";
@Test
public void testNoOp() {
PasswordEncoder noOp = NoOpPasswordEncoder.getInstance();
System.out.println(noOp.encode(PASSWORD));
}
@Test
public void hashingExample() {
// 哈希的结果是相同的
// 1a1dc91c907325c69271ddf0c944bc72
System.out.println(DigestUtils.md5DigestAsHex(PASSWORD.getBytes()));
System.out.println(DigestUtils.md5DigestAsHex(PASSWORD.getBytes()));
// 我之前每次都加上当前分钟单位的时间戳的想法就是动态加盐
String salted = PASSWORD + "ThisIsMySaltValue";
// 5ab82dc28edd60cc752e1b10cca0f96f
System.out.println(DigestUtils.md5DigestAsHex(salted.getBytes()));
}
}
005 LDAP Password Encoder
package guru.sfg.brewery.web.controllers;
import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.password.LdapShaPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.DigestUtils;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class PasswordEncodingTests {
static final String PASSWORD = "pass";
@Test
public void testLdap() {
PasswordEncoder ldap = new LdapShaPasswordEncoder();
// ldap每次都加随机的盐
System.out.println(ldap.encode(PASSWORD));
System.out.println(ldap.encode(PASSWORD));
String encodedPwd = ldap.encode(PASSWORD);
assertTrue(ldap.matches(PASSWORD, encodedPwd));
}
@Test
public void testNoOp() {
// ...
}
@Test
public void hashingExample() {
// ...
}
}
package guru.sfg.brewery.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.LdapShaPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 指定使用什么密码加密编码器
@Bean
PasswordEncoder passwordEncoder() {
// return NoOpPasswordEncoder.getInstance();
return new LdapShaPasswordEncoder();
}
@Override
public void configure(HttpSecurity http) throws Exception {
// ...
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("spring")
// .password("{noop}123")
.password("123")
.roles("ADMIN")
.and()
.withUser("user")
// 还可以设置加盐后的密码
.password("{SSHA}QKElFOKPdGDjeugAdOJrSFaXV8gkQxUJoUO9wQ==")
// .password("pass")
// .password("{noop}pass")
.roles("USER");
auth.inMemoryAuthentication().withUser("scott").password("tiger").roles("CUSTOMER");
}
}
006 SHA-256 Password Encoder
package guru.sfg.brewery.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.LdapShaPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.StandardPasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 指定使用什么密码加密编码器
@Bean
PasswordEncoder passwordEncoder() {
// return NoOpPasswordEncoder.getInstance();
// return new LdapShaPasswordEncoder();
return new StandardPasswordEncoder();
}
@Override
public void configure(HttpSecurity http) throws Exception {
// ...
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("spring")
// .password("{noop}123")
.password("123").roles("ADMIN").and().withUser("user").password("fbfa64628d6d1f9b071aeb184ca48edc46153500823a11e21c9dba29ec248bc24bfc2da3bb0525b5") // sha256
// .password("{SSHA}QKElFOKPdGDjeugAdOJrSFaXV8gkQxUJoUO9wQ==") // ldap
// .password("pass")
// .password("{noop}pass")
.roles("USER");
auth.inMemoryAuthentication().withUser("scott").password("tiger").roles("CUSTOMER");
}
}
package guru.sfg.brewery.web.controllers;
import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.password.LdapShaPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.StandardPasswordEncoder;
import org.springframework.util.DigestUtils;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class PasswordEncodingTests {
static final String PASSWORD = "pass";
@Test
public void testSha256() {
PasswordEncoder sha256 =new StandardPasswordEncoder();
// ldap每次都加随机的盐
System.out.println(sha256.encode(PASSWORD));
System.out.println(sha256.encode(PASSWORD));
String encodedPwd = sha256.encode(PASSWORD);
assertTrue(sha256.matches(PASSWORD, encodedPwd));
}
@Test
public void testLdap() {
// ...
}
@Test
public void testNoOp() {
// ...
}
@Test
public void hashingExample() {
// ...
}
}
007 BCrypt Password Encoder
package guru.sfg.brewery.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 指定使用什么密码加密编码器
@Bean
PasswordEncoder passwordEncoder() {
// return NoOpPasswordEncoder.getInstance();
// return new LdapShaPasswordEncoder();
// return new StandardPasswordEncoder();
return new BCryptPasswordEncoder();
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests(authorize -> {
// 允许所有访问 / 路径的请求
authorize.antMatchers("/", "/webjars/**").permitAll().antMatchers("/beers/find", "/beers*").permitAll().antMatchers(HttpMethod.GET, "/api/v1/beer/**").permitAll().mvcMatchers(HttpMethod.GET, "/api/v1/beerUpc/{upc}").permitAll();
}).authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("spring")
// .password("{noop}123")
.password("123").roles("ADMIN").and().withUser("user")
.password("$2a$08$TL5yHkRQAQZiIvnSO4x.q.ThhdJsgIL1ga9O5ZefSAozomLpa8xY.") // bcrypt
// .password("fbfa64628d6d1f9b071aeb184ca48edc46153500823a11e21c9dba29ec248bc24bfc2da3bb0525b5") // sha256
// .password("{SSHA}QKElFOKPdGDjeugAdOJrSFaXV8gkQxUJoUO9wQ==") // ldap
// .password("pass")
// .password("{noop}pass")
.roles("USER");
auth.inMemoryAuthentication().withUser("scott").password("tiger").roles("CUSTOMER");
}
}
package guru.sfg.brewery.web.controllers;
import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.LdapShaPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.StandardPasswordEncoder;
import org.springframework.util.DigestUtils;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class PasswordEncodingTests {
static final String PASSWORD = "pass";
@Test
public void testBcrypt() {
// 可以指定strength: 强度,默认是10
// PasswordEncoder bcrypt =new BCryptPasswordEncoder(12);
PasswordEncoder bcrypt = new BCryptPasswordEncoder();
// ldap每次都加随机的盐
System.out.println(bcrypt.encode(PASSWORD));
System.out.println(bcrypt.encode(PASSWORD));
String encodedPwd = bcrypt.encode(PASSWORD);
assertTrue(bcrypt.matches(PASSWORD, encodedPwd));
}
@Test
public void testSha256() {
// ...
}
@Test
public void testLdap() {
// ...
}
@Test
public void testNoOp() {
// ...
}
@Test
public void hashingExample() {
// ...
}
}
008 Delegating Password Encoder
package guru.sfg.brewery.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 指定使用什么密码加密编码器
@Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Override
public void configure(HttpSecurity http) throws Exception {
// ...
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("spring")
.password("{bcrypt}$2a$10$LJXzDtJ.kMsKNgZNwq74RuvvE3Srlc3dOZFWFJBIeFyoZMVhyxmfa")
.roles("ADMIN").and()
.withUser("user")
.password("{sha256}fbfa64628d6d1f9b071aeb184ca48edc46153500823a11e21c9dba29ec248bc24bfc2da3bb0525b5") // sha256
.roles("USER");
// 使用PasswordEncoderFactories,可以通过在密码前面加{加密方式}来指定如何编解码
auth.inMemoryAuthentication()
.withUser("scott")
.password("{ldap}{SSHA}pkI3NgDq814I4cRJNvoVBmJMdDW82sOiN6EDdA==")
// .password("tiger")
.roles("CUSTOMER");
}
}
package guru.sfg.brewery.web.controllers;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.security.test.context.support.WithMockUser;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest
public class BeerControllerT extends BaseIt {
@Test
public void initCreationFormWithSpring() throws Exception {
mockMvc.perform(get("/beers/new").with(httpBasic("spring", "123")))
.andExpect(status().isOk())
.andExpect(view().name("beers/createBeer"))
.andExpect(model().attributeExists("beer"));
}
@Test
public void initCreationFormWithScott() throws Exception {
mockMvc.perform(get("/beers/new").with(httpBasic("scott", "tiger")))
.andExpect(status().isOk())
.andExpect(view().name("beers/createBeer"))
.andExpect(model().attributeExists("beer"));
}
@Test
public void initCreationForm() throws Exception {
mockMvc.perform(get("/beers/new").with(httpBasic("user", "pass")))
.andExpect(status().isOk())
.andExpect(view().name("beers/createBeer"))
.andExpect(model().attributeExists("beer"));
}
@WithMockUser("spring") // 不加这个测试就会提示未认证
@Test
public void findBeers() throws Exception {
// ...
}
@Test
public void findBeersWithHttpBasic() throws Exception {
// ...
}
@Test
public void findBeersWithHttpBasicAuth() throws Exception {
// ...
}
}
009 Custom Delegating Password Encoder
package guru.sfg.brewery.security;
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
import java.util.HashMap;
import java.util.Map;
public class SfgPasswordEncoderFactories {
private SfgPasswordEncoderFactories() {
}
public static PasswordEncoder createDelegatingPasswordEncoder() {
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
return new DelegatingPasswordEncoder(encodingId, encoders);
}
}
package guru.sfg.brewery.config;
import guru.sfg.brewery.security.SfgPasswordEncoderFactories;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 指定使用什么密码加密编码器
@Bean
PasswordEncoder passwordEncoder() {
return SfgPasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Override
public void configure(HttpSecurity http) throws Exception {
// ...
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("spring")
.password("{bcrypt}$2a$10$LJXzDtJ.kMsKNgZNwq74RuvvE3Srlc3dOZFWFJBIeFyoZMVhyxmfa")
.roles("ADMIN").and()
.withUser("user")
.password("{sha256}fbfa64628d6d1f9b071aeb184ca48edc46153500823a11e21c9dba29ec248bc24bfc2da3bb0525b5") // sha256
.roles("USER");
// 使用PasswordEncoderFactories,可以通过在密码前面加{加密方式}来指定如何编解码
auth.inMemoryAuthentication()
.withUser("scott")
.password("{ldap}{SSHA}pkI3NgDq814I4cRJNvoVBmJMdDW82sOiN6EDdA==")
.roles("CUSTOMER");
}
}
07 - Custom Authentication Filter
002 Custom Authentication Filter Overview
接下来使用的认证方法是不推荐的(api+密钥?),但是遗留项目可能有
003 Delete Beer by ID MockMVC Test
package guru.sfg.brewery.web.controllers.api;
import guru.sfg.brewery.web.controllers.BaseIt;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@WebMvcTest
public class BeerRestControllerIT extends BaseIt {
@Test
public void detateBeer() throws Exception {
mockMvc.perform(delete("/api/v1/beer/97df8c39-98c4-4ae8-b663-453e8e19c311")
.header("Api-Key", "spring").header("Api-Secret", "123"))
.andExpect(status().isOk());
}
// ...
}
004 Custom Authentication Filter
package guru.sfg.brewery.security;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.RequestMatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
public class RestHeaderAuthFilter extends AbstractAuthenticationProcessingFilter {
public RestHeaderAuthFilter(RequestMatcher requiresAuthenticationRequestMatcher) {
super(requiresAuthenticationRequestMatcher);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
String username = getUsername(request);
String password = getPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
log.debug("Authenticating User: " + username);
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(username, password);
return this.getAuthenticationManager().authenticate(token);
}
private String getPassword(HttpServletRequest request) {
return request.getHeader("Api-Secret");
}
private String getUsername(HttpServletRequest request) {
return request.getHeader("Api-Key");
}
}
005 Spring Security Configuration
package guru.sfg.brewery.config;
import guru.sfg.brewery.security.RestHeaderAuthFilter;
import guru.sfg.brewery.security.SfgPasswordEncoderFactories;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AndRequestMatcher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
public RestHeaderAuthFilter restHeaderAuthFilter(AuthenticationManager authenticationManager) {
RestHeaderAuthFilter filter =
// 自己写的过滤器
new RestHeaderAuthFilter(
new AntPathRequestMatcher("/api/**")
);
filter.setAuthenticationManager(authenticationManager);
return filter;
}
// 指定使用什么密码加密编码器
@Bean
PasswordEncoder passwordEncoder() {
return SfgPasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Override
public void configure(HttpSecurity http) throws Exception {
// 添加过滤器
http.addFilterBefore(
restHeaderAuthFilter(
authenticationManager()
),
UsernamePasswordAuthenticationFilter.class
);
http.authorizeRequests(authorize -> {
// 允许所有访问 / 路径的请求
authorize.antMatchers("/", "/webjars/**").permitAll().antMatchers("/beers/find", "/beers*").permitAll().antMatchers(HttpMethod.GET, "/api/v1/beer/**").permitAll().mvcMatchers(HttpMethod.GET, "/api/v1/beerUpc/{upc}").permitAll();
}).authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// ...
}
}
006 Debugging Spring Security
再次运行上面的测试还是报错,调整 log 日志输出等级,然后查看日志
package guru.sfg.brewery.config;
import guru.sfg.brewery.security.RestHeaderAuthFilter;
import guru.sfg.brewery.security.SfgPasswordEncoderFactories;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AndRequestMatcher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
// 添加过滤器
http.addFilterBefore(
restHeaderAuthFilter(
authenticationManager()
),
UsernamePasswordAuthenticationFilter.class
)
// 关闭csrf
.csrf().disable();
// ...
}
// ...
}
007 Custom Do Filter Method
package guru.sfg.brewery.security;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.StringUtils;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
public class RestHeaderAuthFilter extends AbstractAuthenticationProcessingFilter {
public RestHeaderAuthFilter(RequestMatcher requiresAuthenticationRequestMatcher) {
super(requiresAuthenticationRequestMatcher);
}
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
+ authResult);
}
// 获取上下文,授权
SecurityContextHolder.getContext().setAuthentication(authResult);
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
Authentication authResult = attemptAuthentication(request, response);
if (authResult != null) {
successfulAuthentication(request, response, chain, authResult);
} else {
chain.doFilter(request, response);
}
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
String username = getUsername(request);
String password = getPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
log.debug("Authenticating User: " + username);
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
if (!StringUtils.isEmpty(username)) {
return this.getAuthenticationManager().authenticate(token);
} else {
return null;
}
}
private String getPassword(HttpServletRequest request) {
return request.getHeader("Api-Secret");
}
private String getUsername(HttpServletRequest request) {
return request.getHeader("Api-Key");
}
}
008 Custom Failure Handler
package guru.sfg.brewery.security;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.StringUtils;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
public class RestHeaderAuthFilter extends AbstractAuthenticationProcessingFilter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
try {
Authentication authResult = attemptAuthentication(request, response);
if (authResult != null) {
successfulAuthentication(request, response, chain, authResult);
} else {
chain.doFilter(request, response);
}
} catch (AuthenticationException e) {
unsuccessfulAuthentication(request, response, e);
}
}
// ...
}
package guru.sfg.brewery.security;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.StringUtils;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
public class RestHeaderAuthFilter extends AbstractAuthenticationProcessingFilter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
try {
Authentication authResult = attemptAuthentication(request, response);
if (authResult != null) {
successfulAuthentication(request, response, chain, authResult);
} else {
chain.doFilter(request, response);
}
} catch (AuthenticationException e) {
log.error("Authentication Failed: ", e);
unsuccessfulAuthentication(request, response, e);
}
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException failed)
throws IOException, ServletException {
SecurityContextHolder.clearContext();
if (log.isDebugEnabled()) {
log.debug("Authentication request failed: " + failed.toString(), failed);
log.debug("Updated SecurityContextHolder to contain null Authentication");
}
// 返回报错
response.sendError(HttpStatus.UNAUTHORIZED.value(),
HttpStatus.UNAUTHORIZED.getReasonPhrase());
}
// ...
}
08 - Database Authentication
002 Database Authentication Overview
003 JPA Entities
package guru.sfg.brewery.domain.security;
import javax.persistence.*;
import java.util.Set;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private String password;
private String username;
@ManyToMany(cascade = CascadeType.MERGE)
@JoinTable(name = "user_authority",
joinColumns = {
@JoinColumn(name = "USER_ID", referencedColumnName = "ID"),
},
inverseJoinColumns = {
@JoinColumn(name = "AUTHORITY_ID", referencedColumnName = "ID")
}
)
private Set<Authority> authorities;
private Boolean accountNonExpired = true;
private Boolean accountNonLocked = true;
private Boolean credentialsNonExpired = true;
private Boolean enabled = true;
}
package guru.sfg.brewery.domain.security;
import javax.persistence.*;
import java.util.Set;
@Entity
public class Authority {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private String role;
@ManyToMany(mappedBy = "authorities")
private Set<User> users;
}
004 Project Lombok Configuration
package guru.sfg.brewery.domain.security;
import lombok.*;
import javax.persistence.*;
import java.util.Set;
@Entity
// 用@Data可能导致循环然后崩溃
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private String password;
private String username;
/**
* 在使用@Singular注释注释一个集合字段(使用@Builder注释类),
* lombok会将该构建器节点视为一个集合,并生成两个adder方法而不是setter方法。
* <p>
* 一个向集合添加单个元素
* 一个将另一个集合的所有元素添加到集合中
*/
@Singular
@ManyToMany(cascade = CascadeType.MERGE)
@JoinTable(name = "user_authority",
joinColumns = {
@JoinColumn(name = "USER_ID", referencedColumnName = "ID"),
},
inverseJoinColumns = {
@JoinColumn(name = "AUTHORITY_ID", referencedColumnName = "ID")
}
)
private Set<Authority> authorities;
@Builder.Default
private Boolean accountNonExpired = true;
@Builder.Default
private Boolean accountNonLocked = true;
@Builder.Default
private Boolean credentialsNonExpired = true;
@Builder.Default
private Boolean enabled = true;
}
package guru.sfg.brewery.domain.security;
import lombok.*;
import javax.persistence.*;
import java.util.Set;
@Entity
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Authority {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private String role;
@ManyToMany(mappedBy = "authorities")
private Set<User> users;
}
005 Spring Data JPA Repositories
package guru.sfg.brewery.repositories.security;
import guru.sfg.brewery.domain.security.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Integer> {
Optional<User> findByUsername(String username);
}
package guru.sfg.brewery.repositories.security;
import guru.sfg.brewery.domain.security.Authority;
import org.springframework.data.jpa.repository.JpaRepository;
public interface AuthorityRepository extends JpaRepository<Authority,Integer> {
}
006 H2 Database Console Access
访问 h2 控制台需要验证(生产环境需要,但我们现在是测试),我们不希望这样
package guru.sfg.brewery.config;
import guru.sfg.brewery.security.RestHeaderAuthFilter;
import guru.sfg.brewery.security.SfgPasswordEncoderFactories;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AndRequestMatcher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// ...
@Override
public void configure(HttpSecurity http) throws Exception {
// 添加过滤器
http.addFilterBefore(
restHeaderAuthFilter(
authenticationManager()
),
UsernamePasswordAuthenticationFilter.class
)
.csrf().disable();
http.authorizeRequests(authorize -> {
// 允许所有访问 / 路径的请求
authorize
.antMatchers("/h2-console/**").permitAll()
.antMatchers("/", "/webjars/**").permitAll()
.antMatchers("/beers/find", "/beers*").permitAll()
.antMatchers(HttpMethod.GET, "/api/v1/beer/**").permitAll()
.mvcMatchers(HttpMethod.GET, "/api/v1/beerUpc/{upc}").permitAll();
})
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin()
.and()
.httpBasic();
}
}
从控制台复制最新的 url 然后连接
请求被拒绝
package guru.sfg.brewery.config;
import guru.sfg.brewery.security.RestHeaderAuthFilter;
import guru.sfg.brewery.security.SfgPasswordEncoderFactories;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AndRequestMatcher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
// 添加过滤器
http.addFilterBefore(
restHeaderAuthFilter(
authenticationManager()
),
UsernamePasswordAuthenticationFilter.class
)
.csrf().disable();
http.authorizeRequests(authorize -> {
// 允许所有访问 / 路径的请求
authorize
.antMatchers("/h2-console/**").permitAll()
.antMatchers("/", "/webjars/**").permitAll()
.antMatchers("/beers/find", "/beers*").permitAll()
.antMatchers(HttpMethod.GET, "/api/v1/beer/**").permitAll()
.mvcMatchers(HttpMethod.GET, "/api/v1/beerUpc/{upc}").permitAll();
})
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin()
.and()
.httpBasic();
// h2-console
http.headers().frameOptions().sameOrigin();
}
// ...
}
我们没数据(bug?),就每次创建时手动加,密码就把我们设置在 configure 里的放上去
007 User Details Service
package guru.sfg.brewery.security;
import guru.sfg.brewery.domain.security.Authority;
import guru.sfg.brewery.domain.security.User;
import guru.sfg.brewery.repositories.security.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class JpaUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username).orElseThrow(() -> {
return new UsernameNotFoundException("User name: " + username + " not fount");
});
return new org.springframework.security.core.userdetails.User(
user.getUsername(), user.getPassword(),
user.getEnabled(), user.getAccountNonExpired(), user.getCredentialsNonExpired(),
user.getAccountNonLocked(), covertToSpringAuthrorities(user.getAuthorities())
);
}
private Collection<? extends GrantedAuthority> covertToSpringAuthrorities(Set<Authority> authorities) {
if (authorities != null && authorities.size() > 0) {
return authorities.stream()
.map(Authority::getRole)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet());
} else {
return new HashSet<>();
}
}
}
008 Spring Security Configuration
package guru.sfg.brewery.security;
import guru.sfg.brewery.domain.security.Authority;
import guru.sfg.brewery.domain.security.User;
import guru.sfg.brewery.repositories.security.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class JpaUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
/**
* 注意@transaction的注解一定要加上。并且@Rollback(value = false) 也加上。
* springboot-test 默认在内存中save,不提交,所有以通过了但是数据库中无内容,官方说为了不影响上下文环境。
*/
@Override
@Transactional
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.debug("Getting User info via JAP");
User user = userRepository.findByUsername(username).orElseThrow(() -> {
return new UsernameNotFoundException("User name: " + username + " not fount");
});
return new org.springframework.security.core.userdetails.User(
user.getUsername(), user.getPassword(),
user.getEnabled(), user.getAccountNonExpired(), user.getCredentialsNonExpired(),
user.getAccountNonLocked(), covertToSpringAuthrorities(user.getAuthorities())
);
}
private Collection<? extends GrantedAuthority> covertToSpringAuthrorities(Set<Authority> authorities) {
if (authorities != null && authorities.size() > 0) {
return authorities.stream()
.map(Authority::getRole)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet());
} else {
return new HashSet<>();
}
}
}
009 Spring Boot Test Context
insert INTO user
VALUES (1,true,true,true,true,'{bcrypt}$2a$10$LJXzDtJ.kMsKNgZNwq74RuvvE3Srlc3dOZFWFJBIeFyoZMVhyxmfa','spring');
insert INTO user
VALUES (2,true,true,true,true,'{sha256}fbfa64628d6d1f9b071aeb184ca48edc46153500823a11e21c9dba29ec248bc24bfc2da3bb0525b5','user');
insert INTO user
VALUES (3,true,true,true,true,'{ldap}{SSHA}pkI3NgDq814I4cRJNvoVBmJMdDW82sOiN6EDdA==','scott');
insert into AUTHORITY
VALUES (1, 'ADMIN');
insert into AUTHORITY
VALUES (2, 'USER');
insert into AUTHORITY
VALUES (3, 'CUSTOMER');
insert into USER_AUTHORITY
values (1, 1);
insert into USER_AUTHORITY
values (2, 2);
insert into USER_AUTHORITY
values (3, 3);
这两可以删了