当前位置: 首页 > news >正文

SpringSecurity6

一、Spring Security概述

1、Spring Security简介

Spring Security 是一个能够为基于 Spring 的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在 Spring 应用上下文中配置的 Bean,充分利用了 Spring IoC(Inversion of Control 控制反转),DI(Dependency Injection 依赖注入)和 AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

Spring Security 拥有以下特性:

  • 对身份验证和授权的全面且可扩展的支持
  • 防御会话固定、点击劫持,跨站请求伪造等攻击
  • 支持 Servlet API 集成
  • 支持与 Spring Web MVC 集成

2、Spring Security入门案例(认证)

Spring Security是Spring的一个子项目,天生支持Spring,不需要做额外的整合

  • 创建一个 Spring Boot 应用,并引入依赖

      <properties><java.version>17</java.version><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><spring-boot.version>3.0.2</spring-boot.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- springboot整合security依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies>
    
  • 创建springBoot启动类

      @SpringBootApplicationpublic class ApplicatoinApp {public static void main(String[] args) {SpringApplication.run(ApplicatoinApp.class, args);}}
    
  • 创建一个控制器

    @RestController
    public class HelloController {@RequestMapping("/hello")public String hello(){return "Hello Spring security";}
    }
    

然后直接启动项目,访问 :http://localhost:8080/login

结果打开的是一个登录页面,其实这时候我们的请求已经被保护起来了,要想访问,需要先登录。

在这个案例中仅仅是引入了一个 Spring Security 的 starter 启动器,没有做任何的配置,而项目已经具有了权限认证。

Spring Security 默认提供了一个用户名为 user 的用户,其密码在控制台可以找到:
成功登录以后就可以正常访问了:

如果想要想修改配置,则应使用 spring.security.user.namespring.security.user.password

spring:application:name: security-demo #项目名称security:user:name: tompassword: 123

此时启动项目,将只能通过自己配置的用户名和密码登录。

可以通过配置类的方式进行配置,创建Security配置类

/*** Spring Security配置类*/
@Configuration
@EnableWebSecurity
public class SecurityConfig{/*** 设置用户正确的信息,这个数据一般是从数据库中查询* @return*/@Beanpublic UserDetailsService userDetailsService(){InMemoryUserDetailsManager inMemoryUserDetailsManager=new InMemoryUserDetailsManager();//创建两个用户信息,后面是从数据库中得到的UserDetails u1= User.withUsername("tom").password("123").roles("admin").build();UserDetails u2= User.withUsername("jack").password("123").roles("admin").build();inMemoryUserDetailsManager.createUser(u1);inMemoryUserDetailsManager.createUser(u2);return inMemoryUserDetailsManager;}/*** 认证判断* @return*/@Beanpublic AuthenticationProvider authenticationProvider(){DaoAuthenticationProvider authProvider=new DaoAuthenticationProvider();authProvider.setUserDetailsService(userDetailsService()); //获得UserDetailsService回传的数据return authProvider;}}

在登录时会提示:

No AuthenticationProvider found for org.springframework.security.authentication.UsernamePasswordAuthenticationToken

从 5.x 开始,强制性要求必须使用密码加密器(PasswordEncoder)对原始密码(注册密码)进行加密。因此,如果忘记指定 PasswordEncoder 会导致执行时会出现 异常。

这是因为我们在对密码加密的时候使用到了 BCryptPasswordEncoder 对象,而 Spring Security 在对密码比对的过程中不会『自己创建』加密器,因此,需要我们在 Spring IoC 容器中配置、创建好加密器的单例对象,以供它直接使用。

所以,我们还需要在容器中配置、创建加密器的单例对象(上面那个 new 理论上可以改造成注入),修改Spring securitry配置类

/*** Spring Security配置类*/
@Configuration
@EnableWebSecurity
public class SecurityConfig{/*** 将PasswordEncoder注入到ioc容器** @return*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** 设置用户正确的信息* @return*/@Beanpublic UserDetailsService userDetailsService(){InMemoryUserDetailsManager inMemoryUserDetailsManager=new InMemoryUserDetailsManager();//创建两个用户信息,后面是从数据库中得到的UserDetails u1= User.withUsername("tom").password("$2a$10$dhj0K3tu5e9wi/cVMwBI3O7jv1AveFSZQNcjn51vjesxhQAu.T8sm").roles("admin").build();inMemoryUserDetailsManager.createUser(u1);return inMemoryUserDetailsManager;}/*** 认证规则判断* @return*/@Beanpublic AuthenticationProvider authenticationProvider(){DaoAuthenticationProvider authProvider=new DaoAuthenticationProvider();authProvider.setUserDetailsService(userDetailsService()); //调用自定义登录类,获得用户数据authProvider.setPasswordEncoder(passwordEncoder()); //设置密码加密器return authProvider;}
}

再次重新启动一切正常。

3、 Password Encoder密码管理器

之前有提及,Spring Security 升级到 5 版本后提高了安全要求:Spring Security 要求所有的密码的存储都『必须』是加密形式的。为此,我们必须要确保 Spring IoC 容器中有一个了 PasswordEncoder 的单例对象用以供 Spring Security 使用。

PasswordEncoder 在两处场景会被使用到:

  • 当我们实现注册功能时,要将用户在前端页面输入的明文密码使用 PasswordEncoder 进行加密后存储、持久化。
  • 当用户在登录时,Spring Security 会将用户在前端页面输入的明文密码使用 PasswordEncoder 进行加密之后,再和由我们提供的密码『标准答案』进行比对。

Spring Security 使用 PasswordEncoder 对你提供的密码进行加密。该接口中有两个方法:加密方法,是否匹配方法。

