SpringSecurity6

news2024/9/20 15:13:12

一、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启动类

      @SpringBootApplication
      public 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: tom
            password: 123

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

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

/**
 * Spring Security配置类
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig{

    /**
     * 设置用户正确的信息,这个数据一般是从数据库中查询
     * @return
     */
    @Bean
    public 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
     */
    @Bean
    public 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
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 设置用户正确的信息
     * @return
     */
    @Bean
    public 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
     */
    @Bean
    public 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 {

    @Resource
    private UsersMapper usersMapper;
    @Resource
    private PermsMapper permsMapper;

    /**
     * 只能在这个方法中编写登录查询逻辑
     * 该方法只做正确的数据提供,不做任何的判断
     * @param username  就是前端传给security中的帐号
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public 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{

    @Resource
    private LoginService loginService;

    /**
     * 将PasswordEncoder注入到ioc容器
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


    /**
     * 设置用户正确的信息
     * @return
     */
    @Bean
    public UserDetailsService userDetailsService(){
       //调用LoginService中的登录查询方法
        return username -> loginService.loadUserByUsername(username);
    }

    /**
     * 认证规则判断
     * @return
     */
    @Bean
    public 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
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
        http.httpBasic(); // 或
        http.formLogin(); // 两者二选一。
        
        return http.build();
    }
}
a、Http Basic 认证
  • SpringSecurityConfig 类中的配置代码

     /**
         * spring security过滤器链配配置
         * @param http
         * @return
         * @throws Exception
         */
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
            //鉴权配置
            http.authorizeHttpRequests(authorizeHttpRequests->authorizeHttpRequests
                    .anyRequest().authenticated());
    
            //认证配置
            http.httpBasic();
    
            //前后端项目中要禁用掉session
            http.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
         */
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
            //鉴权配置
            http.authorizeHttpRequests(authorizeHttpRequests->authorizeHttpRequests
                    .anyRequest().authenticated());
    
            //认证配置
            http.formLogin();
    
            //前后端项目中要禁用掉session
            http.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
     */
    @Bean
    public 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 的访问权

        //前后端项目中要禁用掉session
        http.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(){
                      //将表单对象封装为queryString
                      let 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 {
    
        @Resource
        private RedisMapper redisMapper;
    
        @Override
        public 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, ",");
    
            //生成token
            String token= JwtUtils.createJwtToken(user.getUsername());
    
            //获得用户帐号
            String account=user.getUsername().split(",")[1];
            //保存用户token和权限到redis
            redisMapper.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
         */
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
            //鉴权配置
            http.authorizeHttpRequests(authorizeHttpRequests->authorizeHttpRequests
                    .anyRequest().authenticated());
    
            //认证配置
            http.formLogin()
                    .successHandler(simpleAuthenticationSuccessHandler) //认证成功后的处理器
                    .permitAll();//这句配置很重要,新手容易忘记。放开 login.html和dologin 的访问权
    
           //前后端项目中要禁用掉session
            http.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 {

    @Override
    public 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
         */
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
            //鉴权配置
            http.authorizeHttpRequests(authorizeHttpRequests->authorizeHttpRequests
                    .anyRequest().authenticated());
    
            //认证配置
            http.formLogin()
                    .successHandler(simpleAuthenticationSuccessHandler) //认证成功后的处理器
                    .failureHandler(simpleAuthenticationFailureHandler) //认证失败后的处理器
                    .permitAll();//这句配置很重要,新手容易忘记。放开 login.html和dologin 的访问权
    
            //前后端项目中要禁用掉session
            http.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 {
        @Override
        public 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
         */
        @Bean
        public 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);
    
         	//前后端项目中要禁用掉session
            http.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 {

    @Resource
    private RedisMapper redisMapper;

    @Override
    protected 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;
        }

        //到此处说明是非登录请求,获得请求头中的token
        String 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
     */
    @Bean
    public 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);

       //前后端项目中要禁用掉session
        http.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 {
    
        @Override
        public 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
         */
        @Bean
        public 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);//认证异常处理器
    
            //前后端项目中要禁用掉session
            http.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{

    @Resource
    private LoginService loginService;

    //认证成功后处理器
    @Resource
    private SimpleAuthenticationSuccessHandler simpleAuthenticationSuccessHandler;

    //认证失败后的处理器
    @Resource
    private SimpleAuthenticationFailureHandler simpleAuthenticationFailureHandler;

    //退出成功后的处理器
    @Resource
    private SimpLogoutSuccessHandler simpLogoutSuccessHandler;
    //认证异常处理器
    @Resource
    private SimpleAuthenticationEntryPoint simpleAuthenticationEntryPoint;
    //鉴权异常处理器
    @Resource
    private SimpleAccessDeniedHandler simpleAccessDeniedHandler;

    //判断是否登录过滤器
    @Resource
    private JwtFilter jwtFilter;

    /**
     * 将PasswordEncoder注入到ioc容器
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


    /**
     * 设置用户正确的信息
     * @return
     */
    @Bean
    public UserDetailsService userDetailsService(){
       //调用LoginService中的登录查询方法
        return username -> loginService.loadUserByUsername(username);
    }

    /**
     * 认证规则判断
     * @return
     */
    @Bean
    public AuthenticationProvider authenticationProvider(){
        DaoAuthenticationProvider authProvider=new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService()); //调用自定义登录类,获得用户数据
        authProvider.setPasswordEncoder(passwordEncoder()); //设置密码加密器
        return authProvider;
    }

    /**
     * spring security过滤器链配配置
     * @param http
     * @return
     * @throws Exception
     */
    @Bean
    public 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); //鉴权异常处理器

        //前后端项目中要禁用掉session
        http.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 {
        @Override
        public 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
         */
        @Bean
        public 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); //鉴权异常处理器
    
            //前后端项目中要禁用掉session
            http.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 {
    
        @Resource
        private AuthenticationManager authenticationManager;
    
        @Resource
        private RedisMapper redisMapper;
    
        /**
         * 认证请求
         * @param loginUser
         * @return
         */
        @PostMapping("/login")
        public ResponseResult<String> login(@RequestBody LoginUser loginUser){
            try {
                //生成认证token
                UsernamePasswordAuthenticationToken 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, ",");
    
                //生成token
                String jwsToken= JwtUtils.createJwtToken(user.getUsername());
    
                //获得用户帐号
                String account=user.getUsername().split(",")[1];
                //保存用户token和权限到redis
                redisMapper.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{
    
        @Resource
        private LoginService loginService;
    
        //退出成功后的处理器
        @Resource
        private SimpLogoutSuccessHandler simpLogoutSuccessHandler;
        //认证异常处理器
        @Resource
        private SimpleAuthenticationEntryPoint simpleAuthenticationEntryPoint;
        //鉴权异常处理器
        @Resource
        private SimpleAccessDeniedHandler simpleAccessDeniedHandler;
    
        //判断是否登录过滤器
        @Resource
        private JwtFilter jwtFilter;
    
        /**
         * 将PasswordEncoder注入到ioc容器
         *
         * @return
         */
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
    
        /**
         * 设置用户正确的信息
         * @return
         */
        @Bean
        public UserDetailsService userDetailsService(){
           //调用LoginService中的登录查询方法
            return username -> loginService.loadUserByUsername(username);
        }
    
    
        /**
         * 用户数据比较器
         * @return
         */
        @Bean
       public 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
         */
        @Bean
        public 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); //鉴权异常处理器
    
            //前后端项目中要禁用掉session
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
            //关闭crsf 跨域漏洞防御
            http.csrf(csrf-> csrf.disable());
            return http.build();
        }
    }
    

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2059148.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

