文章目录
SpringSecurity介绍
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LmzUJhvV-1672023223305)(images/1583585891579.png)]
Spring Security是基于spring的应用程序提供声明式安全保护的安全性框架,它提供了完整的安全性解决方案,能够在web请求级别和方法调用级别处理身份证验证和授权,它充分使用了依赖注入和面向切面的技术。
Spring Security 的前身是 Acegi Security,后来成为了 Spring 在安全领域的顶级项目,并正式更名到 Spring 名下,成为 Spring 全家桶中的一员,所以 Spring Security 很容易地集成到基于 Spring 的应用中来。Spring Security 在用户认证方面支持众多主流认证标准,包括但不限于 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等,在用户授权方面,Spring Security 不仅支持最常用的基于 URL 的 Web 请求授权,还支持基于角色的访问控制(Role-Based Access Control,RBAC)以及访问控制列表(Access Control List,ACL)等。
Spring Security 是 Spring 家族中的一个安全管理框架,Spring Security 已经发展了多年了,但是使用的并不多,安全管理这个领域,一直是 Shiro 的天下。相对于 Shiro,在 SSM/SSH 中整合 Spring Security 都是比较麻烦的操作,所以,Spring Security 虽然功能比 Shiro 强大,但是使用反而没有 Shiro 多,Shiro 虽然功能没有 Spring Security 多,但是对于大部分项目而言,Shiro 也够用了。
SpringSecurity主要是从两个方面解决安全性问题:
web请求级别:使用servlet过滤器保护web请求并限制URL级别的访问
**方法调用级别:**使用Spring AOP保护方法调用,确保具有适当权限的用户才能调用方法.
权限相关概念
主体(principal)
使用系统的用户或设备或从其他系统远程登录的用户等等。认证(authentication)
权限管理系统确认一个主体的身份,允许主体进入系统。授权(authorization)
将操作系统的“权力”“授予”“主体”,这样主体就具备了操作系统中特定功能的能力。
SpringSecurity主要功能
- 认证就是确定主体的过程,当未认证的主体访问系统资源的时候,系统会对主体的身份进行验证,确定该主体是否有合法的身份,不合法的主体将被应用拒绝访问,这一点也很容易理解,比如某电商网站,未登录的用户是无法访问敏感数据资源的,比如订单信息。
- 授权是在主体认证结束后,判断该认证主体是否有权限去访问某些资源,没有权限的访问将被系统拒绝,比如某电商网站的登录用户去查看其它用户的订单信息,很明显,系统会拒绝这样的无理要求。
- 攻击防护:防止攻击伪造
环境搭建
POM
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
Controller
@Controller
public class UserController {
@RequestMapping(value = "/hello")
@ResponseBody
public String hello(){
System.out.println("UserController.hello");
return "Hello Sprign Security";
}
}
访问
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LM3sE9yz-1672023223306)(images/1583592152841.png)]
Spring Boot 项目引入了 Spring Security 以后,自动装配了 Spring Security 的环境,Spring Security 的默认配置是要求经过了 HTTP Basic 认证成功后才可以访问到 URL 对应的资源,且默认的用户名是 user,密码是自动随机生成一串 UUID 字符串,输出到了控制台日志里,如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WcJzJtpe-1672023223306)(images/1583592227948.png)]
身份认证的三种方式
配置文件
spring:
security:
user:
name: admin
password: 123
配置类
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
// spring security 官方推荐的是使用bcrypt加密方
// SpringSecurity在对表单密码加密的时候需要用到这个bean,所以这里需要给它创建。如果不创建会登录会报错。
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 对原始密码加密
String encode = bCryptPasswordEncoder().encode("123");
auth.
inMemoryAuthentication()
// 设置加密算法
.passwordEncoder(bCryptPasswordEncoder())
// 设置用户名
.withUser("admin")
// 设置密码
.password(encode)
// 设置角色,
.roles("admin")
// 设置权限
.authorities("user:add","user:update");
}
}
自定义实现类(常用)
通过UserDetailsService来查询用户的密码,最后把用户封装成一个user返回给SpringSecurity。
MyUserDetailsService
在
Spring Security
中,用户的登录操作都是通过 UserDetailsService完成的,它是一个接口,该接口中只有一个loadUserByUsername(),在用户登录的时候SpringSecurity就会调用这个方法,该方法返回一个UserDetails对象,这个对象就代表了当前认证的主体,需要从数据库中根据用户名查询,从而返回出去,SpringSecurity会把该方法的返回值放到Session中,如果验证失败这个出现异常会抛出一个UsernameNotFoundException。
@Configuration
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private BCryptPasswordEncoder passwordEncoder;
/**
* 根据用户名查询密码
* @param username 表单输入的用户名
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("s = [" + s + "]");
// 在这里可以根据用户名从数据库中查询密码
// User user = baseMapper.getUserByUsername(username);
//if(user == null){
// 如果用户名不存在直接抛出异常
// throw new UsernameNotFoundException("用户名不存在");
//}
// 密码加密
String password = passwordEncoder.encode("123");
// 权限
List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("admin");
// 返回user(用户名,密码,权限)
return new User("点toString",password,authorities);
}
}
SpringSecurityConfig
在SpringSecurityConfig中设置使用UserDetailsService来加载用户的密码。
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
// spring security 官方推荐的是使用bcrypt加密方
// SpringSecurity在对表单密码加密的时候需要用到这个bean,所以这里需要给它创建。如果不创建会登录会报错。
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService) // 自定义实现类完成密码的设置
.passwordEncoder(passwordEncoder()); // 设置密码加密算法
}
}
自定义登录页面
SpringSecurityConfig中复写认证方法
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 对登录操作进行设置
.loginPage("/toLogin") // 登录页面的地址
.loginProcessingUrl("/login") // 处理用户登录的地址
.defaultSuccessUrl("/toSuccess") // 登录成功后跳转的地址
.and()
.authorizeRequests()
.antMatchers("/","/hello","/toLogin","/login").permitAll() // 配置不需要认证的路径(匿名访问)
.anyRequest().authenticated() // 其他的路径都需要认证
.and().csrf().disable(); // 关闭csrf防护
}
设置视图跳转页面
@Configuration
public class WebMVCConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("toLogin").setViewName("login");
registry.addViewController("toSuccess").setViewName("success");
}
}
Controller
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello(){
System.out.println("HelloController.hello");
return "ok";
}
@RequestMapping("/hello2")
public String hello2(){
System.out.println("HelloController.hello2");
return "ok";
}
}
登录页面
<!-- 显示认证失败信息-->
<div th:if="${param.error}" th:text="${session?.SPRING_SECURITY_LAST_EXCEPTION?.message}"></div>
<form action="/login" method="POST">
username:<input type="text" name="username" value="admin"><br>
password:<input type="text" name="password" value="123"><br>
<input type="submit" value="登录">
</form>
上面的表示如果有调用显示,没有就不调用后面的方法。
登录页面中的用户和密码默认是username和password,因为SpringSecurity在认证的的时候从requset中默认获取的就是username和password
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bqMRyxEu-1672023223307)(images/1615010259497.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sHBoUtdK-1672023223307)(images/1615010243935.png)]
修改登录页面默认的表单属性
设置自定义登录表单属性
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 对登录操作进行设置
.loginPage("/toLogin") // 登录页面的地址
.loginProcessingUrl("/login") // 处理用户登录的地址
.defaultSuccessUrl("/toSuccess") // 登录成功后跳转的地址
// 设置自定义的登录表单属性
.usernameParameter("name")
.passwordParameter("pw")
.and()
.authorizeRequests()
.antMatchers("/","/hello","/toLogin","/login").permitAll() // 配置不需要认证的路径
.anyRequest().authenticated() // 其他的路径都需要认证
.and().csrf().disable(); // 关闭csrf防护
}
修改表单属性
<form action="/login" method="POST">
username:<input type="text" name="name" value="admin"><br>
password:<input type="text" name="pw" value="123"><br>
<input type="submit" value="登录">
</form>
用户注销
http.formLogin()
.loginPage("/toLogin")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/toSuccess")
.usernameParameter("name")
.passwordParameter("pw")
.and()
.logout() // 添加注销处理
.logoutUrl("/toLogout") // 注销地址
.logoutSuccessUrl("http://www.baidu.com") // 注销成功后调整的地址
//退出处理器,可以写匿名内部类
.addLogoutHandler(new LogoutHandler() {
@Override
public void logout(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) {
}
})
// 设置该方法会覆盖logoutSuccessUrl()方法
// 退出成功处理器
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
}
})
权限控制
SpringSecurity中可以通过角色和权限进行权限的控制,主体登录成功对访问的资源进行判断是否有这个权限,如果有就执行,否则就会跳转到没有权限访问的页面。
Spring Security 中对于权限控制默认已经提供了很多了 , 其中有四种时常用的权限控制方式。
- 表达式控制 URL 路径权限
- 使用过滤注解
- 动态权限
表达式控制URL路径权限
给用户设置角色和权限
在SpringSecurity中角色和权限并没有分开,而是都统一的封装到了一个 List中。其中通过ROLE_来区分该权限是角色还是权限。
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
System.out.println("s = [" + s + "]");
// 密码加密
String password = passwordEncoder.encode("123");
List<GrantedAuthority> authorities = null;
if ("admin".equals(s)) {
// authorities = AuthorityUtils.createAuthorityList("user:add","ROLE_admin");
// 给登录用户设置角色和权限
authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("user:add,ROLE_admin");
} else if ("tomcat".equals(s)) {
authorities = AuthorityUtils.createAuthorityList("user:update","ROLE_manager");
}
// 权限
// 返回user(用户名,密码,权限)
return new User(s, password, authorities);
}
为什么角色前面需要加ROLE_?
因为SpringSecurity再给资源绑定角色的时候底层自动给角色前面加了ROLE_,所以我们这里也需要添加前缀才能和真实的角色名称匹配
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kQBUeCMk-1672023223308)(images/1615101994748.png)]
两种设置权限的区别:
AuthorityUtils.createAuthorityList:设置用户的权限和角色,可以接收多个参数
AuthorityUtils.commaSeparatedStringToAuthorityList:设置用户的权限和角色,只能接收一个参数,多个权限之间使用逗号隔开。
给资源绑定权限和角色
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/toLogin")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/toSuccess")
.usernameParameter("name")
.passwordParameter("pw")
.and()
.authorizeRequests()
.antMatchers("/","/hello","/toLogin","/login").permitAll()
// 给资源绑定权限
// 访问hello5需要user:add权限
// .antMatchers("/hello3").hasAuthority("user:add")
// 访问hello4,需要user:add或者user:update权限才能访问 .antMatchers("/hello4").hasAnyAuthority("user:add","user:update")
// 给资绑定角色
// 访问hello3需要admin角色
.antMatchers("/hello3").hasRole("admin" )
// 访问hello4需要admin或者manager角色
.antMatchers("/hello4").hasAnyRole("admin","manager")
.anyRequest().authenticated()
.and().csrf().disable();
}
基于注解控制权限
SpringSecurity中提供的三个注解来控制权限
- @PreAuthorize:方法执行前进行权限检查
- @PostAuthorize:方法执行后进行权限检查
- @Secured:方法执行之前进行角色的检查
@Secured
// 方法该方法需要admin或manager的角色
@Secured({"ROLE_admin","ROLE_manager"}) // 角色需要添加前缀
@RequestMapping("/hello3")
public String hello3(){
System.out.println("HelloController.hello3");
return "hello3";
}
@SpringBootApplication(scanBasePackages = "com.ts")
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SpringSecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SpringSecurityApplication.class, args);
}
}
@PreAuthorize
// 必须有user:add权限才可以方法这个方法
// @PreAuthorize("hasAuthority('user:add')") // 注意这里写的表达式
// 必须有admin这个角色才才可以访问这个方法
@PreAuthorize("hasRole('ROLE_admin')")
@RequestMapping("/hello4")
public String hello4(){
System.out.println("HelloController.hello4");
return "hello4";
}
// 表示访问该方法的 age 参数必须大于 10,否则请求不予通过。
@PreAuthorize("#age>10")
@RequestMapping("/hello5")
public String hello5(Integer age) {
return String.valueOf(age);
}
如果想引用方法的参数,前面加上一个
#
即可,既可以引用基本类型的参数,也可以引用对象参数。
@SpringBootApplication(scanBasePackages = "com.ts")
@EnableGlobalMethodSecurity(securedEnabled = true,,prePostEnabled = true)
public class SpringSecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SpringSecurityApplication.class, args);
}
}
@PostAuthorize
// 在方法执行之后校验是否有user:add权限
@PostAuthorize("hasAuthority('user:add')")
@RequestMapping("/hello6")
public String hello6(){
System.out.println("HelloController.hello6");
return "hello5";
}
动态控制权限
定义一个查询用户url的类,在用户请求任何url时都会调用该类中的方法,在该方法中查询用户所拥有的url,然后和用户正在访问的url对比,如果有就返回true,否则就false。
UrlAuthService
@Component
public class UrlAuthService {
private AntPathMatcher antPathMatcher = new AntPathMatcher();
public boolean hashPermission(HttpServletRequest request, Authentication authentication){
boolean hashPer = false;
Object principal = authentication.getPrincipal();
if(principal instanceof SecurityUser){
SecurityUser securityUser = (SecurityUser)principal;
// 查询用户所有的权限,放到集合汇总
Set<String> urls = new HashSet<>();
if("admin".equals(securityUser.getUser().getUsername())){
urls.add("/update");
}
// 循环遍历用户是否有当前url的权限
for(String url:urls){
// 因为权限表示可能会包含getUserById/*,所以这里不能用equals比较
if(antPathMatcher.match(url,request.getRequestURI())){
hashPer=true;
break;
}
}
}
return hashPer;
}
}
配置权限验证时候调用自定义类
// 任何请求都要经过身份验证
//.anyRequest().authenticated()
// 通过表示调用hashPermission方法
.anyRequest().access("@urlAuthService.hashPermission(request,authentication)")
@urlAuthService代表Spring容器中的bean名称,request和authentication Spring在调用的时候回自动注入。
设置无权限访问时跳转的页面
无权限跳转到指定页面
当用户在访问一个资源时,如果没有权限访问这个资源,SpringSecurity默认会反问403状态码。这个状态一般在开发中都不用,所以我们可以设置自定义提示信息。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling() // 开启拒绝访问后的处理
// 跳转到没有权限的页面(也可以访问JSON数据)
.accessDeniedPage("/toOnAuth")
// 无权访问资源时调用的处理器,会覆盖accessDeniedPage()
.accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
httpServletResponse.sendRedirect("toOnAuth");
}
});
http.formLogin()
.loginPage("/toLogin")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/toSuccess")
.usernameParameter("name")
.passwordParameter("pw")
.and()
.authorizeRequests()
.antMatchers("/","/hello","/toLogin","/login").permitAll()
// .antMatchers("/hello3").hasAuthority("user:add")
// .antMatchers("/hello4").hasAnyAuthority("user:add","user:update")
.antMatchers("/hello3").hasRole("admin" )
.antMatchers("/hello4").hasAnyRole("admin","manager")
.anyRequest().authenticated()
.and().csrf().disable();
}
无权限响应JSON数据
@RequestMapping(value = "/toOnAuth")
@ResponseBody
public String toOnAuth(){
return "你没有权限访问该资源";
}
CSRF
CSRF是什么
CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF。
CSRF可以做什么
你这可以这么理解CSRF攻击:攻击者盗用了你的身份,以你的名义发送恶意请求。CSRF能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账…造成的问题包括:个人隐私泄露以及财产安全。
CSRF原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G9nbYwCc-1672023223309)(images/1583762792137.png)]
从上图可以看出,银行网站通过cookie来识别用户,当用户成功进行身份验证之后浏览器就会得到一个标识其身份的cookie,只要不关闭浏览器或者退出登录,以后访问银行网站会一直带上这个cookie。如果这期间浏览器被人控制向银行网站发起请求去执行一些用户不想做的功能(比如添加账号),这就是会话劫持了。因为这个不是用户真正想发出的请求,这就是所谓的“请求伪造”。
Spring Security 对 CSRF防护原理
从Spring Security 4.0开始,默认情况下会启用CSRF保护,以防止CSRF攻击应用程序,Spring Security CSRF会针对PATCH,POST,PUT和DELETE方法进行防护。我这边是spring boot项目,在启用了@EnableWebSecurity注解后,csrf保护就自动生效了。所以在默认配置下,即便已经登录了,页面中发起PATCH,POST,PUT和DELETE请求依然会被拒绝,并返回403,需要在请求接口的时候加入csrfToken才行。如果你使用了freemarker之类的模板引擎或者jsp,针对表单提交,可以在表单中增加如下隐藏域
实现CSRF防护
表单设置CSRFToken
<form action="/login" method="POST">
<!-- csrf Token -->
<input type="hidden" name="_csrf" th:value="${_csrf.token}">
username:<input type="text" name="username" value="admin"><br>
password:<input type="text" name="password" value="123"><br>
<input type="submit" value="登录">
</form>
表单提交后的效果如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qLjck57R-1672023223309)(images/1583761357556.png)]
异步请求设置CSRFToken
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
$.ajax({
url:url,
type:'POST',
async:false,
dataType:'json',
beforeSend: function(xhr) {
xhr.setRequestHeader(header, token); //发送请求前将csrfToken设置到请求头中
},
success:function(data,textStatus,jqXHR){
}
});
服务端关闭或打开CSRF防护
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
System.out.println("SecurityConfig.configure1");
httpSecurity.
.authorizeRequests()
.antMatchers("/","hello") .permitAll()
.anyRequest().authenticated()
// 禁用跨站伪造,默认是开启CSRF防护
.and().csrf().disable()
;
}
如果你不想启用CSRF保护,可以在spring security配置中取消csrf 。如果打开scrf那SpringSecurity每个POST请求都会判断scrf
记住我
登录框中通常有一个“记住我”的checkbox按钮,它是用来记住当前用户输入的用户名和密码,下次用户再次登录的时候就不用重新输入直接点击登录就可以了(现在很多浏览器自身就带有这样的功能)。该功能使用Cookie就可以实现了。
实现原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c1nPFQty-1672023223310)(images/1615116362163.png)]
当用户发起认证请求,会通过UsernamePasswordAuthenticationFilter,在认证成功之后,可以调用SpringSecurity提供的RememberMeServices,它会生成一个Token并将它写入浏览器的Cookie中,同时这个它里面有一个JdbcTokenRepositoryImpl会将Token放入数据库中。
当下次浏览器再请求的时候,会经过RememberMeAuthenticationFilter,在这个filter里面会读取Cookie中的Token,然后去数据库中查找是否有相应的Token,然后再通过UserDetailsService获取用户的信息。
配置JdbcTokenRepository
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private DataSource dataSource
// 配置JdbcTokenRepository
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setCreateTableOnStartup(true); // 自动在数据库创建存放token的表
jdbcTokenRepository.setDataSource(dataSource);
return jdbcTokenRepository;
}
}
开启Remember配置
.and()
.rememberMe()// 设置rememberMeg规则
// // 设置查询数据库的bean
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(30) // cookie的有效时间,单位是s
// 设置userDetailsService查询用户信息要用到
.userDetailsService(userDetailsService)
表单添加remember-me属性
<form action="/login" method="POST">
username:<input type="text" name="username" value="admin"><br>
password:<input type="text" name="password" value="123"><br>
记住我<input type="checkbox" name="remember-me" value="1">
<input type="submit" value="登录">
</form>
用户登录的时候必须要勾选记住我才能实现该功能,底层是通过Cookie实现。用户勾选记住我登录成功后服务端会写给浏览器一个Cookie,这个Cookie中存放了用户的等信息的凭证,下次在请求的时候浏览器会自动带着这个Cookie,服务端获取到Cookie后会对这个Cookie进行检查,如果检查没有问题就不用在登录。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h4kxVHqM-1672023223310)(images/1583810082873.png)]
密码加密
为需要什么加密
我们先来说一下使用加密后的密码处理流程。我们为什么要使用密文呢?也就是加密后的密码呢?如果一个用户在网站注册时写入的密码是123456,当他点击保存之后,如果不加密的话,在数据库中存的就是123456,这样有什么不好吗?答案是肯定的,想想12306密码泄露导致用户所有信息泄露,是用户密码不够复杂吗?可能是?但是最主要的原因是数据库中如果使用的是明文,黑客很容易就会进行破解,而且一旦破解了一个密码可能就会拿到一组数据,为了方便大家理解,画了张草图:而且数据库中密码为123456的用户绝对不是2个,大家可以想象后果由多可怕了吗。。。
MD5+salt
其实关于这个问题上网就可以收集到,这里不再赘述,我们这里主要说在SpringSecurity中使用MD5,我们知道单纯的MD5加密并不是安全的,而如果我们使用MD5加盐的方式去对密码进行加密,那么想要破解的话难度系数就会成倍出现,基本上是不可能破解的。大家都知道MD5是不可逆的,也就是说加密完的密码无法进行解密,要不然怎么会是安全的呢?那我们来看下如果使用了MD5加密的话,黑客在获取密码的时候会是怎样的呢?看图
虽然说张三和李四在注册的时候都使用的是123456作为密码。但是在保存到数据库之前,我们首先进行了MD5+salt操作,到数据库中的是显然不同的2个密文,这时黑客拿到密文之后进行破解,破解出来以后去登录,发现只有张三能登录,而李四的并不能登录,这时为什么呢?因为在登录的时候我们会把用户输入的明文以之前的加密方式再次加密得到一个密文,然后拿这个密文和数据库之前保存的密文比较,如果相同才会放行,显然张三和李四的密文不同,所以即使张三的密码被破译了,但是李四的123456密码还是安全的, 这样的话就可以对密码进行很好的保护,这也就是SpringSecurity对密码进行比较的流程。
SpringSecurity加密
Spring Security 为我们提供了一套加密规则和密码比对规则,org.springframework.security.crypto.password.PasswordEncoder 接口,该接口里面定义了三个方法。
public interface PasswordEncoder {
//加密(外面调用一般在注册的时候加密前端传过来的密码保存进数据库)
String encode(CharSequence rawPassword);
//加密前后对比(一般用来比对前端提交过来的密码和数据库存储密码, 也就是明文和密文的对比)
boolean matches(CharSequence rawPassword, String encodedPassword);
//是否需要再次进行编码, 默认不需要
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
该接口实现类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fJNwMCdK-1672023223311)(images/1583827055318.png)]
其中常用到的分别有下面这么几个
- BCryptPasswordEncoder:Spring Security 推荐使用的,使用BCrypt强哈希方法来加密。
- MessageDigestPasswordEncoder:用作传统的加密方式加密(支持 MD5、SHA-1、SHA-256…)
- DelegatingPasswordEncoder:最常用的,根据加密类型id进行不同方式的加密,兼容性强
- NoOpPasswordEncoder:明文, 不做加密(已过时)
静态资源文件忽略
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.
// 进行身份验证,每个子匹配器将会按照声明的顺序起作用
authorizeRequests()
//配置要忽略的资源
.antMatchers("/","/js/**").permitAll()
// 任何请求都可以匿名访问
.anyRequest().anonymous();
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css/**");
}
permitAll(): 会被SpringSecurity的Filter拦截到,给没有登录的用户适配一个AnonymousAuthenticationToken,设置到SecurityContextHolder,方便后面的filter可以统一处理authentication, 其中包含了登录的以及匿名的 。
anonymous(): 主要功能就是给没有登陆的用户,填充AnonymousAuthenticationToken到SecurityContextHolder的Authentication,后续依赖Authentication的代码可以统一处理。
web ignore :比较适合配置前端相关的静态资源,它是完全绕过spring security的所有filter的。
SpringSecurity标签
SprignBoot的版本和SpringSecurity的版本存在很多地方的冲突,标签这里演示一定要设置SpringBoot和SpringSecurity版本。
添加依赖
<!-- SpringBoot版本使用2.0.0.RELEASE -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- SpringSecurity和Thymeleaf整合版本为3.0.2.RELEASE -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
<version>3.0.2.RELEASE</version>
</dependency>
封装登录的用户类
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
import java.util.List;
public class SecurityUser extends User { // 继承SpringSecurity提供的User类
private com.qf.entity.User user; // 用户自定义user
public SecurityUser(com.qf.entity.User user,List<GrantedAuthority> authorities){
super(user.getUsername(),user.getPassword(),authorities);
this.user = user;
}
public com.qf.entity.User getUser() {
return user;
}
}
UserDetailsService
@Component
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private IUserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1.根据username查询对象
User loginUser = userMapper.getUserByUsername(username);
// 2.创建要返回给SpringSecurity对象,该对象中包含登录用户的密码,权限等信息
SecurityUser securityUser = null;
if(loginUser != null){
List<GrantedAuthority> authorityList = null;
if("admin".equals(loginUser.getUsername())){
authorityList = AuthorityUtils.createAuthorityList("ROLE_admin");
}else{
authorityList = AuthorityUtils.createAuthorityList( "add");
}
// 把用户名,密码,权限,封装到User对象中
securityUser = new SecurityUser(loginUser,authorityList);
}
// 3.返回user,这个对象最终放入到session中
return securityUser;
}
这里需要注意的是loadUserByUsername()的返回值,登录成功后SpringSecurity会把这个方法的返回值放入到session中,为了页面获取方便所以我们所以使用SecurityUser封装。
页面标签
第一步引入头文件
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
第二步使用SpringSecurity标签控制页面显示
<div sec:authorize="isAuthenticated()">
<!-- 如果用户已经认证成功,就是现实里面的内容 -->
欢迎<span sec:authentication="principal.user.username"></span>登录<br
<!-- 也可以通过这种方式获取当前登录用户
<span th:text="${#authentication.principal.user.username}"></span><br>
-->
</div>
<div sec:authorize="isAuthenticated()==false">
<!-- 如果还没有认证就显示里面的内容 -->
你还没有的登录,请先<a href="/toLoginPage">login</a>
</div>
<!-- 如果有add权限就显示add超链接 -->
<a href="add" sec:authorize="hasAuthority('add')">add</a><br>
<!-- 如果有admin角色就显示update超链接 -->
<a href="update" sec:authorize="hasRole('admin')" >update</a><br>
控制器获取当前登录用户
/**
* 获取认证对象
* 1.直接从方法上注入
* 2.通过静态方法获取
*/
@RequestMapping(value = "/getLoginUser")
@ResponseBody
public String getLoginUser(Authentication authentication){ // 也可以直接注入进来
String currentUser = "";
Object principl = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if(principl instanceof UserDetails) {
currentUser = ((UserDetails)principl).getUsername();
}else {
currentUser = principl.toString();
}
return currentUser;
}
/**
* 直接获取登录对象
*/
@RequestMapping(value = "/me")
@ResponseBody
public SecurityUser me(@AuthenticationPrincipal SecurityUser user){
return user;
}
SpringSecurity核心组件
spring security核心组件有:SecurityContext、SecurityContextHolder、Authentication、Userdetails 和 AuthenticationManager,下面分别介绍。
SecurityContext
安全上下文,用户通过Spring Security 的校验之后,验证信息存储在SecurityContext中, 实际上其主要作用就是获取
Authentication
对象。
public interface SecurityContext extends Serializable {
/**
* Obtains the currently authenticated principal, or an authentication request token.
*
* @return the <code>Authentication</code> or <code>null</code> if no authentication
* information is available
*/
Authentication getAuthentication();
/**
* Changes the currently authenticated principal, or removes the authentication
* information.
*
* @param authentication the new <code>Authentication</code> token, or
* <code>null</code> if no further authentication information should be stored
*/
void setAuthentication(Authentication authentication);
}
SecurityContextHolder
在典型的web应用程序中,用户登录一次,会创建一个会话,该会话就储存在了SecurityContextHolder中, 其作用就是存储当或者获取前认证信息。
// 获取当前认证信息
Object principl = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
// 储存当前认证信息
SecurityContextHolder.getContext().setAuthentication(token);
Authentication
authentication 直译过来是“认证”的意思,在Spring Security 中Authentication用来表示当前用户是谁,一般来讲你可以理解为authentication就是一组用户名密码信息。Authentication也是一个接口,其定义如下:
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
其实现类有这4个属性。这几个方法作用如下:
getAuthorities
: 获取用户权限,一般情况下获取到的是用户的角色信息。getCredentials
: 获取证明用户认证的信息,通常情况下获取到的是密码等信息。getDetails
: 获取用户的额外信息,(这部分信息可以是我们的用户表中的信息)getPrincipal
: 获取用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails (UserDetails也是一个接口,里边的方法有getUsername,getPassword等)。isAuthenticated
: 获取当前 Authentication 是否已认证。setAuthenticated
: 设置当前 Authentication 是否已认证(true or false)。
UserDetails
存储的就是用户信息
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
方法含义如下:
getAuthorites
:获取用户权限,本质上是用户的角色信息。getPassword
: 获取密码。getUserName
: 获取用户名。isAccountNonExpired
: 账户是否过期。isAccountNonLocked
: 账户是否被锁定。isCredentialsNonExpired
: 密码是否过期。isEnabled
: 账户是否可用。
UserDetailsService
提到了
UserDetails
就必须得提到UserDetailsService
, UserDetailsService也是一个接口,且只有一个方法loadUserByUsername
,他可以用来获取UserDetails。 通常在spring security应用中,我们会自定义一个CustomUserDetailsService来实现UserDetailsService接口,并实现其
public UserDetails loadUserByUsername(final String login);
方法。我们在实现loadUserByUsername
方法的时候,就可以通过查询数据库(或者是缓存、或者是其他的存储形式)来获取用户信息,然后组装成一个UserDetails
,(通常是一个org.springframework.security.core.userdetails.User
,它继承自UserDetails) 并返回。 在实现
loadUserByUsername
方法的时候,如果我们通过查库没有查到相关记录,需要抛出一个异常来告诉spring security来“善后”。这个异常是org.springframework.security.core.userdetails.UsernameNotFoundException
。
自定义登录成功或失败处理
SpringSecurity登录成功后默认是跳转到未认证之前的URL,在实际开发中登录成功后会返回一个JOSN字符串,这时候就需要自定义登录成功后的处理了,登录失败也是一样。
httpSecurity
.formLogin() // 定义登录页
.loginPage("/toLoginPage") // 未认证通过后调整重定向到登录页面,不设置使用默认页面
.loginProcessingUrl("/login") // 处理登录的接口(验证用户名和密码)
.defaultSuccessUrl("/toOkPage") // 登录成功后调整的地址
.usernameParameter("username") // 设置登录表单的用户名属性,默认是username
.passwordParameter("password") // 设置登录表单的密码属性,默认是password
// 登录成功后的自定义处理,默认是跳转到未登录前访问的地址
.successHandler(new AuthenticationSuccessHandler(){
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
System.out.println("登录成功");
httpServletResponse.setContentType("application/json;charset=utf8");
// authentication:认证对象
// 把authentication对象转成JSON字符串
String json = objectMapper.writeValueAsString(authentication);
httpServletResponse.getWriter().write(json);
}
})
// 登录失败后调用
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
System.out.println("登录失败");
httpServletResponse.setContentType("application/json;charset=utf8");
// 返回状态码为500
httpServletResponse.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
// e:用户认证失败异常
// 把authentication对象转成JSON字符串
String json = objectMapper.writeValueAsString(e);
httpServletResponse.getWriter().write(json);
}
})
登录图片验证码处理
ImageCode
public class ImageCode {
// 图片对象
private BufferedImage image;
// 验证码
private String code
// 省略setXxx
}
ImageCodeUtils
public class ImageCodeUtils {
public static ImageCode createImageCode(HttpServletRequest request) {
ImageCode imageCode = new ImageCode();
int width = 65; // 图片宽度
int height = 25; // 图片高度
/* 创建一个位于缓存中的图片,宽度为65,高度为20 */
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();// 获取用于处理图形上下文的对象,相当于画笔
Random random = new Random();// 创建生成随机数的对象
g.setColor(getRandomColor(100, 200));// 设置图像的背景色
g.fillRect(0, 0, width, height);// 画一个矩形,坐标为(0,0),宽度为width,高度为height
g.setFont(new Font("Times New Roman", Font.PLAIN, 23));// 设定字体格式
for (int i = 0; i < 150; i++) {// 产生150条随机干扰线
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
g.drawLine(x, y, xl, yl);// 在图像的坐标(x,y)和坐标(x+xl, y+yl)之间画干扰线
}
String strCode = "";// 定义一个字符串,用于保存此次生成的验证码
for (int i = 0; i < 4; i++) { // 验证码个数
String strNumber = String.valueOf(random.nextInt(10));// 随机产生一个小于10的数,并转化为string型
strCode += strNumber;
g.setColor(new Color(15 + random.nextInt(120), 15 + random.nextInt(120), 15 + random.nextInt(120)));// 设置字体的颜色
g.drawString(strNumber, 13 * i + 6, 20);// 依次将验证码画到图画上,坐标为(x=13*i+6
// , y=20)
}
g.dispose();// 释放此图像的上下文以及它使用的所有系统资源
imageCode.setCode(strCode);
imageCode.setImage(image);
return imageCode;
}
/* 获取随机颜色方法 */
private static Color getRandomColor(int fc, int bc) {// fc为前景色;bc为背景色
Random random = new Random();
Color randomColor = null;
if (fc > 255)
fc = 255;
if (bc > 255)
bc = 255;
int red = random.nextInt(bc - fc);// 设置0~255之间的随机数
int green = random.nextInt(bc - fc);
int blue = random.nextInt(bc - fc);
randomColor = new Color(red, green, blue);
return randomColor;// 返回具有指定红色、绿色和蓝色值的不透明的sRGB颜色
}
}
ValidataController
@Controller
public class ValidataController {
@RequestMapping(value = "/createCode")
public void createCode(HttpServletRequest request,HttpServletResponse resp) throws IOException {
// 1.创建ImageCode对象
ImageCode imageCode= ImageCodeUtils.createImageCode(request);
// 2.把验证码放入到Session中
request.getSession().setAttribute("SESSION_KEY_IMAGE_CODE",imageCode.getCode());
// 3.把图片写入到resp对象中
ImageIO.write(imageCode.getImage(),"JPEG",resp.getOutputStream());
}
}
ValidataCodeException
自定义一个验证码校验失败后抛出的异常,这个异常是继承AuthenticationException类,这个类是SpringSecurity提供的,是认证失败后抛出的异常。
public class ValidataCodeException extends AuthenticationException {
public ValidataCodeException(String msg, Throwable t) {
super(msg, t);
}
public ValidataCodeException(String msg) {
super(msg);
}
}
ValidateCodeFilter
OncePerRequestFilter,它能够确保在一次请求中只通过一次filter,而需要重复的执行。大家常识上都认为,一次请求本来就只filter一次,为什么还要由此特别限定呢,往往我们的常识和实际的实现并不真的一样,此方法是为了兼容不同的web container,也就是说并不是所有的container都入我们期望的只过滤一次,servlet版本不同,执行过程也不同,因此,为了兼容各种不同运行环境和版本,默认filter继承OncePerRequestFilter是一个比较稳妥的选择。
在这个Filter里面验证用户输入的验证码是否正确,如果验证码没问题就执行下一个Filter,否则就抛出验证码校验失败异常。
public class ValidateCodeFilter extends OncePerRequestFilter {
private AuthenticationFailureHandlerImpl authenticationFailureHandler;
public AuthenticationFailureHandlerImpl getAuthenticationFailureHandler() {
return authenticationFailureHandler;
}
public void setAuthenticationFailureHandler(AuthenticationFailureHandlerImpl authenticationFailureHandler) {
this.authenticationFailureHandler = authenticationFailureHandler;
}
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
// 判断请求地址是/login,请求方式是post才能
System.out.println(httpServletRequest.getRequestURI());
if (StringUtils.equals("/login", httpServletRequest.getRequestURI())
&& StringUtils.equalsIgnoreCase(httpServletRequest.getMethod(), "POST")) {
try {
// 进行验证码的验证,这里有可能会验证失败,所以要捕捉异常
validate(httpServletRequest);
} catch (ValidataCodeException e) { // 自定义一个验证码验证失败异常
authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
return;
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
private void validate(HttpServletRequest req) {
// 用户输入验证码
String reqCode = req.getParameter("imageCode");
// session中的验证码
String sessionCdoe = req.getSession().getAttribute("SESSION_KEY_IMAGE_CODE").toString();
if (StringUtils.isBlank(reqCode)) {
throw new ValidataCodeException("验证码不能为空");
}
if (StringUtils.isBlank(sessionCdoe)) {
throw new ValidataCodeException("验证码不存在");
}
if (!StringUtils.equals(reqCode, sessionCdoe)) {
throw new ValidataCodeException("验证码不匹配");
}
// 验证成功后从session中删除验证码
req.getSession().removeAttribute("SESSION_KEY_IMAGE_CODE");
}
}
AuthenticationFailureHandlerImpl
SpringSecurity认证失败后会有一个触发的组件,这个组件就是AuthenticationFailureHandler,我们实现该类中的方法认证失败后就会自动调用,我们可以在这个方法中响应用户验证码输入错误。
@Component
public class AuthenticationFailureHandlerImpl implements AuthenticationFailureHandler {
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
System.out.println("登录失败");
httpServletResponse.setContentType("application/json;charset=utf8");
// 返回状态码为500
httpServletResponse.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
// e:用户认证失败异常
// 把authentication对象转成JSON字符串
String json = objectMapper.writeValueAsString(e.getMessage());
httpServletResponse.getWriter().write(json);
}
}
SecurityConfig
在SecurityConfig中添加我们自定义的过滤器
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationFailureHandlerImpl authenticationFailureHandler;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
// 实例化自定义Filter
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
// 给Filter中的authenticationFailureHandler赋值
validateCodeFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
httpSecurity
// 把自定义Filter添加到UsernamePasswordAuthenticationFilter的前面
.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin() // 定义登录页
.loginPage("/toLoginPage") // 未认证通过后调整重定向到登录页面,不设置使用默认页面
.loginProcessingUrl("/login") // 处理登录的接口(验证用户名和密码)
.defaultSuccessUrl("/toOkPage") // 登录成功后调整的地址
.failureHandler(authenticationFailureHandler)
// 此处省略
;
}
}
SpringSecurity原理
SpringSecurity 过滤器链
SpringSecurity 采用的是责任链的设计模式,它有很多个过滤器组成一个过滤器链,所以说SpringSecurity本质上就是一个顾虑器链。
- WebAsyncManagerIntegrationFilter: 将 Security 上下文与 Spring Web 中用于处理异步请求映射的 WebAsyncManager 进行集成
- SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上下文信息加载到 SecurityContextHolder 中,然后在该次请求处理完成之后,将 SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将 SecurityContextHolder 中的信息清除,例如在Session中维护一个用户的安全信息就是这个过滤器处理的。
- HeaderWriterFilter: 用于将头信息加入响应中
- CsrfFilter 用于处理跨站请求伪造
- LogoutFilter用于处理退出登录
- UsernamePasswordAuthenticationFilter用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自 /login 的请求。从表单中获取用户名和密码时,默认使用的表单 name 值为 username 和 password,这两个值可以通过设置这个过滤器的usernameParameter 和 passwordParameter 两个参数的值进行修改。
- DefaultLoginPageGeneratingFilter如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面
- BasicAuthenticationFilter检测和处理 http basic 认证
- RequestCacheAwareFilter 用来处理请求的缓存
- SecurityContextHolderAwareRequestFilter主要是包装请求对象request
- AnonymousAuthenticationFilter:检测 SecurityContextHolder 中是否存在 Authentication 对象,如果不存在为其提供一个匿名 Authentication
- SessionManagementFilter:管理 session 的过滤器
- **ExceptionTranslationFilter:**处理认证和授权的异常。
- **FilterSecurityInterceptor:**可以看做是过滤器链的出口,位于最底部,一个方法级别的过滤器。
- RememberMeAuthenticationFilter当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r4irp4JH-1672023223311)(images/1615367184356.png)]
已上图为例,表单登录只是其中的一种过滤方式,httpBasic这种过滤方式是在表单登录之后,除了这两种方式SpringSecurity还支持很多种过滤方式。当请求通过这些绿色的过滤器之后,请求会进入到FilterSecurityInterceptor适配器上,这个是整个SpringSecurity过滤器的最后一环,是最终的守门人,它会去决定请求最终能否去访问到我们的Rest服务,那它依据什么区判断的呢?就是根据我们在SpringSecurityConfig中的配置(什么样的资源应该有什么样的权限访问),这些规则都会被放到
FilterSecurityInterceptor中,当认证成功则可以访问资源,如果认证失败,就会根据失败原因抛出相应的异常,进入到ExceptionTranslationFilter中,这个过滤器根据这些异常会做出相应的处理。
护一个用户的安全信息就是这个过滤器处理的。
- HeaderWriterFilter: 用于将头信息加入响应中
- CsrfFilter 用于处理跨站请求伪造
- LogoutFilter用于处理退出登录
- UsernamePasswordAuthenticationFilter用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自 /login 的请求。从表单中获取用户名和密码时,默认使用的表单 name 值为 username 和 password,这两个值可以通过设置这个过滤器的usernameParameter 和 passwordParameter 两个参数的值进行修改。
- DefaultLoginPageGeneratingFilter如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面
- BasicAuthenticationFilter检测和处理 http basic 认证
- RequestCacheAwareFilter 用来处理请求的缓存
- SecurityContextHolderAwareRequestFilter主要是包装请求对象request
- AnonymousAuthenticationFilter:检测 SecurityContextHolder 中是否存在 Authentication 对象,如果不存在为其提供一个匿名 Authentication
- SessionManagementFilter:管理 session 的过滤器
- **ExceptionTranslationFilter:**处理认证和授权的异常。
- **FilterSecurityInterceptor:**可以看做是过滤器链的出口,位于最底部,一个方法级别的过滤器。
- RememberMeAuthenticationFilter当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启
[外链图片转存中…(img-r4irp4JH-1672023223311)]
已上图为例,表单登录只是其中的一种过滤方式,httpBasic这种过滤方式是在表单登录之后,除了这两种方式SpringSecurity还支持很多种过滤方式。当请求通过这些绿色的过滤器之后,请求会进入到FilterSecurityInterceptor适配器上,这个是整个SpringSecurity过滤器的最后一环,是最终的守门人,它会去决定请求最终能否去访问到我们的Rest服务,那它依据什么区判断的呢?就是根据我们在SpringSecurityConfig中的配置(什么样的资源应该有什么样的权限访问),这些规则都会被放到
FilterSecurityInterceptor中,当认证成功则可以访问资源,如果认证失败,就会根据失败原因抛出相应的异常,进入到ExceptionTranslationFilter中,这个过滤器根据这些异常会做出相应的处理。 在这一系列过滤器链中,绿色的过滤器可以通过配置去完成自定义配置,蓝色及黄色是不能控制的。它们一定都会生效且位置固定。(ExceptionTranslationFilter一定在FilterSecurityInterceptor之前)