  • 加密方法(encode)方法在用户注册时使用。在注册功能处,我们(程序员)需要将用户提供的密码加密后存储(至数据库)。
  • 匹配方法 matches 方法是由 Spring Security 调用的。在登录功能处,Spring Security 要用它来比较登录密码和密码『标准答案』。

Spring Security 内置的 Password Encoder 有:

加密算法名称PasswordEncoder
NOOPNoOpPasswordEncoder.getInstance()
SHA256new StandardPasswordEncoder()
BCRYPT(官方推荐)new BCryptPasswordEncoder()
LDAPnew LdapShaPasswordEncoder()
PBKDF2new Pbkdf2PasswordEncoder()
SCRYPTnew SCryptPasswordEncoder()
MD4new Md4PasswordEncoder()
MD5new MessageDigestPasswordEncoder(“MD5”)
SHA_1new MessageDigestPasswordEncoder(“SHA-1”)
SHA_256new MessageDigestPasswordEncoder(“SHA-256”)

上述 Password Encoder 中有一个『无意义』的加密器:NoOpPasswordEncoder 。它对原始密码没有做任何处理(现在也被标记为废弃)。

记得使用 @SuppressWarnings(“deprecation”) 去掉 IDE 的警告信息。

4、“消失”的登录功能

不知道大家有没有注意到,其实,我们的 Controller 中还没有写登录功能的相关代码。但是,之前的示例中,就已经有了完整的『登录』(甚至『退出』)功能,并且,Spring Security 似乎还能记住我们已经登陆过(当我们第二次访问页面时,它不会要求我们再次登录)!

二、根据数据库实现认证

security中的登录请求不经过自己写的controller,在security中已经内置了登录的请求地址:login,并且该登录请求只支持post

要实现认证需要使用security内置的UserDetailsService接口

1、根据输入的帐号获得用户的数据

在security中登录请求的参数名只能叫username和password

a、根据帐号获得用户信息
public interface UsersMapper extends BaseMapper<UsersPo> {/*** 根据帐号查询用户信息* @param account* @return*/default UsersPo getUsersByAccount(String account){QueryWrapper<UsersPo> queryWrapper=new QueryWrapper<UsersPo>();queryWrapper.eq("account", account);return this.selectOne(queryWrapper);}
}
b、根据帐号获得用户的权限
public interface PermsMapper extends BaseMapper<PermsPo> {/*** 根据用户id查询用户所有的权限* @param userid* @return*/@Select("select p.code from rbac_perms p " +"left join rbac_user_perms up on p.id=up.permsid " +"where up.userid=#{userid}")List<String> listPerms(Integer userid);}
c、创建security认证service
/*** security认证service*/
@Service
@Transactional
@Slf4j
public class LoginService implements UserDetailsService {@Resourceprivate UsersMapper usersMapper;@Resourceprivate PermsMapper permsMapper;/*** 只能在这个方法中编写登录查询逻辑* 该方法只做正确的数据提供,不做任何的判断* @param username  就是前端传给security中的帐号* @return* @throws UsernameNotFoundException*/@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//只需要根据用户输入的帐号到数据库中查出用户的信息,//security中PasswordEncoder的matches会自动将用户输入的内容与查出来的内容进行比对UsersPo usersPo=usersMapper.getUsersByAccount(username);//保存权限String authoritys=null;if(usersPo!=null){//获得所有的权限List<String> permsItem=permsMapper.listPerms(usersPo.getId());//将集合转为字符串authoritys=StringUtils.join(permsItem, ",");log.info("你的权限:"+authoritys);}//告诉security当前用户正确的帐号,正确的密码,拥有权限String usrStr=usersPo.getId()+","+usersPo.getAccount();return new User(usrStr, usersPo.getPassword(),AuthorityUtils.commaSeparatedStringToAuthorityList(authoritys));}
}

ProviderManager/AuthenticationProvider 在做密码密码的比对工作时,会调用 UserDetailsService 的 .loadUserByUsername() 方法,并传入『用户名』,用以查询该用户的密码和权限信息。

UserDetails 中封装了用户登录过程中所需的全部信息:

方法说明
isAccountNonExpired暂时用不到,返回 true ,帐号是否过期
isAccountNonLocked暂时用不到,返回 true ,帐号是否锁定
isCredentialsNonExpired暂时用不到,返回 true ,认证是否过期
isEnabled配合数据库层面的逻辑删除功能,用来表示当前用户是否还存在、是否可用。
getPassword getUsername需要返回的内容显而易见。
getAuthorities用于返回用户的权限信息。这里的权限就这是指用户的角色。它的返回值类型是 Collection<? extends GrantedAuthority>,具体形式通常是:List,里面用来存储角色信息(或权限信息)
 return new User("tom", password,AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN"));

SimpleGrantedAuthority 是 GrantedAuthority 的一个实现类,也是最常见最常用的和实现类。如果直接使用的话那就是 new SimpleGrantedAuthority(“ROLE_USER”) 。

d、创建security配置类
/*** Spring Security配置类*/
@Configuration
@EnableWebSecurity
public class SecurityConfig{@Resourceprivate LoginService loginService;/*** 将PasswordEncoder注入到ioc容器** @return*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** 设置用户正确的信息* @return*/@Beanpublic UserDetailsService userDetailsService(){//调用LoginService中的登录查询方法return username -> loginService.loadUserByUsername(username);}/*** 认证规则判断* @return*/@Beanpublic AuthenticationProvider authenticationProvider(){DaoAuthenticationProvider authProvider=new DaoAuthenticationProvider();authProvider.setUserDetailsService(userDetailsService()); //调用自定义登录类,获得用户数据authProvider.setPasswordEncoder(passwordEncoder()); //设置密码加密器return authProvider;}
}

由于 @EnableWebSecurity 注解的功能涵盖了 @Configuration 注解,因此这个配置类上不用再标注 @Configuration 注解。另外,@EnableWebSecurity 注解还有一个 debug 参数用于指定是否采用调试模式,默认为 false 。在调试模式下,每个请求的详细信息和所经过的过滤器都会被打印至控制台。

2、Spring Security 和 RBAC【了解】

  • AuthorityUtils.commaSeparatedStringToAuthorityList(authoritys)权限写法

    虽然在 RBAC 模型中,用户的 “权限” 是 “角色” 的下一级,但是在 Spring Security 中,它是将角色和权限一视同仁的,即,Spring Security 不强求你的角色和权限有上下级的关系。

    在 Spring Security 中角色和权限都属于 Authority 。不过,Spring Security 有个『人为约定』:

    • 如果你的 Authority 指的是角色,那么角色(的标准答案)就需要以 ROLE_ 开头;
    • 如果你的 Authority 指的是权限,那么权限(的标准答案)则不需要特定的开头。

    在后续很多涉及『角色』的地方,Spring Security 都会对 ROLE_ 做额外处理。

3、security认证流程细节【重点,要求每个人能说出来】

  • AuthenticationManager

    它是 “表面上” 的做认证和鉴权比对工作的那个人,它是认证和鉴权比对工作的起点。

    ProvierderManager 是 AuthenticationManager 接口的具体实现。

  • AuthenticationProvider

    它是 “实际上” 的做认证和鉴权比对工作的那个人。从命名上很容易看出,Provider 受 ProviderManager 的管理,ProviderManager 调用 Provider 进行认证和鉴权的比对工作。

    我们最常用到 DaoAuthenticationProvider 是 AuthenticationProvider 接口的具体实现。

  • UserDetailsService

    虽然 AuthenticationProvider 负责进行用户名和密码的比对工作,但是它并不清楚用户名和密码的『标准答案』,而标准答案则是由 UserDetailsService 来提供。简单来说,UserDetailsService 负责提供标准答案 ,以供 AuthenticationProvider 使用。

  • UserDetails

    UserDetails 它是存放用户认证信息和权限信息的标准答案的 “容器” ,它也是 UserDetailService “应该” 返回的内容。

  • PasswordEncoder

    Spring Security 要求密码不能是明文,必须经过加密器加密。这样,AuthenticationProvider 在做比对时,就必须知道『当初』密码时使用哪种加密器加密的。所以,AuthenticationProvider 除了要向 UserDetailsService 『要』用户名密码的标准答案之外,它还需要知道配套的加密算法(加密器)是什么。

4、security内置过滤器

spring security 在 org.springframework.security.config.annotation.web.builders.FilterComparator 中提供的规则进行比较按照比较结果进行排序注册。

FilterComparator() {Step order = new Step(INITIAL_ORDER, ORDER_STEP);put(ChannelProcessingFilter.class, order.next());put(ConcurrentSessionFilter.class, order.next());put(WebAsyncManagerIntegrationFilter.class, order.next());put(SecurityContextPersistenceFilter.class, order.next());...
}

spring security 中的默认的过滤器(如果被激活、启用的话),它在过滤器链上的位置和顺序就一定如上述规则所述。序号越小优先级越高。

接下来我们就对这些内置过滤器中常见的过滤器进行一个系统的认识。我们将按照默认顺序进行讲解

4.1 SecurityContextPersistenceFilter

SecurityContextPersistenceFilter 主要控制 SecurityContext 的在一次请求中的生命周期 。

  • 请求来临时,创建 SecurityContext 安全上下文信息;
  • 请求结束时清空 SecurityContextHolder 。

SecurityContextPersistenceFilter 通过 HttpScurity#securityContext() 及相关方法引入其配置对象 SecurityContextConfigurer 来进行配置。

4.2 CsrfFilter

CsrfFilter 用于防止 csrf 攻击,前后端使用 json 交互需要注意的一个问题。

你可以通过 HttpSecurity.csrf() 来开启或者关闭它。在你使用 jwt 等 token 技术时,是不需要这个的。

4.3 LogoutFilter

很明显它是处理注销的过滤器。

你可以通过 HttpSecurity.logout() 来定制注销逻辑,非常有用。

4.4 UsernamePasswordAuthenticationFilter

处理用户以及密码认证的核心过滤器。认证请求提交的 usernamepassword ,被封装成 token 进行一系列的认证,便是主要通过这个过滤器完成的,在表单认证的方法中,这是最最关键的过滤器。

你可以通过 HttpSecurity#formLogin() 及相关方法引入其配置对象 FormLoginConfigurer 来进行配置。

4.5 DefaultLoginPageGeneratingFilter

生成默认的登录页。默认情况下,你访问 /login 所看到的内容就是它生成的 。

4.6 DefaultLogoutPageGeneratingFilter

生成默认的退出页。 默认情况下,你访问 /logout 所看到的内容就是它生成的 。

4.7 BasicAuthenticationFilter

Basic 身份验证是 Web 应用程序中流行的可选的身份验证机制。

BasicAuthenticationFilter 负责处理 HTTP 头中显示的基本身份验证凭据。这个 Spring Security 的 Spring Boot 自动配置默认是启用的 。

BasicAuthenticationFilter 通过 HttpSecurity#httpBasic() 及相关方法引入其配置对象 HttpBasicConfigurer 来进行配置。

4.8 RequestCacheAwareFilter

用于用户认证成功后,重新恢复因为登录被打断的请求。当匿名访问一个需要授权的资源时。会跳转到认证处理逻辑,此时请求被缓存。在认证逻辑处理完毕后,从缓存中获取最开始的资源请求进行再次请求。

RequestCacheAwareFilter 通过 HttpScurity#requestCache() 及相关方法引入其配置对象 RequestCacheConfigurer 来进行配置。

4.9 RememberMeAuthenticationFilter

处理『记住我』功能的过滤器。

RememberMeAuthenticationFilter 通过 HttpSecurity.rememberMe() 及相关方法引入其配置对象 RememberMeConfigurer 来进行配置。

4.10 AnonymousAuthenticationFilter

匿名认证过滤器。对于 Spring Security 来说,所有对资源的访问都是有 Authentication 的。对于无需登录(UsernamePasswordAuthenticationFilter)直接可以访问的资源,会授予其匿名用户身份。

AnonymousAuthenticationFilter 通过 HttpSecurity.anonymous() 及相关方法引入其配置对象 AnonymousConfigurer 来进行配置。

4.11 SessionManagementFilter

Session 管理器过滤器,内部维护了一个 SessionAuthenticationStrategy 用于管理 Session 。

SessionManagementFilter 通过 HttpScurity#sessionManagement() 及相关方法引入其配置对象 SessionManagementConfigurer 来进行配置。

4.12 ExceptionTranslationFilter

我们常说的倒数第二个(实际上在注册表中是倒数第三个)过滤器。用来处理下一个过滤器(FilterSecurityInterceptor)所抛出的异常。

通常,它主要处理 2 种异常:

  • 如果下一个过滤器(FilterSecurityInterceptor)抛出的是 AuthenticationException 异常,ExceptionTranslationFilter 会触发 AuthenticationEntryPoint 的执行;
  • 如果下一个过滤器(FilterSecurityInterceptor)抛出的是 AccessDeniedException 异常,ExceptionTranslationFilter 会触发 AccessDeniedHandler 的执行;
4.13 FilterSecurityInterceptor

我们常说的最后一个(实际上在注册表中是倒数第二个)过滤器。

这个过滤器决定了访问特定路径应该具备的权限,访问的用户的角色,权限是什么?访问的路径需要什么样的角色和权限?这些判断和处理都是由该类进行的。

FilterSecurityInterceptor 的大体放行逻辑如下:

  1. 『前面的』过滤器要在 SecurityContext 中存入一个 Authentication Token 。
  2. Authentication Token 的状态需要是『已认证』(isAuthenticated() == true);
  3. Authentication Token 中所包含的当前用户的权限信息,满足他所访问的当前 URI 的权限要求。

5、Spring Security 自带的 2 种认证方式

  • Http Basic 认证
  • 自带的表单页面

这是两个不需要我们『额外』写代码就能实现的登录 “界面” 。

/*** Spring Security配置类*/
@Configuration
@EnableWebSecurity
public class SecurityConfig{// 将PasswordEncoder注入到ioc容器// 设置用户正确的信息//认证规则判断/*** spring security过滤器链配配置* @param http* @return* @throws Exception*/@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{http.httpBasic(); // 或http.formLogin(); // 两者二选一。return http.build();}
}
a、Http Basic 认证
  • SpringSecurityConfig 类中的配置代码

     /*** spring security过滤器链配配置* @param http* @return* @throws Exception*/@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{//鉴权配置http.authorizeHttpRequests(authorizeHttpRequests->authorizeHttpRequests.anyRequest().authenticated());//认证配置http.httpBasic();//前后端项目中要禁用掉sessionhttp.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);//关闭crsf 跨域漏洞防御http.csrf(csrf-> csrf.disable()); return http.build();}
    

以上配置的意思是:

#说明
1要求用户登陆时,是使用 http basic 的方式。
2让 Spring Security 拦截所有请求。要求所有请求都必须通过认证才能放行,否则要求用户登陆。
3暂且认为是固定写法。后续专项讲解。

所谓的 http basic 方式指的就是如下图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

​ 浏览器通过这个弹出框收集你所输入的用户名密码,再发送给后台(Spring Security),而 Spring Security(截至目前为止是)以配置文件中配置的信息为基准,判断你的用户名和密码的正确性。如果认证通过,则浏览器收起弹出框,你将看到你原本的请求所应该看到的响应信息。

提示:有时你看不到这个弹出库那是因为你曾经登陆过之后,相关信息被浏览器缓存了。重开一个窗口即可,或使用 Chrome 浏览器的无痕模式。

​ 不过,http basic 认证方式有很大的安全隐患,在浏览器将用户所输入的用户名和密码发往后台的过程中,有被拦截盗取的可能。所以我们一定不会通过这种方式去收集用户的用户名和密码。

b、Spring Security 自带的表单认证
  • SpringSecurityConfig 类中的配置代码

     /*** spring security过滤器链配配置* @param http* @return* @throws Exception*/@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{//鉴权配置http.authorizeHttpRequests(authorizeHttpRequests->authorizeHttpRequests.anyRequest().authenticated());//认证配置http.formLogin();//前后端项目中要禁用掉sessionhttp.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);//关闭crsf 跨域漏洞防御http.csrf(csrf-> csrf.disable()); return http.build();}
    

    以上配置的意思是:

    #说明
    1要求用户登陆时,是使用表单页面进行登陆。但是,由于我们有意/无意中没有指明登陆页面,因此,Spring Security 会使用它自己自带的一个登陆页面。
    2同上,让 Spring Security 拦截所有请求。要求所有请求都必须通过认证才能放行,否则要求用户登陆。
    3同上,暂且认为是固定写法。后续专项讲解。

登陆页面效果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

​ 这就是我们上面案例所看到并使用的登陆页面。你在这个页面所输入的用户名密码,在发送给后台(Spring Security)后,Spring Security(截至目前为止是)以配置文件中配置的信息为基准,判断你的用户名和密码的正确性。

6、修改过滤器配置实现自定登录界面

在security中所有的功能都是使用过滤器来实现的。

 /*** spring security过滤器链配配置* @param http* @return* @throws Exception*/@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{//鉴权配置http.authorizeHttpRequests(authorizeHttpRequests->authorizeHttpRequests.anyRequest().authenticated());//认证配置http.formLogin().loginPage("/login.html")   //配置需要显示的登录页面;.loginProcessingUrl("/dologin") //配置登录请求路径.usernameParameter("account") //修改默认的登录帐号的键  默认为username.passwordParameter("password") //修改默认的密码的键  默认为password.permitAll();//这句配置很重要,新手容易忘记。放开 login.html和dologin 的访问权//前后端项目中要禁用掉sessionhttp.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);//关闭crsf 跨域漏洞防御http.csrf(csrf-> csrf.disable()); return http.build();}

7、认证的一些细节【重点】

  • security拦截静态资源问题

    如果在项目中没有配置springmvc视图解析器,项目中的静态资源会被security的鉴权过滤器进行拦截

    //鉴权配置http.authorizeHttpRequests(authorizeHttpRequests->authorizeHttpRequests//解决拦截静态资源问题.requestMatchers("/css/**","/js/**","/bootstrap/**").permitAll().anyRequest().authenticated());
    
  • 登录请求参数无法发送给security

    • 当使用自定义登录页面时,登录页面的名字和登录请求的url名称不能相同

       //配置与登录相关的过滤器http.formLogin().loginPage("/logindo.html")  //设置自定义页面,如果设置了自定义页面,则默认的认证地址会改为自定义页面的地址也就是logindo.loginProcessingUrl("/login")  //修改默认的登录请求地址.permitAll();//这句配置很重要,新手容易忘记。放开 login.html和dologin 的访问权
      
    • 异步提交无法将请求体中的数据传给security。UsernamePasswordAuthenticationFilter中规定,登录请求的默认帐号和密码名称必须是:username和password,默认提交地址:login,提交方式为post。这个过滤器中是通过queryString的方式来获得用户发送的登录数据

    • 页面代码

      <script>new Vue({el:"#app",data(){return{user:{username:'',password:''}}},methods:{btnLogin(){//将表单对象封装为queryStringlet formData=new URLSearchParams({'account':this.user.username,'password':this.user.password})axios.post('/login',formData.toString()).then(result=>{console.log(result.data);alert("登录成功");}).catch(e=>{console.log(e);alert("当前网络差。。。");});}},created(){}});
      </script>
      

8、认证成功和失败后的操作

a、默认行为

登录成功后的跳转页面、跳转路径有 2 种:

  • 如果用户是直接请求登录页面,那么登录成功后,默认会跳转至当前应用的根路径(/)。

  • 如果用户时访问某个受限页面/请求,被转到登录页面,那么登录成功后,默认会跳转至原本受限制的页面/请求。

当然,上述是『默认情况』,你可以通过配置,强行指定无论如何,在登录成功后,都跳转至 xxx 页面。

// 登录页面配置
http.formLogin().defaultSuccessUrl("/success.jsp");
//  .defaultSuccessUrl("/success.jsp", true);

​ 通过 .defaultSuccessUrl() 可以指定上述第 1 种情况下的成功跳转页面。如果多加一个参数 true,那么第 2 种情况下,登录成功后也会被强制跳转至这个特定页面。类似的,通过 .failureForwardUrl() 可以指定登录失败时跳转的错误页面。

b、认证成功后(返回jws token)

securtiy认证成功,会进入到security提供的认证成功处理器。在某些前后端完全分离,仅靠 JSON 完成所有交互的系统中,一般会在登陆成功时返回一段 JSON 数据,告知前端,登陆成功与否。在这里,可以通过 .successHandler 方法和 .failureHandler 方法指定『认证通过』之后和『认证未通过』之后的处理逻辑。

  • 创建认证成功后的处理器

    /*** 认证成功后的处理器*/
    @Component
    @Slf4j
    public class SimpleAuthenticationSuccessHandler implements AuthenticationSuccessHandler {@Resourceprivate RedisMapper redisMapper;@Overridepublic void onAuthenticationSuccess(HttpServletRequest request,HttpServletResponse response,Authentication authentication) throws IOException, ServletException {//获得security的UserDetails对象中保存的第一个参数User user=(User)authentication.getPrincipal();//获得security的UserDetails对象中保存的权限Collection<GrantedAuthority> permsItem=user.getAuthorities();String authoritys= StringUtils.join(permsItem, ",");//生成tokenString token= JwtUtils.createJwtToken(user.getUsername());//获得用户帐号String account=user.getUsername().split(",")[1];//保存用户token和权限到redisredisMapper.setKey(account+":token", token, 30, TimeUnit.MINUTES);redisMapper.setKey(account+":author", authoritys,30, TimeUnit.MINUTES);//返回成功状态码//响应请求转码response.setContentType("application/json;charset=UTF-8");ResponseResult<String> responseResult=new ResponseResult<String>(2000, "OK",token);//输出json字符串到客户端PrintWriter printWriter =response.getWriter();printWriter.print(JacksonUtil.toJsonString(responseResult));printWriter.flush();printWriter.close();}
    }
    
  • 将认证成功后的处理器与认证过滤器进行绑定

     /*** spring security过滤器链配配置* @param http* @return* @throws Exception*/@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{//鉴权配置http.authorizeHttpRequests(authorizeHttpRequests->authorizeHttpRequests.anyRequest().authenticated());//认证配置http.formLogin().successHandler(simpleAuthenticationSuccessHandler) //认证成功后的处理器.permitAll();//这句配置很重要,新手容易忘记。放开 login.html和dologin 的访问权//前后端项目中要禁用掉sessionhttp.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);//关闭crsf 跨域漏洞防御http.csrf(csrf-> csrf.disable()); return http.build();}
    
c、认证失败后

Spring Security 的认证工作是由 UsernamePasswordAuthenticationFilter 处理的。查看它( 和它的父类 AbstractAuthenticationProcessingFilter)的源码,我们可以看到:

  • 当认证通过时,会执行它( 继承自父类 )的 successfulAuthentication 方法。successfulAuthentication 的默认行为(之前讲过):继续用户原本像访问的 URI 。

    另外,你可以通过 http.successHandler(...) 来 “覆盖” 默认的成功行为。

  • 当认证不通过时,会执行它( 继承自父类 )的 unsuccessfulAuthentication 方法。unsuccessfulAuthentication 的默认行为是再次显示登陆页面,并在页面上提示用户名密码错误。

    另外,你可以通过 http.failureHandler(...) 来 “覆盖” 默认的失败行为。

  • 创建认证失败后处理器

/*** 认证异常后的处理器*/
@Component
@Slf4j
public class SimpleAuthenticationFailureHandler implements AuthenticationFailureHandler {@Overridepublic void onAuthenticationFailure(HttpServletRequest request,HttpServletResponse response,AuthenticationException exception) throws IOException, ServletException {//返回成功状态码ResponseResult<Void> responseResult=null;if(exception instanceof InternalAuthenticationServiceException){responseResult=new ResponseResult<Void>(3001, "帐号不存在");}if(exception instanceof BadCredentialsException){responseResult=new ResponseResult<Void>(3001, "密码错误");}//响应请求转码response.setContentType("application/json;charset=UTF-8");//输出json字符串到客户端PrintWriter printWriter =response.getWriter();printWriter.print(JacksonUtil.toJsonString(responseResult));printWriter.flush();printWriter.close();}
}
  • 将 失败后的处理器与认证过滤器进行绑定

     /*** spring security过滤器链配配置* @param http* @return* @throws Exception*/@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{//鉴权配置http.authorizeHttpRequests(authorizeHttpRequests->authorizeHttpRequests.anyRequest().authenticated());//认证配置http.formLogin().successHandler(simpleAuthenticationSuccessHandler) //认证成功后的处理器.failureHandler(simpleAuthenticationFailureHandler) //认证失败后的处理器.permitAll();//这句配置很重要,新手容易忘记。放开 login.html和dologin 的访问权//前后端项目中要禁用掉sessionhttp.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);//关闭crsf 跨域漏洞防御http.csrf(csrf-> csrf.disable()); return http.build();}
    

三、Spring Security 退出操作

Spring Security中发送了logout请求成功后会自动跳转到默认的login.html页面。在前后端分离的项目中,所有的页面跳转都是由前端控制的,服务器端只需要返回一个json的状态码即可

  • 创建登录成功后的处理器类

    /*** 退出成功后的处理器*/
    @Component
    public class SimpLogoutSuccessHandler implements LogoutSuccessHandler {@Overridepublic void onLogoutSuccess(HttpServletRequest request,HttpServletResponse response,Authentication authentication) throws IOException, ServletException {//设置响应数据的编码格式response.setContentType("application/json;charset=UTF-8");ResponseResult<Void> responseResult=new ResponseResult<>(2000, "退出成功");PrintWriter out= response.getWriter();out.write(JacksonUtil.toJsonString(responseResult));out.close();}
    }
    
  • 修改Spring Security配置类

    /*** spring security过滤器链配配置* @param http* @return* @throws Exception*/@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{//鉴权配置http.authorizeHttpRequests(authorizeHttpRequests->authorizeHttpRequests.anyRequest().authenticated());//认证配置http.formLogin().successHandler(simpleAuthenticationSuccessHandler) //认证成功后的处理器.failureHandler(simpleAuthenticationFailureHandler) //认证失败后的处理器.permitAll();//这句配置很重要,新手容易忘记。放开 login.html和dologin 的访问权//退出操作配置http.logout().logoutSuccessHandler(simpLogoutSuccessHandler);//前后端项目中要禁用掉sessionhttp.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);//关闭crsf 跨域漏洞防御http.csrf(csrf-> csrf.disable()); return http.build();}
    

四、Spring Security整合JWT

1、放行携带 JWT Token 的请求

放行请求的关键在于 FilterSecurityInterceptor 不要抛异常,而 FilterSecurityInterceptor 不抛异常则需要满足两点:

  • Spring Security 上下文( Context ) 中要有一个 Authentication Token ,且是已认证状态。

  • Authentication Token 中所包含的 User 的权限信息要满足访问当前 URI 的权限要求。

所以实现思路的关键在于:在 FilterSecurityInterceptor 之前( 废话 )要有一个 Filter 将用户请求中携带的 JWT 转化为 Authentication Token 存在 Spring Security 上下文( Context )中给 “后面” 的 FilterSecurityInterceptor 用。

基于上述思路,我们要实现一个 Filter :

/*** 验证token是否合法*/
@Component
public class JwtFilter extends OncePerRequestFilter {@Resourceprivate RedisMapper redisMapper;@Overrideprotected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain filterChain) throws ServletException, IOException {//获得security上下文对象Authentication authentication = SecurityContextHolder.getContext().getAuthentication();//如果authentication!=null说明是登录请求,直接放行if (authentication != null) {filterChain.doFilter(request, response);return;}//到此处说明是非登录请求,获得请求头中的tokenString jwsToken=request.getHeader("jwsToken");//请求头中没有token,表示未登录,放行进入到异常过滤器if(StringUtils.isEmpty(jwsToken)){filterChain.doFilter(request,response);return;}//token不合法,放行进入到异常过滤器if(!JwtUtils.verify(jwsToken)){filterChain.doFilter(request,response);return;}String userStr=JwtUtils.getUserNameFormJwt(jwsToken);String account=userStr.split(",")[1];//token过期,放行进入到异常过滤器if(!redisMapper.hasKey(account+":token")){filterChain.doFilter(request,response);return;}//token合法,且未过期,判断redis中的token与传过来的token是否相同String redisToken= (String) redisMapper.getKey(account+":token");//token不相同,放行进入到异常过滤器if(!jwsToken.equals(redisToken)){filterChain.doFilter(request,response);return;}//token合法,续期String authoritys= (String) redisMapper.getKey(account+":author");redisMapper.setKey(account+":token",jwsToken ,30, TimeUnit.MINUTES);redisMapper.setKey(account+":author", authoritys, 30, TimeUnit.MINUTES);//token验证成功后如何告诉security这个人登录过。可以进行鉴权//将jwttoken转为security能识别的认证标识UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(userStr,redisToken,AuthorityUtils.commaSeparatedStringToAuthorityList(authoritys));//将security认证令牌添加到上下文中SecurityContextHolder.getContext().setAuthentication(authenticationToken);filterChain.doFilter(request, response);//放行到下一个过滤器}
}

虽然 Spring Security Filter Chain 对过滤器没有特殊要求,只要实现了 Filter 接口即可,但是在 Spring 体系中,推荐使用 OncePerRequestFilter 来实现,它可以确保一次请求只会通过一次该过滤器(而普通的 Filter 并不能保证这一点)。

修改spring Security配置类:

/*** spring security过滤器链配配置* @param http* @return* @throws Exception*/@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{//鉴权配置http.authorizeHttpRequests(authorizeHttpRequests->authorizeHttpRequests.anyRequest().authenticated());//认证配置http.formLogin().successHandler(simpleAuthenticationSuccessHandler) //认证成功后的处理器.failureHandler(simpleAuthenticationFailureHandler) //认证失败后的处理器.permitAll();//这句配置很重要,新手容易忘记。放开 login.html和dologin 的访问权//退出操作配置http.logout().logoutSuccessHandler(simpLogoutSuccessHandler);//将自定义的jwtFilter添加到Spring Security过滤器链的倒数第二个以前http.addFilterAfter(jwtFilter, UsernamePasswordAuthenticationFilter.class);//前后端项目中要禁用掉sessionhttp.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);//关闭crsf 跨域漏洞防御http.csrf(csrf-> csrf.disable()); return http.build();}

2、匿名认证异常处理

Spring Security 的认证工作是由 FilterSecurityInterceptor 处理的。FilterSecurityInterceptor 会抛出 2 种异常:

  • 在用户 “该登录而未登录” 时,抛出 AuthenticationException 异常;默认情况下,抛出 AuthenticationException 异常时,Spring Security 返回 401 错误:未授权(Unauthorized)。
  • 在用户 “权限不够” 时,抛出 AccessDeniedException 异常。默认情况下,抛出 AccessDeniedException 异常时,Spring Security 返回 403 错误:被拒绝(Forbidden)访问

在 Spring Security 配置中可以通过 http.exceptionHandling() 配置方法用来自定义鉴权环节的异常处理。配置风格如下:

http.exceptionHandling().authenticationEntryPoint(...).accessDeniedHandler(...);

其中:

  • AuthenticationEntryPoint 该类用来统一处理 AuthenticationException 异常;
  • AccessDeniedHandler 该类用来统一处理 AccessDeniedException 异常。

示例:

  • 创建认证异常处理器

    /*** 认证异常处理器*/
    @Component
    public class SimpleAuthenticationEntryPoint implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response,AuthenticationException authException) throws IOException, ServletException {ResponseResult<Void> responseResult=new ResponseResult<>(4001, "未登录,请先登录");response.setStatus(HttpServletResponse.SC_OK);response.setCharacterEncoding(StandardCharsets.UTF_8.toString());response.setContentType(MediaType.APPLICATION_JSON_VALUE);PrintWriter printWriter = response.getWriter();printWriter.print(JacksonUtil.toJsonString(responseResult));printWriter.flush();printWriter.close();}
    }
    
  • 修改security过滤器配置

     /*** spring security过滤器链配配置* @param http* @return* @throws Exception*/@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{//鉴权配置http.authorizeHttpRequests(authorizeHttpRequests->authorizeHttpRequests.anyRequest().authenticated());//认证配置http.formLogin().successHandler(simpleAuthenticationSuccessHandler) //认证成功后的处理器.failureHandler(simpleAuthenticationFailureHandler) //认证失败后的处理器.permitAll();//这句配置很重要,新手容易忘记。放开 login.html和dologin 的访问权//退出操作配置http.logout().logoutSuccessHandler(simpLogoutSuccessHandler);//将自定义的jwtFilter添加到Spring Security过滤器链的倒数第二个以前http.addFilterAfter(jwtFilter, UsernamePasswordAuthenticationFilter.class);//认证和鉴权异常配置http.exceptionHandling().authenticationEntryPoint(simpleAuthenticationEntryPoint);//认证异常处理器//前后端项目中要禁用掉sessionhttp.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);//关闭crsf 跨域漏洞防御http.csrf(csrf-> csrf.disable());return http.build();}
    

五、Spring Security鉴权

1、配置方式实现鉴权

当前用户是否有权限访问某个 URI 的相关配置也是写在 configure(HttpSecurity http) 方法中。

.requestMatchers() 方法是一个采用 ANT 风格的 URL 匹配器。

  • 使用 ? 匹配任意单个字符

  • 使用 * 匹配 0 或任意数量的字符

  • 使用 ** 匹配 0 或更多的目录

    权限表达式说明
    permitAll()永远返回 true
    denyAll()永远返回 false
    anonymous()当前用户是匿名用户(anonymous)时返回 true
    rememberMe()当前用户是 rememberMe 用户时返回 true
    authentication当前用户不是匿名用户时,返回 true
    fullyAuthenticated当前用户既不是匿名用户,也不是 rememberMe 用户时,返回 true
    hasRole(“role”)当用户拥有指定身份时,返回 true
    hasAnyRole(“role1”, “role2”, …)当用户返回指定身份中的任意一个时,返回 true
    hasAuthority(“authority1”)当用于拥有指定权限时,返回 true
    hasAnyAuthority(“authority1”, “authority2”)当用户拥有指定权限中的任意一个时,返回 true
    hasIpAddress(“xxx.xxx.x.xxx”)发送请求的 ip 符合指定时,返回 true
    principal允许直接访问主体对象,表示当前用户
语法:http.authorizeHttpRequests(authorizeHttpRequests->authorizeHttpRequests.requestMatchers("/user/insert").hasAuthority("user:insert").requestMatchers("/user/modify").hasAuthority("user:modify").requestMatchers("/user/delete").hasAuthority("user:delete").requestMatchers("/user/query").hasAuthority("user:query").requestMatchers("/user/**").hasAnyRole("USER", "ADMIN").requestMatchers("/user-can-do").hasRole("USER") // 这里本质上应该是 ROLE_USER,但是 ROLE_ 要移除。不过你所提供的标准答案中,又必须要有 ROLE_ !.requestMatchers("/admin-can-do").hasRole("ADMIN") // 同上.requestMatchers("/all-can-do").permitAll().anyRequest().authenticated());

提示:本质上 .hasRole("xxx").hasAuthority("xxx") 并没有太大区别,但是,.hashRole() 在做比对时,会在 xxx 前面拼上 ROLE_所以,确保你的 Role 的『标准答案』是以 Role_ 开头

2、使用注解实现鉴权

​ 在实际的使用过程中用户的鉴权并不是通过置来写的而是通过注解来进行,Spring Security 默认是禁用注解的。

要想开启注解功能需要在配置类上加入 @EnableGlobalMethodSecurity 注解来判断用户对某个控制层的方法是否具有访问权限。

注解就是用来替换springSecurity配置类中的http.authorizeRequests()配置

/*** Spring Security配置类*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true) //开启security的PrePost 鉴权注解
public class SecurityConfig{@Resourceprivate LoginService loginService;//认证成功后处理器@Resourceprivate SimpleAuthenticationSuccessHandler simpleAuthenticationSuccessHandler;//认证失败后的处理器@Resourceprivate SimpleAuthenticationFailureHandler simpleAuthenticationFailureHandler;//退出成功后的处理器@Resourceprivate SimpLogoutSuccessHandler simpLogoutSuccessHandler;//认证异常处理器@Resourceprivate SimpleAuthenticationEntryPoint simpleAuthenticationEntryPoint;//鉴权异常处理器@Resourceprivate SimpleAccessDeniedHandler simpleAccessDeniedHandler;//判断是否登录过滤器@Resourceprivate JwtFilter jwtFilter;/*** 将PasswordEncoder注入到ioc容器** @return*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** 设置用户正确的信息* @return*/@Beanpublic UserDetailsService userDetailsService(){//调用LoginService中的登录查询方法return username -> loginService.loadUserByUsername(username);}/*** 认证规则判断* @return*/@Beanpublic AuthenticationProvider authenticationProvider(){DaoAuthenticationProvider authProvider=new DaoAuthenticationProvider();authProvider.setUserDetailsService(userDetailsService()); //调用自定义登录类,获得用户数据authProvider.setPasswordEncoder(passwordEncoder()); //设置密码加密器return authProvider;}/*** spring security过滤器链配配置* @param http* @return* @throws Exception*/@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{//鉴权配置http.authorizeHttpRequests(authorizeHttpRequests->authorizeHttpRequests.anyRequest().authenticated());//认证配置http.formLogin().successHandler(simpleAuthenticationSuccessHandler) //认证成功后的处理器.failureHandler(simpleAuthenticationFailureHandler) //认证失败后的处理器.permitAll();//这句配置很重要,新手容易忘记。放开 login.html和dologin 的访问权//退出操作配置http.logout().logoutSuccessHandler(simpLogoutSuccessHandler);//将自定义的jwtFilter添加到Spring Security过滤器链的倒数第二个以前http.addFilterAfter(jwtFilter, UsernamePasswordAuthenticationFilter.class);//认证和鉴权异常配置http.exceptionHandling().authenticationEntryPoint(simpleAuthenticationEntryPoint) //认证异常处理器.accessDeniedHandler(simpleAccessDeniedHandler); //鉴权异常处理器//前后端项目中要禁用掉sessionhttp.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);//关闭crsf 跨域漏洞防御http.csrf(csrf-> csrf.disable());return http.build();}
}

Spring Security 支持三套注解:

注解类型注解
jsr250 注解@DenyAll、@PermitAll、@RolesAllowed
secured 注解@Secured
prePost 注解@PreAuthorize、@PostAuthorize
a、JSR-250 注解

JSR-250 注解是用来标识controller中的请求需要什么角色才能访问

@RestController
public class UserController {@RolesAllowed({"USER","ADMIN"}) // 这里可以省略前缀 ROLE_,但是数据库中的角色信息必须以 ROLE_ 开头@RequestMapping("/all-can-do")public String show7(){return "all-can-do";}@RolesAllowed("USER") // 这里可以省略前缀 ROLE_,但是数据库中的角色信息必须以 ROLE_ 开头@RequestMapping("/admin-can-do")public String show6(){return "admin-can-do";}
}

@DenyAll@PermitAll@RolesAllowed 三个注解的功能显而易见。

不过有一个容易误解的地方: .permitAll().anonymous() 的区别:

Spring Security 为了统一,给 “未登录” 的用户赋予了一个角色:匿名用户

配置类中的配置 .antMatchers("/anonCanDo").anonymous() 表示匿名用户可访问,自然也就是用户不需要登录认证即可访问该 URI

注意:一旦用户经过登陆后,其身份无论在是什么,他都不再是匿名用户了,即,它失去了匿名用户这个身份。此时,如果他再去访问匿名用户可登陆的 URI 反而是显示没有权限!

.antMatchers("/", "/users").permitAll() 就没有这个问题。它是指无论是否登陆,登陆后无论是什么身份都能访问。所以,你心里想要表达的『匿名用户也可以访问』大概率是指 .permitAll(),而非 .anonymous()

b、Secured 注解

@Secured 注解是 jsr250 标准出现之前,Spring Security 框架自己定义的注解。

@Secured 标注于方法上,表示只有具有它所指定的角色的用户才可以调用该方法。如果当前用户不具备所要求的角色,那么,将会抛出 AccessDenied 异常。

@RestController
public class UserController {@Secured({"USER","ADMIN"}) // 这里可以省略前缀 ROLE_,但是数据库中的角色信息必须以 ROLE_ 开头@RequestMapping("/all-can-do")public String show7(){return "all-can-do";}@Secured("USER") // 这里可以省略前缀 ROLE_,但是数据库中的角色信息必须以 ROLE_ 开头@RequestMapping("/admin-can-do")public String show6(){return "admin-can-do";}
}
c、PrePost 注解

PrePost 注解也是 jsr250 标准出现之前,Spring Security 框架自己定义的注解。

PrePost 注解的功能比 Secured 注解的功能更强大,它可以通过使用 Spring EL 来表达具有逻辑判断的校验规则。

  • @PreAuthorize 注解:适合进入方法前的权限验证;
  • @PostAuthorize 注解:使用并不多,在方法执行后再进行权限验证。
@RestController
public class UserController {@PreAuthorize("hasRole('USER')")  // 等同于前面章节的配置中的 hasRole("..."),只有USER角色才能访问@PreAuthorize("hasAuthority('user:insert')")@RequestMapping("/admin-can-do")public String show6(){return "admin-can-do";}
}

3、鉴权的异常处理

Spring Security 的认证工作是由 FilterSecurityInterceptor 处理的。FilterSecurityInterceptor 会抛出 2 种异常:

  • 在用户 “该登录而未登录” 时,抛出 AuthenticationException 异常;默认情况下,抛出 AuthenticationException 异常时,Spring Security 返回 401 错误:未授权(Unauthorized)。
  • 在用户 “权限不够” 时,抛出 AccessDeniedException 异常。默认情况下,抛出 AccessDeniedException 异常时,Spring Security 返回 403 错误:被拒绝(Forbidden)访问

在 Spring Security 配置中可以通过 http.exceptionHandling() 配置方法用来自定义鉴权环节的异常处理。配置风格如下:

http.exceptionHandling().authenticationEntryPoint(...).accessDeniedHandler(...);

其中:

  • AuthenticationEntryPoint 该类用来统一处理 AuthenticationException 异常;
  • AccessDeniedHandler 该类用来统一处理 AccessDeniedException 异常。

示例:

  • 创建鉴权异常处理器

    /***鉴权异常处理器*/
    @Component
    public class SimpleAccessDeniedHandler implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request,HttpServletResponse response,AccessDeniedException accessDeniedException)throws IOException, ServletException {ResponseResult<Void> responseResult=new ResponseResult<>(4003, "无此权限,请联系管理员");response.setStatus(HttpServletResponse.SC_FORBIDDEN);response.setCharacterEncoding(StandardCharsets.UTF_8.toString());response.setContentType(MediaType.APPLICATION_JSON_VALUE);PrintWriter printWriter = response.getWriter();printWriter.print(JacksonUtil.toJsonString(responseResult));printWriter.flush();printWriter.close();}
    }
    
  • 修改SpringSecurity配置类

      /*** spring security过滤器链配配置* @param http* @return* @throws Exception*/@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{//鉴权配置http.authorizeHttpRequests(authorizeHttpRequests->authorizeHttpRequests.anyRequest().authenticated());//认证配置http.formLogin().successHandler(simpleAuthenticationSuccessHandler) //认证成功后的处理器.failureHandler(simpleAuthenticationFailureHandler) //认证失败后的处理器.permitAll();//这句配置很重要,新手容易忘记。放开 login.html和dologin 的访问权//退出操作配置http.logout().logoutSuccessHandler(simpLogoutSuccessHandler);//将自定义的jwtFilter添加到Spring Security过滤器链的倒数第二个以前http.addFilterAfter(jwtFilter, UsernamePasswordAuthenticationFilter.class);//认证和鉴权异常配置http.exceptionHandling().authenticationEntryPoint(simpleAuthenticationEntryPoint) //认证异常处理器.accessDeniedHandler(simpleAccessDeniedHandler); //鉴权异常处理器//前后端项目中要禁用掉sessionhttp.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);//关闭crsf 跨域漏洞防御http.csrf(csrf-> csrf.disable());return http.build();}
    

六、SpringSecurity认证接收JSON参数

Security默认是key-value的形式的。通过查看UsernamePasswordAuthenticationFilter可以看到如下代码

image-20240815090729886

可以看到帐号和密码在security中是通过request.getParameter获取的,而request.getParameter是不支持json格式,所以我们只需要自己写代码获得json中的帐号和密码然后生成一个UsernamePasswordAuthenticationToken令牌即可

  • 创建接收认证参数对象

    /*** 接收认证参数*/
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class LoginUser implements Serializable {private String account;private String password;
    }
    
  • 创建登录Controller

    /*** 认证Controller*/
    @RestController
    public class LoginController {@Resourceprivate AuthenticationManager authenticationManager;@Resourceprivate RedisMapper redisMapper;/*** 认证请求* @param loginUser* @return*/@PostMapping("/login")public ResponseResult<String> login(@RequestBody LoginUser loginUser){try {//生成认证tokenUsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(loginUser.getAccount(), loginUser.getPassword());//数据比较器,判断用户输入的帐号和密码是否相同,不相同则抛异常Authentication auth = authenticationManager.authenticate(authRequest);//获得security的UserDetails对象中保存的第一个参数User user=(User)auth.getPrincipal();//获得security的UserDetails对象中保存的权限Collection<GrantedAuthority> permsItem=user.getAuthorities();String authoritys= StringUtils.join(permsItem, ",");//生成tokenString jwsToken= JwtUtils.createJwtToken(user.getUsername());//获得用户帐号String account=user.getUsername().split(",")[1];//保存用户token和权限到redisredisMapper.setKey(account+":token", jwsToken, 30, TimeUnit.MINUTES);redisMapper.setKey(account+":author", authoritys,30, TimeUnit.MINUTES);//返回结果给用户return new ResponseResult<String>(2000, "OK",jwsToken);} catch (AuthenticationException e) {if(e instanceof InternalAuthenticationServiceException){return new ResponseResult<String>(3001, "帐号不存在");}if(e instanceof BadCredentialsException){return new ResponseResult<String>(3002, "密码错误");}}return new ResponseResult<String>(3003, "帐号或密码错误");}
    }
    
  • 修改security配置类

    项目中不用再配置认证成功和失败的处理器

    /*** Spring Security配置类*/
    @Configuration
    @EnableWebSecurity
    @EnableMethodSecurity(prePostEnabled = true) //开启security的PrePost 鉴权注解
    public class SecurityConfig{@Resourceprivate LoginService loginService;//退出成功后的处理器@Resourceprivate SimpLogoutSuccessHandler simpLogoutSuccessHandler;//认证异常处理器@Resourceprivate SimpleAuthenticationEntryPoint simpleAuthenticationEntryPoint;//鉴权异常处理器@Resourceprivate SimpleAccessDeniedHandler simpleAccessDeniedHandler;//判断是否登录过滤器@Resourceprivate JwtFilter jwtFilter;/*** 将PasswordEncoder注入到ioc容器** @return*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** 设置用户正确的信息* @return*/@Beanpublic UserDetailsService userDetailsService(){//调用LoginService中的登录查询方法return username -> loginService.loadUserByUsername(username);}/*** 用户数据比较器* @return*/@Beanpublic AuthenticationManager authenticationManager(){DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();daoAuthenticationProvider.setUserDetailsService(userDetailsService());daoAuthenticationProvider.setPasswordEncoder(passwordEncoder()); //设置密码加密器ProviderManager providerManager = new ProviderManager(daoAuthenticationProvider);return providerManager;}/*** spring security过滤器链配配置* @param http* @return* @throws Exception*/@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{//鉴权配置http.authorizeHttpRequests(authorizeHttpRequests->authorizeHttpRequests//允许所有的OPTIONS请求.requestMatchers(HttpMethod.OPTIONS,"/**").permitAll()//放行swagger3.requestMatchers(HttpMethod.GET,"/v3/api-docs/**","/doc.html","/webjars/**").permitAll().requestMatchers("/login").permitAll() //放行自定义登录请求.anyRequest().authenticated());//退出操作配置http.logout().logoutSuccessHandler(simpLogoutSuccessHandler);//将自定义的jwtFilter添加到Spring Security过滤器链的倒数第二个以前http.addFilterAfter(jwtFilter, UsernamePasswordAuthenticationFilter.class);//认证和鉴权异常配置http.exceptionHandling().authenticationEntryPoint(simpleAuthenticationEntryPoint) //认证异常处理器.accessDeniedHandler(simpleAccessDeniedHandler); //鉴权异常处理器//前后端项目中要禁用掉sessionhttp.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);//关闭crsf 跨域漏洞防御http.csrf(csrf-> csrf.disable());return http.build();}
    }
    

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 学习C语言 第十八天
  • Java面试题--分布式锁
  • 多目标跟踪之StrongSORT论文(翻译+精读)
  • 目标检测 | yolov9 原理和介绍
  • 吐血整理 ChatGPT 3.5/4.0/4o 新手使用手册~
  • 使用 Python 进行 PDF 文件加密
  • Sed编辑器
  • 【案例49】ORA-01000:超出打开游标的最大数
  • 基于SpringBoot+Vu e.js校园疫情防控系统的设计与实现
  • Visual Studio中 生成版本号
  • LeetCode //C - 316. Remove Duplicate Letters
  • Java-ByteArrayResource和InputStream
  • RabbitMQ的介绍
  • 深入理解 Go 语言原子内存操作
  • VS工程中的ALL_BUILD、INSTALL、ZERO_CHECK简介
  • Akka系列(七):Actor持久化之Akka persistence
  • ERLANG 网工修炼笔记 ---- UDP
  • Git初体验
  • JavaScript-Array类型
  • MD5加密原理解析及OC版原理实现
  • Otto开发初探——微服务依赖管理新利器
  • Redis学习笔记 - pipline(流水线、管道)
  • uni-app项目数字滚动
  • Vim 折腾记
  • 翻译--Thinking in React
  • 如何设计一个比特币钱包服务
  • 深入体验bash on windows,在windows上搭建原生的linux开发环境,酷!
  • 使用 @font-face
  • 使用 Docker 部署 Spring Boot项目
  • 手机端车牌号码键盘的vue组件
  • 王永庆:技术创新改变教育未来
  • 云栖大讲堂Java基础入门(三)- 阿里巴巴Java开发手册介绍
  • 自定义函数
  • hi-nginx-1.3.4编译安装
  • #我与Java虚拟机的故事#连载10: 如何在阿里、腾讯、百度、及字节跳动等公司面试中脱颖而出...
  • (13)Hive调优——动态分区导致的小文件问题
  • (13)Latex:基于ΤΕΧ的自动排版系统——写论文必备
  • (22)C#传智:复习,多态虚方法抽象类接口,静态类,String与StringBuilder,集合泛型List与Dictionary,文件类,结构与类的区别
  • (27)4.8 习题课
  • (pt可视化)利用torch的make_grid进行张量可视化
  • (附源码)计算机毕业设计SSM保险客户管理系统
  • (经验分享)作为一名普通本科计算机专业学生,我大学四年到底走了多少弯路
  • (算法)N皇后问题
  • (算法)Travel Information Center
  • (转)Sql Server 保留几位小数的两种做法
  • (转)Windows2003安全设置/维护
  • ***汇编语言 实验16 编写包含多个功能子程序的中断例程
  • .bat批处理出现中文乱码的情况
  • .NET “底层”异步编程模式——异步编程模型(Asynchronous Programming Model,APM)...
  • .NET Core 发展历程和版本迭代
  • .Net Core中Quartz的使用方法
  • .NET Reactor简单使用教程
  • .NET 应用架构指导 V2 学习笔记(一) 软件架构的关键原则
  • .NET程序集编辑器/调试器 dnSpy 使用介绍
  • .NET轻量级ORM组件Dapper葵花宝典