el-table实现动态添加行,并且有父子级联动下拉框

<template><div><el-button click"addRow">添加行</el-button><el-table :data"tableData" style"width: 100%"><el-table-column label"序号"type"index"width"100"align"…

flink读写案例合集

文章目录 前言一、flink 写kafka1.第一种使用FlinkKafkaProducer API2.第二种使用自定义序列化器3.第三种使用FlinkKafkaProducer011 API4.使用Kafka的Avro序列化 (没有使用过,感觉比较复杂)5.第五种使用 (强烈推荐使用)二、Flink读kafka三、Flink写其他外部系统前言 提示:这…

【Kettle实战】组件讲解(战前磨刀)

目录 【 CSV 文件输入 】组件【过滤记录】组件【字段选择】组件【排序记录】组件【分组】组件【Excel输出】组件【 CSV 文件输入 】组件 基础参数解释: 字段参数解释: 【过滤记录】组件 在数据处理时,往往要对数据所属类别、区域和时间等进行限制,将限制范围外的数据过…

上千条备孕至育儿指南速查ACCESS\EXCEL数据库

虽然今天这个数据库的记录数才不过区区上千条&#xff0c;但是每条记录里的内容都包含四五个子标题&#xff0c;可以将相关的知识完整且整齐的展现&#xff0c;是个属于简而精的数据库。并且它包含2级分类。 【备孕】大类包含&#xff1a;备孕百科(19)、不孕不育(23)、精子卵子…

