目录
介绍
Spring Security是基于Spring的安全框架,它提供了一种声明式的安全访问控制解决方案。它提供了包括身份验证、授权、运行时访问控制、单点登录、注销等功能。
Spring Security提供了许多集成选项,包括集成WebMVC、REST、SOAP等应用程序,也支持和Spring Boot等常见框架进行集成。
Spring Security的核心原则是基于过滤器链(FilterChain)来实施安全策略。Spring Security内部维护了一个过滤器链,每个过滤器都可以执行一些特定的安全任务。过滤器链按顺序执行,每当一个请求进入应用程序时,过滤器链就会进行筛选和处理。
在Spring Security中,安全是基于角色和权限的。角色是一组权限的集合,而权限是一种特定的访问控制。
最后,Spring Security具有高度的可配置性和可扩展性,可以定制各种安全策略,并支持开发自定义的安全插件。
常用的15个过滤器
-
UsernamePasswordAuthenticationFilter
:该过滤器处理基于用户名和密码的身份验证。 -
BasicAuthenticationFilter
:该过滤器处理基于HTTP基本身份验证的身份验证。 -
AnonymousAuthenticationFilter
:该过滤器允许未经身份验证的用户访问应用程序。 -
RememberMeAuthenticationFilter
:该过滤器为具有"记住我"功能的身份验证提供支持。 -
ExceptionTranslationFilter
:该过滤器负责处理Spring Security中发生的异常。 -
FilterSecurityInterceptor
:该过滤器基于数据库中的安全配置保护应用程序资源。 -
SessionManagementFilter
:该过滤器管理用户会话,并处理会话超时、并发登录等问题。 -
CsrfFilter
:该过滤器提供跨站点请求伪造保护。 -
LogoutFilter
:该过滤器处理用户注销并管理用户会话。 -
SwitchUserFilter
:该过滤器允许管理员模拟其他用户进行操作。 -
RequestCacheAwareFilter
:该过滤器允许将未经身份验证的请求缓存,以便在身份验证后重新处理这些请求。 -
ConcurrentSessionFilter
:该过滤器处理并发登录的问题。 -
SessionFixationProtectionFilter
:该过滤器提供会话固定保护。 -
SecurityContextHolderAwareRequestFilter
:该过滤器将SecurityContext添加到HttpServletRequest的常规属性中。 -
OAuth2ClientAuthenticationProcessingFilter
:该过滤器提供OAuth2客户端身份验证的支持。
快速开始
1.创建一个springboot程序,导入相关的坐标
<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>
2.启动程序
这个时候访问该应用的资源就会自动跳转到认证页面,用户名是user,密码是控制台上打印的一串字符串。
其中177c91ab-eac6-43a4-a4d4-aef782114ce6就是密码。
这个用户是在SpringSecurity中没有配置该用户的时候自动创建的,具体源码如下:
public static class User {
/**
* Default user name.
*/
private String name = "user";
/**
* Password for the default user name.
*/
private String password = UUID.randomUUID().toString();
/**
* Granted roles for the default user name.
*/
private List<String> roles = new ArrayList<>();
private boolean passwordGenerated = true;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return this.password;
}
public void setPassword(String password) {
if (!StringUtils.hasLength(password)) {
return;
}
this.passwordGenerated = false;
this.password = password;
}
public List<String> getRoles() {
return this.roles;
}
public void setRoles(List<String> roles) {
this.roles = new ArrayList<>(roles);
}
public boolean isPasswordGenerated() {
return this.passwordGenerated;
}
}
由上面的代码可以看出,默认的用户名是user,默认的密码是随机的UUID。
3.修改默认的用户名和密码
可以通过修改配置文件来修改默认的用户名和密码。
spring:
security:
user:
name: xinghe
password: 123456
这样也会存在一个问题,就是一个系统之中只能有一个用户,这个问题在后面的内容中会有解决。
配置自定义认证页面
在一些系统中,往往不会使用SpringSecurity默认的认证页面,而是使用用户自定义的认证页面,可以通过配置SpringSecurity来设置默认的认证页面。
package com.xinghe.config;
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;
/**
* @author heimi
* @version 1.0
* @description springSecurity配置类
* @date 2023/5/23 上午9:45
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable(); // 关闭csrf
http.authorizeRequests()
.mvcMatchers("/login.html").permitAll() // 释放认证页面
.anyRequest().authenticated(); // 其它请求需要经过认证才能访问
http.formLogin() // 配置认证相关
.loginPage("/login.html") // 认证页面
.loginProcessingUrl("/login") // 认证处理的地址
.failureForwardUrl("/login.html") // 认证失败重定向地址
.defaultSuccessUrl("/index.html"); // 认证成功默认跳转的页面
}
}
第一条配置是关闭csrf防护,为了测试方便,最好关闭csrf防护,但是真实使用中需要开启csrf防护。
第二条配置是配置url拦截,首先匹配到/login.html,因为认证的时候需要用到login.html,所以使用permitAll来释放该页面,anyRequest是匹配除了上面配置的url之外的其它url,authenticated是需要认证才能访问。
第三条配置是配置和认证相关的,loginPage是配置认证页面,loginProcessinUUrl是处理认证请求的后端接口,failureForwardUrl是配置认证失败的重定向地址,defaultSuccessUrl是配置认证成功之后默认的跳转页面。
基于内存的多用户认证
在上面通过配置文件配置用户名和密码的时候存在一个明显的问题,就是只能配置一个用户。当然SpringSecurity可以配置多个用户,可以基于内存配置多用户,也可以基于数据库配置多用户。在SpringSecurity中的用户对象是由UserDetails的接口的实现类来封装,而创建用户详情的是UserDetailsService的实现类,如果自定义用户,那么SpringSecurity就不会自动创建用户了,因此需要自定义一个SpringSecurity相关的Bean。
package com.xinghe.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;
/**
* @author heimi
* @version 1.0
* @description 配置用户
* @date 2023/5/23 上午10:14
*/
@Configuration
public class SecurityUserConfig {
@Bean
public UserDetailsService userDetailsService() {
UserDetails root = User.builder()
.username("root")
.password("123456")
.authorities("root")
.build();
UserDetails admin = User.builder()
.username("admin")
.password("1223456")
.authorities("admin")
.build();
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(root);
manager.createUser(admin);
return manager;
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
在自定义多用户的时候需要配置密码编码器,所有的密码编码器都实现了PasswordEncoder接口,第二个bean就是配置密码编码器。
InMemoryUserDetailsManager是SpringSecurity对UserDetailsService默认的实现类,用户将用户创建并且存储在内存中。
第一个Bean是定义了两个User对象,然后使用InMemoryUserDetailsManager的createUser方法来创建用户,下面来看一下源码:
public class InMemoryUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {
private final Map<String, MutableUserDetails> users = new HashMap<>();
public InMemoryUserDetailsManager() {
}
@Override
public void createUser(UserDetails user) {
Assert.isTrue(!userExists(user.getUsername()), "user should not exist");
this.users.put(user.getUsername().toLowerCase(), new MutableUser(user));
}
}
这段源码中删减掉了一些代码。可以看出在该实现类中,有一个users的map集合,通过createUser方法来创建用户详情对象,然后放进这个map集合中,这样在内存中就可以实现多用户访问了。
当然,在内存中配置多用户的时候除了使用自定义的bean配置之外还有另外一种配置方式,就是通过AuthenticationManagerBuilder来配置多用户的访问。
package com.xinghe.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* @author heimi
* @version 1.0
* @description springSecurity配置类
* @date 2023/5/23 上午9:45
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("root").password("123456").authorities("root");
auth.inMemoryAuthentication()
.withUser("admin").password("123456").authorities("admin");
}
}
基于内存的用户授权
在上面的配置多用户的时候配置了一个authorities,就是配置用户权限的,也可以使用roles来配置角色,这两个同时配置的话只能生效一个,哪个配置比较靠后哪个生效,这两个的区别就是roles在配置的时候会在前面加上ROLE_,而authorities配置的什么就是什么,就比如说roles("user")的话,用authorities配置的话就是ROLE_user。
其中授权可以针对于url授权,也可以针对与方法授权。
针对url授权的话可以这样配置:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.mvcMatchers("/root").hasAnyAuthority("root")
.mvcMatchers("/admin").hasAnyAuthority("admin")
.anyRequest().authenticated();
}
上面的配置就是说匹配到/root需要有root权限的才能访问,匹配到/admin需要有admin权限的用户才能访问,如果权限不足,就出出现403。
针对url授权的话太广泛了,可以进行更小粒度的授权,针对方法。
要向对方法进行授权,需要在SpringSecurity配置类中添加全局方法安全注解@EnableGlobalMethodSecurity
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {)
开启使用EL表达式的方法授权注解。
创建业务逻辑层并使用@PreAuthorize来控制权限:
package com.xinghe.service;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
/**
* @author heimi
* @version 1.0
* @description 业务逻辑
* @date 2023/5/23 上午11:03
*/
@Service
public class UserService {
@PreAuthorize("hasAnyAuthority('root')")
public String root() {
return "root root root";
}
@PreAuthorize("hasAnyAuthority('admin')")
public String admin() {
return "admin admin admin";
}
}
这样当访问到该方法的时候就会进行权限验证了。
加密方式
在进行密码存储的过程中不能将明文存储进去,这个时候需要用到数据加密了,SpringSecurity推荐使用BCryptPasswordEncoder加密器进行加密,如果向使用该加密器,那么直接放入IOC容器就可以了,SpringSecurity会自动找到并且使用。
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
所有的密码加密器都实现了PasswordEncoder接口。
基于数据库的多用户认证与授权
认证
1.创建数据库模型
create table user (
id bigint primary key auto_increment,
username varchar(50) not null ,
password varchar(60) not null
);
create table authentication (
id bigint primary key auto_increment,
code varchar(50) not null
);
create table user_and_authentication (
uid bigint not null ,
aid bigint not null
);
user表是存储用户的数据
authentication表是存储权限信息
user_and_authentication是存储用户和权限的关系信息
2.导入需要的依赖坐标
<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>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
</dependencies>
3.创建对应的实体类
package com.xinghe.domain.po;
import lombok.Data;
/**
* @author heimi
* @version 1.0
* @description 用户
* @date 2023/5/23 上午11:21
*/
@Data
public class User {
private Long id;
private String username;
private String password;
}
package com.xinghe.domain.po;
import lombok.Data;
/**
* @author heimi
* @version 1.0
* @description 权限
* @date 2023/5/23 上午11:24
*/
@Data
public class Authentication {
private Long id;
private String code;
}
4.创建自定义的用户详情对象,实现UserDetails
package com.xinghe.config;
import com.xinghe.domain.po.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
/**
* @author heimi
* @version 1.0
* @description 自定义用户详情
* @date 2023/5/23 上午11:27
*/
public class MyUserDetails implements UserDetails {
private User user;
public MyUserDetails(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
String password = user.getPassword();
user.setPassword(null);
return password;
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
getAuthorities是和配置权限相关的,这里就先不配置。
isAccountNonExpired账户是否未过期
isAccountNonLocked账户是否未锁定
isCredentialsNonExpired凭据是否未过期
isEnabled账户是否可用
这四个布尔值就先不配置,全设置成true即可,如果有需要可以在数据库添加相应的字段动态的判断这四个值。
需要注意的是这里容器中的用户详情对象需要将密码擦除,防止暴露到端中。
5.编写mapper接口
package com.xinghe.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xinghe.domain.po.User;
import org.apache.ibatis.annotations.Mapper;
/**
* @author heimi
* @version 1.0
* @description userMapper
* @date 2023/5/23 上午11:36
*/
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
6.编写认证相关的业务逻辑类
package com.xinghe.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xinghe.config.MyUserDetails;
import com.xinghe.domain.po.User;
import com.xinghe.mapper.UserMapper;
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 javax.annotation.Resource;
/**
* @author heimi
* @version 1.0
* @description 业务逻辑
* @date 2023/5/23 上午11:03
*/
@Service
public class UserService implements UserDetailsService {
@Resource
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername, username);
User user = userMapper.selectOne(wrapper); // 查询用户对象
MyUserDetails myUserDetails = new MyUserDetails(user); // 封装用户
return myUserDetails;
}
}
这样在用户输入用户名和密码的时候,用户名传到该方法上,然后查询出用户信息,之后根据用户名查询用户数据,然后封装成自定义的UserDetails对象,之后交给SpringSecurity框架,由SpringSecrity进行判断账号是否可用。
授权
在上面的认证操作中,自定义的UserDetails对象中getAuthorities没有配置,接下来进行权限的配置。
1.新建认证相关的实体类
package com.xinghe.config;
import com.xinghe.domain.po.Authentication;
import org.springframework.security.core.GrantedAuthority;
/**
* @author heimi
* @version 1.0
* @description 权限相关
* @date 2023/5/23 上午11:53
*/
public class MyGrantedAuthority implements GrantedAuthority {
private Authentication authentication;
public MyGrantedAuthority(Authentication authentication) {
this.authentication = authentication;
}
@Override
public String getAuthority() {
return authentication.getCode();
}
}
2.修改自定义的UserDetails添加上权限功能
package com.xinghe.config;
import com.xinghe.domain.po.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
/**
* @author heimi
* @version 1.0
* @description 自定义用户详情
* @date 2023/5/23 上午11:27
*/
public class MyUserDetails implements UserDetails {
private User user;
private List<MyGrantedAuthority> grantedAuthorities;
public MyUserDetails(User user, List<MyGrantedAuthority> grantedAuthorities) {
this.user = user;
this.grantedAuthorities = grantedAuthorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return grantedAuthorities;
}
@Override
public String getPassword() {
String password = user.getPassword();
user.setPassword(null);
return password;
}
@Override
public String getUsername() {
return user.getUsername();
}
}
3.编写查询权限的mapper接口
package com.xinghe.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xinghe.domain.po.Authentication;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* @author heimi
* @version 1.0
* @description authenticationMapper
* @date 2023/5/23 上午11:37
*/
@Mapper
public interface AuthenticationMapper extends BaseMapper<Authentication> {
@Select("select * from user_and_authentication uaa inner join authentication a on uaa.aid=a.id where uaa.uid=#{id}")
List<Authentication> getAuthenticationByUserId(Long id);
}
4.修改认证相关的业务逻辑层
package com.xinghe.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xinghe.config.MyGrantedAuthority;
import com.xinghe.config.MyUserDetails;
import com.xinghe.domain.po.Authentication;
import com.xinghe.domain.po.User;
import com.xinghe.mapper.AuthenticationMapper;
import com.xinghe.mapper.UserMapper;
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 javax.annotation.Resource;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author heimi
* @version 1.0
* @description 业务逻辑
* @date 2023/5/23 上午11:03
*/
@Service
public class UserService implements UserDetailsService {
@Resource
private UserMapper userMapper;
@Resource
private AuthenticationMapper authenticationMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername, username);
User user = userMapper.selectOne(wrapper); // 查询用户对象
// 查询权限
List<Authentication> authentications = authenticationMapper.getAuthenticationByUserId(user.getId());
// 封装权限
List<MyGrantedAuthority> authorityList = authentications.stream().map(MyGrantedAuthority::new).collect(Collectors.toList());
return new MyUserDetails(user, authorityList);
}
}
5.针对方法或url授权
针对方法和url授权的话就和前面的基于内存中的对象进行url或者方法授权的步骤是一样的了。