准备
maven 依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
IndexController.java
@RestController
public class IndexController {
@GetMapping("/index")
public String index() {
return "index";
}
@GetMapping("/index/admin001")
public String index1() {
return "admin001";
}
@GetMapping("/index/user001")
public String index2() {
return "user001";
}
}
启动类:
@SpringBootApplication
public class SecurityBoot {
public static void main(String[] args) {
SpringApplication.run(SecurityBoot.class, args);
}
}
一、认证
1.默认认证
默认表单模式,用户为 user,密码打印在控制台
例:访问 http://localhost:8080/index
会 302 重定向到 GET 请求 http://localhost:8080/login
,输入用户名和密码,会 POST 请求到 http://localhost:8080/login
, 验证成功后,会 302 重定向到 http://localhost:8080/index
2.指定用户名密码
默认的用户名密码在 org.springframework.boot.autoconfigure.security.SecurityProperties.User
指定
在 org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration#inMemoryUserDetailsManager
定义了 InMemoryUserDetailsManager 的 bean,用户名密码
方式一
使用 application.properties 指定用户名密码
spring.security.user.name=user001
spring.security.user.password=123
方式二
定义 UserDetailsManager 的 bean
@Configuration(proxyBeanMethods = false)
public class UserDetailsConfig {
@Bean
public UserDetailsManager users() {
UserDetails user = User.withUsername("user002")
.password("{noop}123")
.roles("USER")
.build();
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(user);
return manager;
}
}
方式三
继承 WebSecurityConfigurerAdapter
@Configuration(proxyBeanMethods = false)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user003")
.password("{noop}123")
.roles("admin");
}
}
方式四
实现 UserDetailsService 接口,InMemoryUserDetailsManager 和 JdbcUserDetailsManager 就是实现了此接口
@Service
public class UserDetailServiceImpl implements UserDetailsService {
private static Map<String, User> userMap = new HashMap<>();
static {
userMap.put("user004", new User("user004", "{noop}123",
Arrays.asList(new SimpleGrantedAuthority("USER"))));
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMap.get(username);
if (user == null) {
throw new UsernameNotFoundException(username);
}
return new User(user.getUsername(), user.getPassword(), user.getAuthorities());
}
}
3.登陆认证方式
在使用的 Spring Boot 1.X 版本,依赖的 Security 4.X 版本,默认的验证模式是 httpbasic 认证。现在使用的是 Spring Boot 2.0 以上版本,依赖的 Security 5.X 版本,默认的验证模式是表单模式。
认证方式 | 有无状态 | 简介 | 应用场景 |
---|---|---|---|
httpBasic (基础认证) | 无状态 | 不使用cookie | 对外API |
formLogin (表单认证) | 有状态 | 使用session会话 | 网站应用 |
方式一
httpBasic 登陆,请求后端页面时,弹出登陆框进行登陆,前端会增加请求头,如 Authorization: Basic dXNlcjAwMToxMjM=
httpBasic 模式要求传输的用户名密码使用 Base64 模式进行加密。如果用户名是 “user001”,密码是 “123”,则将字符串 "user001:123
" 使用 Base64 编码算法加密成 “dXNlcjAwMToxMjM=”
请求头携带 Authorization,即可访问,curl -H "Authorization: Basic dXNlcjAwMToxMjM=" http://localhost:8080/index
@Configuration(proxyBeanMethods = false)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
// httpBasic 登陆
@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic() // 开启 httpBasic 认证
.and()
.authorizeRequests()
.anyRequest().authenticated(); // 所有请求都需要登录认证才能访问
}
}
方式二
formLogin 登陆,请求后端资源时,会定向到登陆页面,使用表单登陆
@Configuration(proxyBeanMethods = false)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
// formLogin 登陆
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 开启 formLogin 认证
.and()
.authorizeRequests()
.anyRequest().authenticated(); // 所有请求都需要登录认证才能访问
}
}
4.用户信息存储
方式一
基于内存
@Configuration(proxyBeanMethods = false)
public class UserDetailsConfig {
@Bean
public UserDetailsManager users() {
UserDetails user = User.withUsername("user002")
.password("{noop}123")
.roles("USER")
.build();
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(user);
return manager;
}
}
方式二
基于数据库
添加 jdbc 驱动
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.32</version>
<scope>runtime</scope>
</dependency>
application.properties 配置文件
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/security_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456
spring.security.user.name=user001
spring.security.user.password=123
执行 sql 语句
sql 语句在 org.springframework.security.core.userdetails.jdbc.users.ddl
,其实是针对 HSQLDB 数据库的,mysql 的 sql 语句如下:
create table users(
username varchar(50) not null primary key,
password varchar(500) not null,
enabled boolean not null
);
create table authorities (
username varchar(50) not null,
authority varchar(50) not null,
constraint fk_authorities_users foreign key(username) references users(username)
);
create unique index ix_auth_username on authorities (username, authority);
增加 JdbcUserDetailsManager 的 bean
@Configuration(proxyBeanMethods = false)
public class UserDetailsConfig {
@Autowired
private DataSource dataSource;
@Bean
public UserDetailsManager users() {
JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
if (!manager.userExists("user005")) {
UserDetails user = User.builder()
.username("user005")
.password("{noop}123")
.roles("USER")
.build();
manager.createUser(user);
}
return manager;
}
}
JdbcUserDetailsManager 实现了 UserDetailsService 的 loadUserByUsername,从数据库中查询数据
二、授权
鉴权的前提需要认证通过;认证不通过的状态码为401,鉴权不通过的状态码为403,两者是不同的。
Spring Security 可以基于角色的权限控制,也可以基于权限的权限控制。
以下基于角色的权限控制:
- 不同的用户可以属于不同的角色
- 不同的角色可以访问不同的接口
假设,存在两个角色 USER(普通用户) 和 ADMIN(管理员),角色 USER 可以访问接口 /index/user001,角色 ADMIN 可以访问接口 /index/admin001,所有用户认证后可以访问接口 /。
重新设置 HttpSecurity:
@Configuration(proxyBeanMethods = false)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.
// 开启httpbasic认证
httpBasic()
.and()
// 针对 HttpServletRequest 进行安全配置
.authorizeRequests()
// 设置角色 USER 可以访问接口 /index/user001
.mvcMatchers("/index/user001").hasRole("USER")
// 设置角色 ADMIN 可以访问接口 /index/admin001
.mvcMatchers("/index/admin001").hasRole("ADMIN")
// 设置其他接口认证后即可访问
.anyRequest().authenticated();
}
}
方式一
设置用户 admin001 同时拥有角色 USER 和 ADMIN
@Configuration(proxyBeanMethods = false)
public class UserDetailsConfig {
@Bean
public UserDetailsManager users() {
UserDetails admin001 = User.withUsername("admin001")
.password("{noop}123")
// .roles("ADMIN")
.roles("USER", "ADMIN")
.build();
UserDetails user001 = User.withUsername("user001")
.password("{noop}123")
.roles("USER")
.build();
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(admin001);
manager.createUser(user001);
return manager;
}
}
方式二
设置角色 ADMIN 包含 USER
@Configuration(proxyBeanMethods = false)
public class UserDetailsConfig {
@Bean
public UserDetailsManager users() {
UserDetails admin001 = User.withUsername("admin001")
.password("{noop}123")
.roles("ADMIN")
// .roles("USER", "ADMIN")
.build();
UserDetails user001 = User.withUsername("user001")
.password("{noop}123")
.roles("USER")
.build();
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(admin001);
manager.createUser(user001);
return manager;
}
/**
* 只需要往 Spring IOC 容器中注入一个 RoleHierarchy 类型的 Bean 就可以了
* Spring Security 会处理这个Bean,生成一个 RoleHierarchyVoter,注入到 AccessDecisionManager 中,默认就是 AffirmativeBased
*/
@Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_ADMIN > ROLE_STAFFn ROLE_STAFF > ROLE_USERn ROLE_USER > ROLE_GUEST");
return hierarchy;
}
}
三、自定义的登陆页
1.login.html
增加 thymeleaf 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
在 /resources/static/
下创建 login.html
,static 目录的静态文件不用走 controller
<!DOCTYPE html>
<html lang="en" >
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/form/login" method="post">
用户名:<input type="text" name="username"/> <br/>
密码:<input type="text" name="password"/> <br/>
<input type="submit" value="login"/>
</form>
</body>
</html>
2.指定用户名密码
在 application.properties 指定用户名密码
spring.security.user.name=user001
spring.security.user.password=123
3.指定登陆页
继承 WebSecurityConfigurerAdapter 类,指定登陆页
@Configuration(proxyBeanMethods = false)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 开启 formLogin 认证
.formLogin()
// 自定义登录页,其为 GET 请求,文件位于 /resources/static/login.html,不需要 Controller 跳转
.loginPage("/login.html").permitAll()
// 配置 form 表单 用户名的 name 属性值
.usernameParameter("username")
// 配置 form 表单 密码的 name 属性值
.passwordParameter("password")
// 自定义登录 action, 这个接口不需要自己编写,其为 POST 请求,如果不写 Spring Security 配置值同 loginPage,
.loginProcessingUrl("/form/login")
.and()
// 针对 HttpServletRequest 进行安全配置
.authorizeRequests()
// 所有请求都需要登录认证才能访问
.anyRequest().authenticated()
.and()
// 关闭 CSRF 保护功能,否则不支持 Post 请求
.csrf().disable();
}
}
四、原理
在 spring-boot-autoconfigure 模块,spring.factories 文件中,定义了与 Spring Security 相关的自动配置类有:
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration,\
org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration,\
org.springframework.boot.autoconfigure.security.rsocket.RSocketSecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.client.reactive.ReactiveOAuth2ClientAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration,\
SecurityAutoConfiguration 和 SecurityFilterAutoConfiguration 配置类,这两个配置类会自动装配 FilterChainProxy 和 DelegatingFilterProxy 到 Spring 容器中
1.自动装配 FilterChainProxy
SecurityAutoConfiguration 导入了3个配置类:SpringBootWebSecurityConfiguration、WebSecurityEnablerConfiguration、SecurityDataConfiguration
// SecurityAutoConfiguration.java
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(DefaultAuthenticationEventPublisher.class)
@EnableConfigurationProperties(SecurityProperties.class)
@Import({ SpringBootWebSecurityConfiguration.class, WebSecurityEnablerConfiguration.class,
SecurityDataConfiguration.class })
public class SecurityAutoConfiguration {
@Bean
@ConditionalOnMissingBean(AuthenticationEventPublisher.class)
public DefaultAuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher publisher) {
return new DefaultAuthenticationEventPublisher(publisher);
}
}
1.WebSecurityEnablerConfiguration
对于 WebSecurityEnablerConfiguration,当 Spring 容器中没有名称为 springSecurityFilterChain 的 Bean 等条件时,就会加载该配置类,此时 @EnableWebSecurity 注解生效
// WebSecurityEnablerConfiguration.java
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(WebSecurityConfigurerAdapter.class)
@ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@EnableWebSecurity
public class WebSecurityEnablerConfiguration {
}
// EnableWebSecurity.java
@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
@Import({ WebSecurityConfiguration.class,
SpringWebMvcImportSelector.class,
OAuth2ImportSelector.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity {
/**
* Controls debugging support for Spring Security. Default is false.
* @return if true, enables debug support with Spring Security
*/
boolean debug() default false;
}
- SpringWebMvcImportSelector 的作用:判断当前的环境是否包含 springmvc,因为 springsecurity 可以在非 spring 环境下使用,为了避免 DispatcherServlet 的重复配置,所以使用了这个注解来区分
- WebSecurityConfiguration:用来配置web安全的
- OAuth2ImportSelector:该类是为了对 OAuth2.0 开放授权协议进行支持。ClientRegistration 如果被引用,具体点也就是 spring-security-oauth2 模块被启用(引入依赖jar)时。会启用 OAuth2 客户端配置 OAuth2ClientConfiguration
- HttpSecurityConfiguration 会通过 @Autowired 去获取容器中的一个 AuthenticationManager 实例,如果没能获取到则使用依赖注入的 AuthenticationConfiguration 实例创建一个 AuthenticationManager 实例,这个实例其实就是 ProviderManager
WebSecurityConfiguration#springSecurityFilterChain() 最终生成了一个名称为 springSecurityFilterChain 的 Bean 实体,该 Bean 的实际类型其实为 FilterChainProxy,是由 WebSecurity#build() 方法创建的
// WebSecurityConfiguration.java
@Configuration(proxyBeanMethods = false)
public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware {
...
@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain() throws Exception {
boolean hasConfigurers = webSecurityConfigurers != null
&& !webSecurityConfigurers.isEmpty();
if (!hasConfigurers) {
WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor
.postProcess(new WebSecurityConfigurerAdapter() {
});
webSecurity.apply(adapter);
}
return webSecurity.build();
}
...
}
2.自动装配 DelegatingFilterProxy
要加载 SecurityFilterAutoConfiguration 前,必须先加载配置类 SecurityAutoConfiguration
SecurityFilterAutoConfiguration,注入了一个名称为 springSecurityFilterChain 的 Bean,SecurityFilterAutoConfiguration#securityFilterChainRegistration 最终生成一个 DelegatingFilterProxyRegistrationBean 实体
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(SecurityProperties.class)
@ConditionalOnClass({ AbstractSecurityWebApplicationInitializer.class, SessionCreationPolicy.class })
@AutoConfigureAfter(SecurityAutoConfiguration.class) // 须先加载 SecurityAutoConfiguration
public class SecurityFilterAutoConfiguration {
...
@Bean
@ConditionalOnBean(name = DEFAULT_FILTER_NAME)
public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(
SecurityProperties securityProperties) {
DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean(
DEFAULT_FILTER_NAME);
registration.setOrder(securityProperties.getFilter().getOrder());
registration.setDispatcherTypes(getDispatcherTypes(securityProperties));
return registration;
}
...
}
DelegatingFilterProxyRegistrationBean 实现了 ServletContextInitializer 接口,当系统执行 ServletWebServerApplicationContext.selfInitialize() 进行初始化时,会依次调用到: RegistrationBean.onStartup() --> DynamicRegistrationBean.register() --> AbstractFilterRegistrationBean.addRegistration()
// AbstractFilterRegistrationBean.java
@Override
protected Dynamic addRegistration(String description, ServletContext servletContext) {
Filter filter = getFilter();
return servletContext.addFilter(getOrDeduceName(filter), filter);
}
// DelegatingFilterProxyRegistrationBean.java
@Override
public DelegatingFilterProxy getFilter() {
return new DelegatingFilterProxy(this.targetBeanName, getWebApplicationContext()) {
@Override
protected void initFilterBean() throws ServletException {
// Don't initialize filter bean on init()
}
};
}