QStorageInfo 出现C2228报错

这里使用 QStorageInfo info(QDir(path));创建就会报错 改为 // 获取给定路径所在的磁盘信息 QDir d(path); QStorageInfo info(d);就不会报错&#xff0c;怪噻&#xff01;

【区块链+商贸零售】消费券 2.0 应用方案 | FISCO BCOS应用案例

方案基于FISCO BCOS区块链技术与中间件平台WeBASE&#xff0c;实现新一代消费券安全精准高效发放&#xff0c;实现消费激励&#xff0c; 促进消费循环。同时&#xff0c;方案将用户消费数据上链&#xff0c;实现账本记录与管理&#xff0c;同时加密机制保证了数据安全性。

基于python的坦克游戏的设计与实现

获取源码联系方式请查看文章结尾&#x1f345; 摘 要 随着互联网的日益普及、python语言在Internet上的实现&#xff0c;python应用程序产生的 互联网增值服务逐渐体现出其影响力&#xff0c;对丰富人们的生活内容、提供快捷的资讯起着不可忽视的作用。本论文中介绍了python的相…

梯度、偏导数、导数

梯度 对于一个多变量函数 f(x1,x2,…,xn)&#xff0c;其梯度 ∇f 是一个 n 维向量&#xff0c;定义为&#xff1a; ​ 是函数 f在 方向上的偏导数。 偏导数 偏导数是多元函数在某一个方向上的导数&#xff0c;它描述了函数在该方向上的局部变化率。偏导数的计算过程涉及对函…

ARR 竟然超过 150 万美元!斯坦福都在使用的 AI 学术搜索引擎 Consensus获 USV 领投的 1100 万美元。

惊爆&#xff01;就在当下&#xff0c;AI 学术搜索引擎 Consensus 传来令人震撼的消息&#xff0c;其已成功完成 1100 万美元融资。此轮 A 轮融资由 Union Square Ventures 领衔主导&#xff0c;其他参与的投资者有 Nat Friedman、Daniel Gross 以及 Draper Associates 等等。 …

springboot大学生时间管理分析系统---附源码130930

摘 要 时间是一种无形资源,但可以对其进行有效的使用与管理。时间管理倾向是个体在运用时间方式上所表现出来的心理和行为特征&#xff0c;具有多维度、多层次的心理结构&#xff0c;由时间价值感、时间监控观和时间效能感构成。时间是一种重要的资源,作为当代大学生,在进行生…

【UltraVNC】私有远程工具VNC机器部署方式

旨在解决监控端非固定IP的计算机A,远程连接受控的端非固定IP的计算机B。 一、UltraVNC下载和安装 官网:Home - UltraVNC VNC OFFICIAL SITE, Remote Desktop Free Opensource 二、部署私有的远程维护VNC机器-方式一 UltraVNC中继模式原理: UltraVNC中继模式部署: 1.1 中…

