Spring Security简介
- 是一个功能强大且高度可定制的安全框架,它主要为Java程序提供声明式的身份验证(认证)和访问控制(授权)功能
- 为基于Spring的企业应用系统提供了全面的安全解决方案,通过声明式的方式管理安全访问控制,减少了企业系统安全控制代码的重复编写,并提供了多种安全功能和机制。
Spring Security的核心作用包括:
- 认证:验证系统中是否存在该用户,并确认其身份。Spring Security支持多种身份验证机制,如基于表单、HTTP基本认证、OAuth、LDAP等,并允许自定义身份验证机制。
- 授权:控制用户能访问哪些资源。Spring Security允许定义细粒度的访问控制规则,可以通过注解、XML配置或编程方式来定义角色和权限。
- 防护攻击:提供防护措施,如防止身份伪造等攻击手段。
- 加密功能:对密码进行加密和匹配。
- 会话管理:包括并发控制、超时处理和无效会话处理,并支持使用基于令牌的会话。
- Remember Me功能:实现“记住我”功能,允许用户在登录后保持身份验证状态,并在下次访问应用程序时自动登录。
- 集成Spring MVC:与Spring MVC有良好的集成,提供了一组可以在Spring应用上下文中配置的Bean,利用Spring IoC、DI(控制反转和依赖注入)和AOP(面向切面编程)功能,减少编写重复安全代码的工作。34
- 细粒度授权:支持基于URL的请求授权、方法访问授权、对象访问授权,提供灵活的授权粒度。
- CSRF防护:提供防护措施,防止跨站请求伪造攻击。
Spring Security 核心功能包括:
- 认证:提供了多种认证方式,如表单认证、HTTP Basic认证、OAuth2认证等,可以与多种身份验证机制集成。
- 授权:提供了多种授权方式,如角色授权、基于表达式的授权等,可以对应用程序中的不同资源进行授权。
- 攻击防护:提供了多种防护机制,如跨站点请求伪造(CSRF)防护、注入攻击防护等。
- 会话管理:提供了会话管理机制,如令牌管理、并发控制等。
- 监视与管理:提供了监视与管理机制,如访问日志记录、审计等。
最核心的功能为:身份认证、授权、防护攻击
Spring Security的核心原理是拦截器(Filter)
- Spring Security会在Web应用程序的过滤器链中添加一组自定义的过滤器,这些过滤器可以实现身份验证和授权功能。
- 当用户请求资源时,Spring Security会拦截请求,并使用配置的身份验证机制来验证用户身份。如果身份验证成功,Spring Security会授权用户访问所请求的资源。
Spring Security的具体工作原理如下:
- 用户请求Web应用程序的受保护资源。
- Spring Security拦截请求,并尝试获取用户的身份验证信息。
- 如果用户没有经过身份验证,Spring Security将向用户显示一个登录页面,并要求用户提供有效的凭据(用户名和密码)。
- 一旦用户提供了有效的凭据,Spring Security将验证这些凭据,并创建一个已认证的安全上下文(SecurityContext)对象。
- 安全上下文对象包含已认证的用户信息,包括用户名、角色和授权信息。
- 在接下来的请求中,Spring Security将使用已经认证的安全上下文对象来判断用户是否有权访问受保护的资源。
- 如果用户有权访问资源,Spring Security将允许用户访问资源,否则将返回一个错误信息。
身份认证
Spring Security的核心原理是拦截器(Filter)
- Spring Security会在Web应用程序的过滤器链中添加一组自定义的过滤器,这些过滤器可以实现身份验证和授权功能。
- 当用户请求资源时,Spring Security会拦截请求,并使用配置的身份验证机制来验证用户身份。如果身份验证成功,Spring Security会授权用户访问所请求的资源。
DelegatingFilterProxy
- Spring提供了一个名为DelegatingFilterProxy的过滤器实现,它本身不做任何事情。
- 它的内部有一个属性targetBeanName,真正实现过滤请求的Filter是在Application Context中找到名称为targetBeanName并且实现Filter接口的bean,在Spring Security中这个bean的默认实现是FilterChainProxy
- 所以DelegatingFilterProxy 会将Spring中的 filter 注册到servlet中(并非一个过滤器,而是过滤器链,这些过滤器链由SecurityFilterProxy管理)
DefaultSecurityFilterChain
- 是Spring Security中的一个重要组件,它负责Spring Security的过滤器链初始化。
- 在Spring Security中,
DefaultSecurityFilterChain
是一个Bean,它是通过@EnableWebSecurity
注解自动配置的。这个Bean负责创建一组过滤器,这些过滤器提供了身份验证、授权和其他安全功能。
Spring Security启动的时候,会默认加载16个默认的滤器
SecurityProperties
以下为源代码:
-
所有的配置以:
spring.security
开头 -
有一个内部类user,默认的用户名是user,默认的密码是使用UUID生成的,都可以在配置文件中修改
UsernamePasswordAuthenticationFilter: 过滤器
- 是Spring Security提供的默认身份验证过滤器,用于处理基于表单的用户名密码身份验证请求。
- 监听POST请求的"/login"路径,接收用户名和密码等凭证,并将其封装为一个 UsernamePasswordAuthenticationToken 对象,然后通过 AuthenticationManager 进行身份验证。
- 登录请求中携带的用户信息,一定要是 username 和 password ,如果想要变更参数名称,需要设置
usernameParameter
和passwordParameter
两个参数的名称
Spring Security的认证流程大致可以分为两个过程,
- 首先是用户登录认证的过程,然后是用户访问受保护资源时的授权过程。
- 在认证过程中,用户需要提供用户名和密码,Spring Security通过UsernamePasswordAuthenticationFilter将用户名和密码封装成Authentication对象,并交由AuthenticationManager进行认证。
- 如果认证成功,则认证结果会存储在SecurityContextHolder中。在授权过程中,Spring Security会检查用户是否有访问受保护资源的权限,如果没有则会重定向到登录页面进行认证。
Spring Security基于用户名和密码的认证模式流程
- 拦截未授权的请求,重定向到登录页面的过程:
- 当用户访问需要授权的资源时,Spring Security会检查用户是否已经认证(即是否已登录),如果没有登录则会重定向到登录页面。
- 重定向到登录页面时,用户需要输入用户名和密码进行认证。
- 表单登录的过程:
- 用户在登录页面输入用户名和密码,提交表单。
- Spring Security的UsernamePasswordAuthenticationFilter拦截表单提交的请求,并将用户名和密码封装成一个Authentication对象。
- AuthenticationManager接收到Authentication对象后,会根据用户名和密码查询用户信息,并将用户信息封装成一个UserDetails对象。
- 如果查询到用户信息,则将UserDetails对象封装成一个已认证的Authentication对象并返回,如果查询不到用户信息,则抛出相应的异常。
- 认证成功后,用户会被重定向到之前访问的资源。如果之前访问的资源需要特定的角色或权限才能访问,则还需要进行授权的过程。
基于内存的用户认证
使用配置类来配置账号密码,访问效果和在配置文件中配置效果相同,但是使用配置类后,配置文件中的配置就不生效了
@Configuration
// 开启spring Security的自定义配置,在springBoot项目中可以省略此注解,因为Security-stater默认开启了自定义配置
@EnableWebSecurity
public class SecurityConfig {
@Bean
public UserDetailsService userDetailsService() {
// 这里测试的是基于内存的情况,实际情况并不会使用
InMemoryUserDetailsManager man = new InMemoryUserDetailsManager();
// 使用InMemoryUserDetailsManager管理user对象
man.createUser(User.withDefaultPasswordEncoder().username("lisi").password("123465789")
.roles("admin").build());
// 将用户的信息以bean的形式存入spring容器
return man;
}
}
UsernamePasswordAuthenticationFilter: 过滤器
- 是Spring Security提供的默认身份验证过滤器,用于处理基于表单的用户名密码身份验证请求。
- 监听POST请求的"/login"路径,接收用户名和密码等凭证,并将其封装为一个 UsernamePasswordAuthenticationToken 对象,然后通过 AuthenticationManager 进行身份验证。
- 登录请求中携带的用户信息,一定要是 username 和 password ,如果想要变更参数名称,需要设置
usernameParameter
和passwordParameter
两个参数的名称
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
// 配置自定义表单的参数名称
.formLogin(form->form.
usernameParameter("account").
passwordParameter("pass").
failureUrl("/error") //检验出错时路由的地址
)
.httpBasic(withDefaults());
return http.build();
}
修改参数名后,请求示例:
密码加密算法
hash算法
- 只能加密,不能解密,即使密码泄漏也只能暴力破解
彩虹表
- 彩虹表攻击(Rainbow Table Attack)是一种密码破解技术,用于破解密码哈希的安全性。它是一种预先计算密码哈希和其对应明文的巨大数据表的方法。虽然彩虹表攻击不是直接暴力破解密码,但它可以更快地破解加密密码,尤其是当使用弱密码时。
- 密码通常不会以明文形式存储在数据库中,而是将其哈希后的值存储在数据库中。哈希是一种单向函数,将输入数据转换为固定长度的哈希值。例如,常用的哈希函数如MD5、SHA-1、SHA-256等。因此,当用户登录时,系统会将其提供的密码进行哈希,然后将其与数据库中存储的哈希值进行比较。
- 彩虹表攻击的基本原理如下:
- 预计算阶段:攻击者使用密码哈希函数和彩虹表生成算法,预先计算大量的哈希值和对应的明文密码,并将结果存储在一个庞大的彩虹表中。这个表可以包含数亿条数据项。
- 破解阶段:当攻击者获得了存储在目标系统中的密码哈希值后,他们可以在彩虹表中查找匹配项。如果哈希值在彩虹表中找到了匹配,攻击者就可以通过查找对应的明文密码来获取用户的原始密码。
- 彩虹表攻击的限制:
- 彩虹表攻击的成功率受到彩虹表的规模和生成算法的影响。如果目标密码很复杂,彩虹表需要更大的规模才能成功破解。
- 一些安全措施可以减轻彩虹表攻击的影响,如“盐”(Salt)。盐是一个随机值,附加在用户密码上然后一同进行哈希。这使得即使两个用户使用相同的密码,由于不同的盐,他们的哈希值也是不同的。
- 或者使用更强大、更缓慢的哈希函数(如bcrypt、scrypt)可以大幅降低彩虹表攻击的成功率。
- 但是随着计算机性能的提升,hash算法越来越容易被破解,所以Spring Security 采用自适应单向函数来存储密码
在Spring Security 中
- 使用的是一种自适应单向函数 (Adaptive One-way Functions)来处理密码问题
- 这种自适应单向函数在进行密码匹配时,会有意占用大量系统资源(例如CPU、内存等),这样可以增加恶意用户攻击系统的难度。
- 由于自适应单向函数有意占用大量系统资源,因此每个登录认证请求都会大大降低应用程序的性能,
- 通常把 Spring Secuity 验证密码验证调整到约1秒钟左右,这样可以大大减缓密码被破解的风险,增强系统的安全性。
- 在Spring Securiy 中自适应单项函数有: bcrypt、PBKDF2、sCrypt 以及 argon2 ,默认为 bcrypt
PasswordEncoder 自适应单项函数具体实现:
- BCryptPasswordEncoder 使用 bcrypt 算法对密码进行加密,为了提高密码的安全性,bcrypt算法故意降低运行速度,以增强密码破解的难度。同时 BCryptP asswordEncoder “为自己带盐”开发者不需要额外维护一个“盐” 字段,使用 BCryptPasswordEncoder 加密后的字符串就已经“带盐”了,即使相同的明文每次生成的加密字符串都不相同。
- Argon2PasswordEncoder 使用 Argon2 算法对密码进行加密,Argon2 曾在 Password Hashing Competition 竞赛中获胜。为了解决在定制硬件上密码容易被破解的问题,Argon2也是故意降低运算速度,同时需要大量内存,以确保系统的安全性。
- Pbkdf2PasswordEncoder 使用 PBKDF2 算法对密码进行加密,和前面几种类似,PBKDF2算法也是一种故意降低运算速度的算法,当需要 FIPS (Federal Information Processing Standard,美国联邦信息处理标准)认证时,PBKDF2 算法是一个很好的选择。
- SCryptPasswordEncoder 使用scrypt 算法对密码进行加密,和前面的几种类似,serypt 也是一种故意降低运算速度的算法,而且需要大量内存。
BCryptPasswordEncoder
- encode(“password”),中有一个随机数参与了运算,所以即使同样的密码,多次加密得到的密文也不一样
- matches(“password”,“盐值”);
DelegatingPasswordEncoder
-
如果不自定义容器的
PasswordEncoder
,默认会通过DelegatingPasswordEncoder
来创建加密器 -
实现了
PasswordEncoder
,可以生成不同加密算法的加密器,默认使用的加密算法是BCryptPasswordEncoder
,生成的密文会带上加密算法名称的前缀( {前缀}+密文 ) -
因为需要根据密文的前缀来确定是那种加密算法
如何对密码进行加密
- 在 Spring Security 中对密码进行加密通常使用的是密码编码器(PasswordEncoder)。
- PasswordEncoder 的作用是将明文密码加密成密文密码,以便于存储和校验。Spring Security 提供了多种常见的密码编码器,例如 BCryptPasswordEncoder、SCryptPasswordEncoder、StandardPasswordEncoder 等。
- 引入依赖,创建配置类后,在使用密码的地方调用 passwordEncoder.encode() 方法对密码进行加密即可
基于数据库的认证
注入密码的加密方式
- 在Spring Security 5.7.0-M2中,Spring就废弃了WebSecurityConfigurerAdapter
@Configuration
// 以省略其他无关内容
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(10);
}
}
自定义DbUserDetailsManager
- 参考
InMemoryUserDetailsManager
实现UserDetailsManager
和UserDetailsPasswordService
@Slf4j
@Component
public class DbUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {
@Autowired
private UsersMapper usersMapper;
/**
* 根据传入的账号,获取用户信息
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("username:" + username);
// 用户名称不唯一,这里的账号名account,不是用户名
// 这里user实现了Security的UserDetails对象,去数据库查询用户信息
UsersDO user = usersMapper.selectByAccount(username);
if (user == null || StringUtils.isEmpty(user.getPassword())) {
throw new UsernameNotFoundException(username + ":用户不存在");
}
return user;
}
public UserDetails updatePassword(UserDetails user, String newPassword) {
return null;
}
public void createUser(UserDetails user) {
}
public void updateUser(UserDetails user) {
}
public void deleteUser(String username) {
}
public void changePassword(String oldPassword, String newPassword) {
}
public boolean userExists(String username) {
return false;
}
}
自定以的user类
@Data
@TableName("tm_user")
// 实现了Security的UserDetails对象
public class UsersDO implements UserDetails {
private String id;
private String account;
private String username;
private String password;
private String sex;
private String age;
private String phoneNumber;
private String departmentNo;
private String position;
private String described;
private String cname;
@TableLogic
@TableField(value = "DELETED", fill = FieldFill.INSERT, jdbcType = JdbcType.VARCHAR)
private String deleted;
@Override
public boolean isEnabled() {
// 通过逻辑删除字段判断是否启用
return Integer.valueOf(this.getDeleted()) == 0 ? true : false;
}
public boolean isAccountNonExpired() {
//用户是否未过期
return true;
}
public boolean isCredentialsNonExpired() {
// 用户凭证是否未过期
return true;
}
public Collection<? extends GrantedAuthority> getAuthorities() {
// 权限列表
return null;
}
public boolean isAccountNonLocked() {
// 用户是否没有被锁定
return true;
}
}
Security 的默认配置
- 这里的SecurityFilterChain就是默认的过滤器链
- authorizeHttpRequests 是开启授权保护
- anyRequest() 表示对所有请求开启授权保护
- authenticated() 对以认证的请求自动授权
- formLogin 使用表单授权方式,生成默认的登录、登出页
- httpBasic 使用基本授权方式,不会生成默认的登录、登出页,使用的是浏览器提供的用户名密码的输入弹框
/**
* 这里是 Security 的默认配置
* @param http
* @return
* @throws Exception
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.formLogin(withDefaults())
.httpBasic(withDefaults());
// 为例测试方便,关闭 csrf 攻击防护 (不关闭,所有的post的请求都会被拦截)
http.csrf(a -> a.disable());
return http.build();
}
自定义认证
自定义认证
AuthenticationSuccessHandler
接口的作用是做用户认证成功后执行的操作处理器AuthenticationFailureHandler
接口的作用是做用户认证失败后执行的操作处理器LogoutSuccessHandler
接口的作用是做用户登场后执行的操作AuthenticationEntryPoint
请求未认证处理逻辑- 跨域处理:
http.cors(withDefaults());
- 会话并发处理:
SessionInformationExpiredStrategy
可以控制一个账号的最大登录数量
自定义AuthenticationSuccessHandler
、AuthenticationFailureHandler
和LogoutSuccessHandler
@Configuration
@Slf4j
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler, AuthenticationFailureHandler,
LogoutSuccessHandler, AuthenticationEntryPoint {
/**
* 认证成功
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
log.info("返回成功-------------->");
response.setContentType("application/json;charset=UTF-8");
Map<String, Object> res = new HashMap<>();
res.put("code", "0");
res.put("message", "登录成功");
Map<String, Object> data = new HashMap<>();
// 用户身份信息
data.put("principal", authentication.getPrincipal());
// 用户凭证信息
data.put("credentials", authentication.getCredentials());
// 用户的权限信息
data.put("authorities", authentication.getAuthorities());
res.put("data", data);
response.getWriter().println(JSONUtil.toJsonStr(res));
}
/**
* 认证失败
*/
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
log.info("返回失败-------------->");
response.setContentType("application/json;charset=UTF-8");
Map<String, Object> res = new HashMap<>();
res.put("code", "-1");
res.put("message", "登录失败");
Map<String, Object> data = new HashMap<>();
data.put("message", exception.getMessage());
data.put("localizedMessage", exception.getLocalizedMessage());
res.put("data", data);
response.getWriter().println(JSONUtil.toJsonStr(res));
}
/**
* 登出逻辑
*/
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
log.info("执行登出逻辑-------------->");
response.setContentType("application/json;charset=UTF-8");
Map<String, Object> res = new HashMap<>();
res.put("code", "0");
res.put("message", "注销成功");
Map<String, Object> data = new HashMap<>();
// 用户身份信息
data.put("principal", authentication.getPrincipal());
// 用户凭证信息
data.put("credentials", authentication.getCredentials());
// 用户的权限信息
data.put("authorities", authentication.getAuthorities());
res.put("data", data);
response.getWriter().println(JSONUtil.toJsonStr(res));
}
/**
* 请求未认证
*/
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
log.info("执行请求未认证逻辑-------------->");
response.setContentType("application/json;charset=UTF-8");
Map<String, Object> res = new HashMap<>();
res.put("code", "-1");
res.put("message", "请求未认证");
Map<String, Object> data = new HashMap<>();
data.put("message", authException.getMessage());
data.put("localizedMessage", authException.getLocalizedMessage());
res.put("data", data);
response.getWriter().println(JSONUtil.toJsonStr(res));
}
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
log.info("返回成功-------------->");
HttpServletResponse response = event.getResponse();
response.setContentType("application/json;charset=UTF-8");
Map<String, Object> res = new HashMap<>();
res.put("code", "-1");
res.put("message", "该账号已从其他设备登录");
Map<String, Object> data = new HashMap<>();
res.put("data", data);
response.getWriter().println(JSONUtil.toJsonStr(res));
}
}
最后需要在formLogin
中注册自定义的配置
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> {
// 放行请求
authorize.requestMatchers("/assets/**"
, "/login"
, "/test/**"
).permitAll()
.anyRequest().authenticated();
}
);
// 配置登录自定义表单的参数名称 formLogin(withDefaults()) :默认的登录表单
http.formLogin(form -> form.
// loginPage("/login").permitAll().
usernameParameter("account").
passwordParameter("pass").
failureUrl("/error") //检验出错时路由的地址
.successHandler(new MyAuthenticationSuccessHandler())
.failureHandler(new MyAuthenticationSuccessHandler())
)
// 配置登出
.logout(logout -> logout.logoutSuccessHandler(new MyAuthenticationSuccessHandler()))
.httpBasic(withDefaults());
// 用户未认证
// http.exceptionHandling(exc -> exc.authenticationEntryPoint(new MyAuthenticationSuccessHandler()));
http.sessionManagement(session ->{
// 一个账号最多允许几处登录,以及对应的处理逻辑
session.maximumSessions(1).expiredSessionStrategy(new MyAuthenticationSuccessHandler());
});
// 为例测试方便,关闭 csrf 攻击防护 (不关闭,所有的post的请求都会被拦截)
// http.csrf(a -> a.disable());
// 开启跨域请求
http.cors(withDefaults());
return http.build();
}
最终成功登录不在自动跳转页面,而是返回了预定义的json
登录失败也会返回预定义的错误信息
注销返回结果
请求未认证页面,Spring Security默认情况下这里会跳转到登录页
一个账号,在多个客户端登录,后登录的会挤掉先登录的
Authentication
包含用户认证信息
-
SecurityContextHolder中包含了SecurityContext对象,SecurityContext中包含了Authentication对象
-
可以获取当前验证通过的用户信息
-
其中包含:
- principal:用户身份,如果是用户/密码认证,这个属性就是UserDetails实例
- credentials:通常就是密码,在大多数情况下,在用户验证通过后就会被清除,以防密码泄露。
- authorities:用户权限
/**
* 获取当前登录用户信息
*/
@GetMapping("/get")
public BaseResultModel get() {
SecurityContext securityContext =SecurityContextHolder.getContext();
Authentication authentication = securityContext.getAuthentication();
return BaseResultModel.success(authentication);
}
登录不同的用户,获取当前用户的信息