本文章记录了SpringSecurity各个知识点、以供我们搭建Springboot + SpringSecurity框架打下基础
目录
1.SpringSecurity本质是一个过滤器链 (有很多过滤器组成 ,每个过滤器实现相应的功能)
2.过滤器加载过程 (使用Springboot让我们省略配置了DelegatingFilterProxy过滤器,下面是其加载过程;不使用springboot时,需要手动配置过滤器)
前言
权限框架使我们最常见的,一个管理系统都会先进行登录和相应授权之后才可以进行对系统进行操作,所以我们只会CRUD是远远不够的,我觉得自己CURD写的很溜,但有时总觉得自己什么都不会,所以我们不应该局限于CRUD,应该扩展自己的知识面。
一、SpringSecurity基本介绍
Spring Security概念 : 是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
Spring Security功能 : 对Web安全性的支持大量地依赖于Servlet过滤器。这些过滤器拦截进入请求,并且在应用程序处理该请求之前进行某些安全处理。 Spring Security提供有若干个过滤器,它们能够拦截Servlet请求,并将这些请求转给认证和访问决策管理器处理,从而增强安全性。根据自己的需要,可以使用适当的过滤器来保护自己的应用程序。
二、SpringSecurity基本原理
1.SpringSecurity本质是一个过滤器链 (有很多过滤器组成 ,每个过滤器实现相应的功能)
**过滤器源码分析: 1.FilterSecurityInterceptor: 是一个方法级的权限过滤器,基于位于过滤链的最底部! 2.ExceptionTranslationFilter: 是个异常过滤器,用来处理在认证授权过程中抛出的异常! 3.UsernamePasswordAuthenticationFilter: 对/login的POST请求做拦截,校验表单中对用户名,密码! 4.SecurityContextPersistenceFilter 5.HeaderWriterFilter 6.CsrfFilter 7.LogoutFilter 8.DefaultLoginPageGeneratingFilter 9.DefaultLogoutPageGeneratingFilter 10.RequestCacheAwareFilter 11.SecurityContextHolderAwareRequestFilter 12.AnonymousAuthenticationFilter 13.SessionManagementFilter
2.过滤器加载过程 (使用Springboot让我们省略配置了DelegatingFilterProxy过滤器,下面是其加载过程;不使用springboot时,需要手动配置过滤器)
note:过滤器加载过程: 1.首先进入DelegatingFilterProxy过滤器中 然后进入doFilter 的initDelegate()初始化方法 2.initDelegate() 通过getBean 获取 FilterChainProxy 过滤器 3.进入到 FilterChainProxy 过滤器中 doFilter方法 的 doFilterInternal()方法 4.doFilterInternal()方法 中获取到所有的过滤器 加载到 过滤器链中 5.在用户进行操作时 每次访问该项目 都会经过该过滤器链 进而实现各种功能把控
3.两个重要接口
1.UserDetailService(note : springsecurity默认是框架自己的账号密码,实际开发中,我们需要重写接口来完成我们自己的逻辑;查询数据库用户名和密码过程;下面是重写接口过程中的步骤) a、创建类继承UsernamePasswordAuthenticationFilter 重写三个方法 b、创建类实现UserDetailService,编写查询数据过程,返回User对象,这个User对象是安全框架提供对象
2.PasswordEncoder (note : 数据加密接口,用于返回User对象里面密码加密)
三 、SpringSecurity-web 权限方案
1.用户认证 (设置登录的用户名和密码)
WARN1: 自定义登录逻辑
note : 说明 刚搭建 springsecurity时,该框架有一个默认的登录账号密码,我们如何自己定制账号密码呢 ,下面有三种方式,在项目开发中用的第三种 a、通过配置文件
**通过配置文件修改 springSecurity 默认账号密码** spring: security: user: name: developer password: admin123
b、通过配置类 ** * @author gc * @date 2022/11/25 15:30 * note : 通过配置类修改 springSecurity 默认账号密码 */ @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { String password = passwordEncoder().encode("developer"); auth.inMemoryAuthentication().withUser("jze").password(password).authorities("admin"); } @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
c、自定义编写实现类 (真实项目开发中使用 虚拟数据版本)加载顺序:配置文件 > 配置类 > userDetailService实现类 1.创建配置类 设置使用哪个userDetailService实现类 2.编写实现类 返回User对象 User对象有用户名密码和操作权限 /** * @author gc * @date 2022/11/25 15:30 * note : SpringSecurity配置类 自定义编写实现类 */ @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired @Qualifier("userDetailsService") private UserDetailsService userDetailsService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } } /** * @author guoce * @date 2022/11/25 15:44 * note : 自定义登录实现类,用户登录会走该登录逻辑,该username即是用户提交表单的用户名 */ @Service("userDetailsService") public class MyUserDetailServiceImpl implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role"); return new User(s,passwordEncoder.encode("123"),auths); } }
d、自定义编写实现类 (真实项目开发中使用:查询数据库版本)加载顺序:配置文件 > 配置类 > userDetailService实现类
WARN2: 自定义登录页面
// security 自定义登录页面,访问权限 @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/login.html") //登录页面设置 .loginProcessingUrl("/login") //登陆访问路径 .defaultSuccessUrl("/admin/page").permitAll() //登陆成功之后,跳转路径 .and() .authorizeRequests() .antMatchers("login.html").permitAll() //不需要认证就可以访问的路径 .anyRequest().authenticated(); //所有请求都需要被认证,必须登录后被访问 http.csrf().disable(); //关闭csrf防护 }
2.用户授权
WARN1: 基于权限访问控制
hasAuthority方法:
1.security 自定义登录页面,访问权限 @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/login.html") //登录页面设置 .loginProcessingUrl("/login") //登陆访问路径 .defaultSuccessUrl("/admin/page").permitAll() //登陆成功之后,跳转路径 .and() .authorizeRequests() .antMatchers("login.html").permitAll() //不需要认证就可以访问的路径 ==> .antMatchers("/admin/page").hasAuthority("admin") //指定有admin权限才可以访问该资源 //.antMatchers("/admin/hello").hasAnyAuthority("admin,manager") //指定 有admin或者manager权限的均可以访问该资源 .anyRequest().authenticated(); //所有请求都需要被认证,必须登录后被访问 http.csrf().disable(); //关闭csrf防护 } 2. UserDetailsService 自定义登录逻辑类 为登录用户指定权限 List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
hasAnyAuthority方法:
@Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/login.html") //登录页面设置 .loginProcessingUrl("/login") //登陆访问路径 .defaultSuccessUrl("/admin/page").permitAll() //登陆成功之后,跳转路径 .and() .authorizeRequests() .antMatchers("login.html").permitAll() //不需要认证就可以访问的路径 //.antMatchers("/admin/page").hasAuthority("admin") //指定有admin权限才可以访问该资源 ==> .antMatchers("/admin/hello").hasAnyAuthority("admin,manager") //指定 有admin或者manager权限的均可以访问该资源 .anyRequest().authenticated(); //所有请求都需要被认证,必须登录后被访问 http.csrf().disable(); //关闭csrf防护 } 2.UserDetailsService 自定义登录逻辑类 为登录用户指定权限 List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
WARN2: 基于角色访问控制
hasRole方法:
Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/login.html") //登录页面设置 .loginProcessingUrl("/login") //登陆访问路径 .defaultSuccessUrl("/admin/page").permitAll() //登陆成功之后,跳转路径 .and() .authorizeRequests() .antMatchers("login.html").permitAll() //不需要认证就可以访问的路径 //.antMatchers("/admin/page").hasAuthority("admin") //指定有admin权限才可以访问该资源 //.antMatchers("/admin/hello").hasAnyAuthority("admin,manager") //指定 有admin或者manager权限的均可以访问该资源 ==> .antMatchers("/admin/page").hasRole("sale") //指定有sale权限才可以访问该资源 //.antMatchers("/admin/page").hasAnyRole("sale,consumer") //指定有sale或者consumer权限才可以访问该资源 .anyRequest().authenticated(); //所有请求都需要被认证,必须登录后被访问 http.csrf().disable(); //关闭csrf防护 } 2.自定义登录逻辑类 为登录用户指定角色 List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_sale");
hasAnyRole方法:
1.security 自定义登录页面,访问权限 @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/login.html") //登录页面设置 .loginProcessingUrl("/login") //登陆访问路径 .defaultSuccessUrl("/admin/page").permitAll() //登陆成功之后,跳转路径 .and() .authorizeRequests() .antMatchers("login.html").permitAll() //不需要认证就可以访问的路径 //.antMatchers("/admin/page").hasAuthority("admin") //指定有admin权限才可以访问该资源 //.antMatchers("/admin/hello").hasAnyAuthority("admin,manager") //指定 有admin或者manager权限的均可以访问该资源 //.antMatchers("/admin/page").hasRole("sale") //指定有sale权限才可以访问该资源 ==> .antMatchers("/admin/page").hasAnyRole("sale,consumer") //指定有sale或者consumer权限才可以访问该资源 .anyRequest().authenticated(); //所有请求都需要被认证,必须登录后被访问 http.csrf().disable(); //关闭csrf防护 } 2.自定义登录逻辑类 为登录用户指定角色 List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_consumer");
WARN3: 自定义403页面 (无权限访问)
1.security 自定义登录页面,访问权限 @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/login.html") //登录页面设置 .loginProcessingUrl("/login") //登陆访问路径 .defaultSuccessUrl("/admin/page").permitAll() //登陆成功之后,跳转路径 .and() .authorizeRequests() .antMatchers("login.html").permitAll() //不需要认证就可以访问的路径 //.antMatchers("/admin/page").hasAuthority("admin") //指定有admin权限才可以访问该资源 //.antMatchers("/admin/hello").hasAnyAuthority("admin,manager") //指定 有admin或者manager权限的均可以访问该资源 .antMatchers("/admin/page").hasRole("sale") //指定有sale权限才可以访问该资源 //.antMatchers("/admin/page").hasAnyRole("sale,consumer") //指定有sale或者consumer权限才可以访问该资源 .anyRequest().authenticated(); //所有请求都需要被认证,必须登录后被访问 //配置没有访问权限的跳转的自定义页面 ==> http.exceptionHandling().accessDeniedPage("/unAuth.html"); //关闭csrf防护 http.csrf().disable(); } 2.自定义登录逻辑类 为登录用户指定角色 List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admin");
WARN4: 注解使用
@Secured("ROLE_manager") note : 用户具有某个角色 可以访问这个方法
**使用步骤**: 1.主启动类上增加启动注解 @EnableGlobalMethodSecurity(securedEnabled = true) 2.@Secured作用的接口 @Secured("ROLE_sale") @GetMapping("update") public String update(){ return "hello update!"; } 3.用户返回时,给用户定义的角色 List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admin,ROLE_sale");
b、PreAuthorize ("hasAnyAuthority('admin')")note : 注解适合进入方法前的权限认证 用户具有这个权限 就可以访问这个方法
**使用步骤**: 1.主启动类上增加启动注解 @EnableGlobalMethodSecurity(prePostEnabled = true) 2.@PreAuthorize作用的接口: @PreAuthorize("hasAnyAuthority('admin')") @GetMapping("update") public String update(){ return "hello update!"; } 3.用户返回时,给用户定义的角色 List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_sale");
c、PostAuthorize("hasAnyAuthority('admins')") note : 在接口返回数据前进行处理
**使用步骤**: 1.主启动类上增加启动注解 @EnableGlobalMethodSecurity(prePostEnabled = true) 2.PostAuthorize作用的接口: @PostAuthorize("hasAnyAuthority('admins')") @GetMapping("update") public String update(){ System.out.println("update==================="); return "hello update!"; } 3.用户返回时,给用户定义的角色 List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_sale");
d、PostFilter note : 权限校验之后对返回数据进行过滤
e、PreFilter note : 传入方法数据进行过滤
WARN5 : 用户退出登录
1.security 自定义登录页面,访问权限 @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/login.html") //登录页面设置 .loginProcessingUrl("/login") //登陆访问路径 .defaultSuccessUrl("/success.html").permitAll() //登陆成功之后,跳转路径 .and() .authorizeRequests() .antMatchers("login.html").permitAll() //不需要认证就可以访问的路径 //.antMatchers("/admin/page").hasAuthority("admin") //指定有admin权限才可以访问该资源 //.antMatchers("/admin/hello").hasAnyAuthority("admin,manager") //指定 有admin或者manager权限的均可以访问该资源 .antMatchers("/admin/page").hasRole("sale") //指定有sale权限才可以访问该资源 //.antMatchers("/admin/page").hasAnyRole("sale,consumer") //指定有sale或者consumer权限才可以访问该资源 .anyRequest().authenticated(); //所有请求都需要被认证,必须登录后被访问 //配置没有访问权限的跳转的自定义页面 http.exceptionHandling().accessDeniedPage("/unAuth.html"); //退出登录 点击退出登录触发logout方法 退出成功后跳转到登陆页面 ==> http.logout().logoutUrl("/logout").logoutSuccessUrl("/login.html").permitAll(); //关闭csrf防护 http.csrf().disable(); } 2.登陆页面 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> 登陆成功! <a href="/logout">退出登录</a> </body> </html>
WARN6 : 自动登录 (记住我)
a、实现原理
b、具体实现
1.创建表 CREATE TABLE user.Untitled ( username varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, series varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, token varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, last_used timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0), PRIMARY KEY (series) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; 2.将token写入数据库与浏览器对象创建 @Autowired private DataSource dataSource; @Bean public PersistentTokenRepository persistentTokenRepository(){ JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); jdbcTokenRepository.setDataSource(dataSource); //将token写入数据库 //jdbcTokenRepository.setCreateTableOnStartup(true); 执行就会创建表 return jdbcTokenRepository; } 3.security配置 @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/login.html") //登录页面设置 .loginProcessingUrl("/login") //登陆访问路径 .defaultSuccessUrl("/success.html").permitAll() //登陆成功之后,跳转路径 .and() .authorizeRequests() .antMatchers("login.html").permitAll() //不需要认证就可以访问的路径 //.antMatchers("/admin/page").hasAuthority("admin") //指定有admin权限才可以访问该资源 //.antMatchers("/admin/hello").hasAnyAuthority("admin,manager") //指定 有admin或者manager权限的均可以访问该资源 .antMatchers("/admin/page").hasRole("sale") //指定有sale权限才可以访问该资源 //.antMatchers("/admin/page").hasAnyRole("sale,consumer") //指定有sale或者consumer权限才可以访问该资源 .anyRequest().authenticated() //所有请求都需要被认证,必须登录后被访问 .and() ==> .rememberMe().tokenRepository(persistentTokenRepository()) .tokenValiditySeconds(60) //认证有效时长 单位秒 .userDetailsService(userDetailsService); // 自动登录功能 //配置没有访问权限的跳转的自定义页面 http.exceptionHandling().accessDeniedPage("/unAuth.html"); //退出登录 点击退出登录触发logout方法 退出成功后跳转到登陆页面 http.logout().logoutUrl("/logout").logoutSuccessUrl("/login.html").permitAll(); //关闭csrf防护 http.csrf().disable(); } 4.
WARN7 : csrf 功能
a、概念
跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。 跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。
b、举例
假如一家银行用以运行转账操作的URL地址如下:http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName 那么,一个恶意攻击者可以在另一个网站上放置如下代码: <img src="http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman"> 如果有账户名为Alice的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失1000资金。 这种恶意的网址可以有很多种形式,藏身于网页中的许多地方。此外,攻击者也不需要控制放置恶意网址的网站。例如他可以将这种地址藏在论坛,博客等任何用户生成内容的网站中。这意味着如果服务端没有合适的防御措施的话,用户即使访问熟悉的可信网站也有受攻击的危险。 透过例子能够看出,攻击者并不能通过CSRF攻击来直接获取用户的账户控制权,也不能直接窃取用户的任何信息。他们能做到的,是欺骗用户浏览器,让其以用户的名义运行操作。
c、常用解决方案
添加校验token由于CSRF的本质在于攻击者欺骗用户去访问自己设置的地址,所以如果要求在访问敏感数据请求时,要求用户浏览器提供不保存在cookie中,并且攻击者无法伪造的数据作为校验,那么攻击者就无法再运行CSRF攻击。这种数据通常是窗体中的一个数据项。服务器将其生成并附加在窗体中,其内容是一个伪随机数。当客户端通过窗体提交请求时,这个伪随机数也一并提交上去以供校验。正常的访问时,客户端浏览器能够正确得到并传回这个伪随机数,而通过CSRF传来的欺骗性攻击中,攻击者无从事先得知这个伪随机数的值,服务端就会因为校验token的值为空或者错误,拒绝这个可疑请求。
d、总结
日常项目开发中 我们经常使用 JWT(token令牌)的方式, 而不需要开启csrf
e、JWT概念
上面提到的Token就是JWT(JSON Web Token),是一种用于通信双方之间传递安全信息的简洁的、URL安全的表述性声明规范。一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。
格式 : base64UrlEncode(JWT 头)+"."+base64UrlEncode(载荷)+"."+HMACSHA256(base64UrlEncode(JWT 头) + "." + base64UrlEncode(有效载荷),密钥) eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