在ubuntu16.04下使用词典工具GoldenDict

前言 本来要装有道词典&#xff0c;结果发现各种问题&#xff0c;放弃。 网上看大家对GoldenDict评价比较高&#xff0c;决定安装GoldenDict 。 安装 启动 添加词库 GoldenDict本身并不带词库&#xff0c;需要查词的话&#xff0c;必须先下载离线词库或者配置在线翻译网址才…

安泰电压放大器的设计要求是什么样的

电压放大器的设计要求是一个广泛而复杂的领域&#xff0c;它在电子工程中扮演着至关重要的角色。电压放大器是一种电子电路&#xff0c;用于将输入信号的电压增大&#xff0c;而不改变其波形&#xff0c;通常用于放大微弱的信号以便进行后续处理或传输。下面将详细介绍电压放大…

【Mybatis-plus】Mybatis-plus的踩坑日记之速查版

【Mybatis-plus】Mybatis-plus踩坑日记之速查版 开篇词&#xff1a;干货篇&#xff1a;1.TableField(fill FieldFill.INSERT_UPDATE)的错误使用2.采用MybatisPlus自带update方法&#xff0c;但无法更新null的问题3.表字段为json类型的入库问题4.字段忽略未生效5.自带id生成策略…

RabbitMQ中消息的分发策略

我的后端学习大纲 RabbitMQ学习大纲 1.不公平分发&#xff1a; 1.1.什么是不公平分发&#xff1a; 1.在最开始的时候我们学习到 RabbitMQ 分发消息采用的轮训分发&#xff0c;但在某种场景下这种策略并不是很好&#xff0c;比方说有两个消费者在处理任务&#xff0c;其中有个…

基于vue全家桶的pc端仿淘宝系统_kebgy基于vue全家桶的pc端仿淘宝系统_kebgy--论文

TOC springboot478基于vue全家桶的pc端仿淘宝系统_kebgy基于vue全家桶的pc端仿淘宝系统_kebgy--论文 绪 论 1.1开发背景 改革开放以来&#xff0c;中国社会经济体系复苏&#xff0c;人们生活水平稳步提升&#xff0c;中国社会已全面步入小康社会。同时也在逐渐转型&#xf…

【中项第三版】系统集成项目管理工程师 | 第 15 章 组织保障

前言 本章的知识点预计上午会考1-2分&#xff0c;下午可能会考&#xff0c;一般与其他管理领域进行结合考查。学习要以教材为主。 目录 15.1 信息和文档管理 15.1.1 信息和文档 15.1.2 信息&#xff08;文档&#xff09;管理规则和方法 15.2 配置管理 15.2.1 基本概念 …

web渗透测试 学习导图

web渗透学习路线 前言 一、web渗透测试是什么&#xff1f; Web渗透测试分为白盒测试和黑盒测试&#xff0c;白盒测试是指目标网站的源码等信息的情况下对其渗透&#xff0c;相当于代码分析审计。而黑盒测试则是在对该网站系统信息不知情的情况下渗透&#xff0c;以下所说的Web…

测绘程序设计|初识C#编程语言|C#源码结构|面向对象|MFC、WinFrom与WPF

由于微信公众号改变了推送规则&#xff0c;为了每次新的推送可以在第一时间出现在您的订阅列表中&#xff0c;记得将本公众号设为星标或置顶喔~ 根据笔者经验&#xff0c;分享了C#编程语言、面向对象以及MFC、WinForm与WPF界面框架相关知识~ &#x1f33f;前言 c#作为测绘程序…

微信小程序SSL证书申请重点和方法

微信小程序运行模式主要在手机微信内&#xff0c;这一套程序可以解决了用户注册账户及支付相关问题&#xff0c;另外使用很方便&#xff0c;用户不用特意的去安装小程序&#xff0c;只要在微信里面就可以开发&#xff0c;只因为这样微信小程序很受欢迎。 对于开发者来说&#…