如何利用SpringSecurity进行认证与授权

2024-06-04 6111阅读

目录

一、SpringSecurity简介

1.1 入门Demo

二、认证

​编辑

2.1 SpringSecurity完整流程

2.2 认证流程详解

 2.3 自定义认证实现

2.3.1 数据库校验用户

2.3.2 密码加密存储

2.3.3 登录接口实现

2.3.4 认证过滤器

2.3.5 退出登录 

三、授权

3.1 权限系统作用

3.2 授权基本流程

3.3 授权实现

3.2.1 限制访问资源所需权限

3.2.2 封装权限信息

3.2.3 从数据库查询权限信息

3.2.3.1 RBAC权限模型

3.2.3.2 代码实现

 四、自定义失败处理

4.1 创建自定义实现类

4.2 将实现类配置给SpringSecurity

五、跨域问题解决方案 

六、其他权限校验方法 

七、自定义权限校验方法

八、基于配置的权限控制


一、SpringSecurity简介

Spring Security 是 Spring 家族中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。

一般来说中大型的项目都是使用SpringSecurity 来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。

一般Web应用的需要进行认证和授权。

  • 认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
  • 授权:经过认证后判断当前用户是否有权限进行某个操作

    而认证和授权也是SpringSecurity作为安全框架的核心功能。

    1.1 入门Demo

    依赖如下:

            
                org.springframework.boot
                spring-boot-starter-security
            

    引入依赖后我们在尝试去访问之前的接口就会自动跳转到一个SpringSecurity的默认登陆页面,默认用户名是user,密码会输出在控制台。

    如何利用SpringSecurity进行认证与授权 第1张

    必须登陆之后才能对接口进行访问。

    如何利用SpringSecurity进行认证与授权 第2张

    访问 localhost:8080/logout 这个链接可以 对其进行退出操作。

    如何利用SpringSecurity进行认证与授权 第3张

    Ps:

    以上过程了解即可,因为我们实际Web项目中,一般采用我们自定义的登录验证授权方案,不会采取SpringSecurity框架提供的默认方案。

    二、认证

    登录校验流程:

    如何利用SpringSecurity进行认证与授权 第4张

    为了实现以上这种过程,我们需要先对SpringSecurity默认的流程进行了解,才可以对其进行修改,实现我们自定义的方案。

    2.1 SpringSecurity完整流程

    SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。这里我们可以看看入门案例中的过滤器:

    如何利用SpringSecurity进行认证与授权 第5张

    图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示:

    • UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。
    • ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。
    • FilterSecurityInterceptor:负责权限校验的过滤器。我们可以通过Debug查看当前系统中SpringSecurity过滤器链中有哪些过滤器及它们的顺序。

      如果想查看所有的过滤器,可以通过获取Spring容器,Debug方式来查看:

      如何利用SpringSecurity进行认证与授权 第6张

      2.2 认证流程详解

      箭头代表该方法属于这个实现类的。 

      如何利用SpringSecurity进行认证与授权 第7张

      概念速查:

      • Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。
      • AuthenticationManager接口:定义了认证Authentication的方法
      • UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
      • UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。

         2.3 自定义认证实现

        登录

        ①自定义登录接口

        调用ProviderManager的方法进行认证 如果认证通过生成jwt 把用户信息存入redis中

        ②自定义UserDetailsService

        在这个实现类中去查询数据库

        校验

        ①定义Jwt 认证过滤器

        获取token 解析token获取其中的userid

        从redis中获取用户信息

        存入SecurityContextHolder

        如何利用SpringSecurity进行认证与授权 第8张

        这里为什么要存入 SecurityContextHolder中呢?

        我们自定义的JWT过滤器的时候,肯定是需要将这个JWT过滤器放在UsernamePasswordAuthenticationFilter前的,这时我们将从redis获取的用户信息存入SecurityContextHolder才行,否则后续过滤器在进行校验的时候,可能会因为SecurityContextHolder中没有对应的值而判断当前访问用户验证不通过。

        如何利用SpringSecurity进行认证与授权 第9张

        2.3.1 数据库校验用户

        定义Mapper接口

        package com.example.springsecurity_demo.mapper;
        import com.baomidou.mybatisplus.core.mapper.BaseMapper;
        import com.example.springsecurity_demo.domain.User;
        public interface UserMapper extends BaseMapper {
            
        }

        定义User实体类

        package com.example.springsecurity_demo.domain;
        import com.baomidou.mybatisplus.annotation.TableId;
        import com.baomidou.mybatisplus.annotation.TableName;
        import lombok.AllArgsConstructor;
        import lombok.Data;
        import lombok.NoArgsConstructor;
        import java.io.Serializable;
        import java.util.Date;
        @Data
        @AllArgsConstructor
        @NoArgsConstructor
        @TableName("sys_user")
        public class User implements Serializable {
            private static final long serialVersionUID = -40356785423868312L;
            
            /**
            * 主键
            */
            @TableId
            private Long id;
            /**
            * 用户名
            */
            private String userName;
            /**
            * 昵称
            */
            private String nickName;
            /**
            * 密码
            */
            private String password;
            /**
            * 账号状态(0正常 1停用)
            */
            private String status;
            /**
            * 邮箱
            */
            private String email;
            /**
            * 手机号
            */
            private String phonenumber;
            /**
            * 用户性别(0男,1女,2未知)
            */
            private String sex;
            /**
            * 头像
            */
            private String avatar;
            /**
            * 用户类型(0管理员,1普通用户)
            */
            private String userType;
            /**
            * 创建人的用户id
            */
            private Long createBy;
            /**
            * 创建时间
            */
            private Date createTime;
            /**
            * 更新人
            */
            private Long updateBy;
            /**
            * 更新时间
            */
            private Date updateTime;
            /**
            * 删除标志(0代表未删除,1代表已删除)
            */
            private Integer delFlag;
        }

        配置Mapper扫描

        package com.example.springsecurity_demo;
        import org.mybatis.spring.annotation.MapperScan;
        import org.springframework.boot.SpringApplication;
        import org.springframework.boot.autoconfigure.SpringBootApplication;
        import org.springframework.context.ConfigurableApplicationContext;
        @SpringBootApplication
        @MapperScan("com.example.springsecurity_demo.mapper")
        public class SpringSecurityDemoApplication {
            public static void main(String[] args) {
                ConfigurableApplicationContext run = SpringApplication.run(SpringSecurityDemoApplication.class, args);
                System.out.println(1);
            }
        }
        

        核心代码实现

        package com.example.springsecurity_demo.service.impl;
        import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
        import com.example.springsecurity_demo.domain.LoginUser;
        import com.example.springsecurity_demo.domain.User;
        import com.example.springsecurity_demo.mapper.UserMapper;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.security.core.userdetails.UserDetails;
        import org.springframework.security.core.userdetails.UserDetailsService;
        import org.springframework.security.core.userdetails.UsernameNotFoundException;
        import org.springframework.stereotype.Service;
        import java.util.Objects;
        @Service
        public class UserDetailServiceImpl implements UserDetailsService {
            @Autowired
            private UserMapper userMapper;
            @Override
            public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                // 查询用户信息
                LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper();
                queryWrapper.eq(User::getUserName,username);
                User user = userMapper.selectOne(queryWrapper);
                // 如果没有查询到用户就抛出异常
                if (Objects.isNull(user)) {
                    throw new RuntimeException("用户名或者密码错误");
                }
                //TODO 查询对应的权限信息
                return new LoginUser(user);
            }
        }
        

        因为UserDetailsService方法的返回值是UserDetails(接口):

        如何利用SpringSecurity进行认证与授权 第10张

        所以需要定义一个类,实现该接口,把用户信息封装在其中。

        package com.example.springsecurity_demo.domain;
        import lombok.AllArgsConstructor;
        import lombok.Data;
        import lombok.NoArgsConstructor;
        import org.springframework.security.core.GrantedAuthority;
        import org.springframework.security.core.userdetails.UserDetails;
        import java.util.Collection;
        @Data
        @NoArgsConstructor
        @AllArgsConstructor
        public class LoginUser implements UserDetails {
            private User user;
            @Override
            public Collection
        
        
            
                SELECT
                    DISTINCT m.`perms`
                FROM
                    sys_user_role ur
                    LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`
                    LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`
                    LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`
                WHERE
                    user_id = #{userid}
                    AND r.`status` = 0
                    AND m.`status` = 0
            
        

        然后我们可以在UserDetailsServiceImpl中去调用该mapper的方法查询权限信息封装到LoginUser对象中即可:

        import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
        import com.sangeng.domain.LoginUser;
        import com.sangeng.domain.User;
        import com.sangeng.mapper.MenuMapper;
        import com.sangeng.mapper.UserMapper;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.security.core.userdetails.UserDetails;
        import org.springframework.security.core.userdetails.UserDetailsService;
        import org.springframework.security.core.userdetails.UsernameNotFoundException;
        import org.springframework.stereotype.Service;
        import java.util.ArrayList;
        import java.util.Arrays;
        import java.util.List;
        import java.util.Objects;
        @Service
        public class UserDetailsServiceImpl implements UserDetailsService {
            @Autowired
            private UserMapper userMapper;
            @Autowired
            private MenuMapper menuMapper;
            @Override
            public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                //查询用户信息
                LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper();
                queryWrapper.eq(User::getUserName,username);
                User user = userMapper.selectOne(queryWrapper);
                // 如果没有查询到用户就抛出异常
                if(Objects.isNull(user)){
                    throw new RuntimeException("用户名或者密码错误");
                }
        //        List list = new ArrayList(Arrays.asList("test","admin"));
                List list = menuMapper.selectPermsByUserId(user.getId());
                //把数据封装成UserDetails返回
                return new LoginUser(user,list);
            }
        }
        

         四、自定义失败处理

        我们还希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json,这样可以让前端能对响应进行统一的处理。要实现这个功能我们需要知道SpringSecurity的异常处理机制。

        在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。

        如果是认证过程中出现的异常会被封装成AuthenticationException然后调用

        AuthenticationEntryPoint对象的方法去进行异常处理。

        如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。

        所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和

        AccessDeniedHandler然后配置给SpringSecurity即可。

        4.1 创建自定义实现类

        也就是说,我们只需要创建一个自定义的实现类然后分别去实现AccessDeniedHandler接口和AccessDeniedHandler接口即可,代码如下。

        认证失败自定义实现类如下:

        import com.alibaba.fastjson.JSON;
        import com.sangeng.domain.ResponseResult;
        import com.sangeng.utils.WebUtils;
        import org.springframework.http.HttpStatus;
        import org.springframework.security.core.AuthenticationException;
        import org.springframework.security.web.AuthenticationEntryPoint;
        import org.springframework.stereotype.Component;
        import javax.servlet.ServletException;
        import javax.servlet.http.HttpServletRequest;
        import javax.servlet.http.HttpServletResponse;
        import java.io.IOException;
        @Component
        public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
            @Override
            public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
                ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(),"用户认证失败请查询登录");
                String json = JSON.toJSONString(result);
                //处理异常
                WebUtils.renderString(response,json);
            }
        }
        

         授权失败自定义实现类如下:

        import com.alibaba.fastjson.JSON;
        import com.sangeng.domain.ResponseResult;
        import com.sangeng.utils.WebUtils;
        import org.springframework.http.HttpStatus;
        import org.springframework.security.access.AccessDeniedException;
        import org.springframework.security.web.access.AccessDeniedHandler;
        import org.springframework.stereotype.Component;
        import javax.servlet.ServletException;
        import javax.servlet.http.HttpServletRequest;
        import javax.servlet.http.HttpServletResponse;
        import java.io.IOException;
        @Component
        public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
            @Override
            public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
                ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(),"您的权限不足");
                String json = JSON.toJSONString(result);
                //处理异常
                WebUtils.renderString(response,json);
            }
        }
        

         涉及到的工具类如下:

        由于response对象是较为原生的,所以我们需要进行书写状态码,ContentType等。所以我们需要使用工具类对其进行修改。

        import javax.servlet.http.HttpServletResponse;
        import java.io.IOException;
        public class WebUtils
        {
            /**
             * 将字符串渲染到客户端
             * 
             * @param response 渲染对象
             * @param string 待渲染的字符串
             * @return null
             */
            public static String renderString(HttpServletResponse response, String string) {
                try
                {
                    response.setStatus(200);
                    response.setContentType("application/json");
                    response.setCharacterEncoding("utf-8");
                    response.getWriter().print(string);
                }
                catch (IOException e)
                {
                    e.printStackTrace();
                }
                return null;
            }
        }

        4.2 将实现类配置给SpringSecurity

         注入对应处理器:

        如何利用SpringSecurity进行认证与授权 第11张

         然后我们可以使用HttpSecurity对象的方法去配置:

        如何利用SpringSecurity进行认证与授权 第12张

        配合类代码如下:

        import com.sangeng.filter.JwtAuthenticationTokenFilter;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.context.annotation.Bean;
        import org.springframework.context.annotation.Configuration;
        import org.springframework.http.HttpMethod;
        import org.springframework.security.authentication.AuthenticationManager;
        import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
        import org.springframework.security.config.annotation.web.builders.HttpSecurity;
        import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
        import org.springframework.security.config.http.SessionCreationPolicy;
        import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
        import org.springframework.security.crypto.password.PasswordEncoder;
        import org.springframework.security.web.AuthenticationEntryPoint;
        import org.springframework.security.web.access.AccessDeniedHandler;
        import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
        @Configuration
        @EnableGlobalMethodSecurity(prePostEnabled = true)
        public class SecurityConfig extends WebSecurityConfigurerAdapter {
            //创建BCryptPasswordEncoder注入容器
            @Bean
            public PasswordEncoder passwordEncoder(){
                return new BCryptPasswordEncoder();
            }
            @Autowired
            private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
            @Autowired
            private AuthenticationEntryPoint authenticationEntryPoint;
            @Autowired
            private AccessDeniedHandler accessDeniedHandler;
            @Override
            protected void configure(HttpSecurity http) throws Exception {
                http
                        //关闭csrf
                        .csrf().disable()
                        //不通过Session获取SecurityContext
                        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                        .and()
                        .authorizeRequests()
                        // 对于登录接口 允许匿名访问
                        .antMatchers("/user/login").anonymous()
        //                .antMatchers("/testCors").hasAuthority("system:dept:list222")
                        // 除上面外的所有请求全部需要鉴权认证
                        .anyRequest().authenticated();
                //添加过滤器
                http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
                //配置异常处理器
                http.exceptionHandling()
                        //配置认证失败处理器
                        .authenticationEntryPoint(authenticationEntryPoint)
                        .accessDeniedHandler(accessDeniedHandler);
                //允许跨域
                http.cors();
            }
            @Bean
            @Override
            public AuthenticationManager authenticationManagerBean() throws Exception {
                return super.authenticationManagerBean();
            }
        }
        

        五、跨域问题解决方案 

        浏览器出于安全的考虑,使用 XMLHttpRequest对象发起 HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求,默认情况下是被禁止的。 同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。

        前后端分离项目,前端项目和后端项目一般都不是同源的,所以肯定会存在跨域请求的问题。

        所以我们就要处理一下,让前端能进行跨域请求。

        ①先对SpringBoot配置,运行跨域请求

        import org.springframework.context.annotation.Configuration;
        import org.springframework.web.servlet.config.annotation.CorsRegistry;
        import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
        @Configuration
        public class CorsConfig implements WebMvcConfigurer {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
              // 设置允许跨域的路径
                registry.addMapping("/**")
                        // 设置允许跨域请求的域名
                        .allowedOriginPatterns("*")
                        // 是否允许cookie
                        .allowCredentials(true)
                        // 设置允许的请求方式
                        .allowedMethods("GET", "POST", "DELETE", "PUT")
                        // 设置允许的header属性
                        .allowedHeaders("*")
                        // 跨域允许时间
                        .maxAge(3600);
            }
        }

        ②开启SpringSecurity的跨域访问

        由于我们的资源都会收到SpringSecurity的保护,所以想要跨域访问还要让SpringSecurity运行跨域访问。

        如何利用SpringSecurity进行认证与授权 第13张

        六、其他权限校验方法 

        我们前面都是使用@PreAuthorize注解,然后在在其中使用的是hasAuthority方法进行校验。

        SpringSecurity还为我们提供了其它方法例如:hasAnyAuthority,hasRole,hasAnyRole等。

        这里我们先不急着去介绍这些方法,我们先去理解hasAuthority的原理,然后再去学习其他方法你就更容易理解,而不是***记硬背区别。并且我们也可以选择定义校验方法,实现我们自己的校验逻辑。

        hasAuthority方法实际是执行到了SecurityExpressionRoot的hasAuthority,大家只要断点调试既可知道它内部的校验原理。

        它内部其实是调用authentication的getAuthorities方法获取用户的权限列表。然后判断我们存入的方法参数数据在权限列表中。

        hasAnyAuthority方法可以传入多个权限,只有用户有其中任意一个权限都可以访问对应资源。

            @RequestMapping("/hello")
            @PreAuthorize("hasAnyAuthority('admin','test','system:dept:list')")
            public String hello(){
                return "hello";
            }

        hasRole要求有对应的角色才可以访问,但是它内部会把我们传入的参数拼接上 ROLE_ 后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_ 这个前缀才可以。

            @RequestMapping("/hello")
            @PreAuthorize("hasRole('system:dept:list')")
            public String hello(){
                return "hello";
            }

        hasAnyRole 有任意的角色就可以访问。它内部也会把我们传入的参数拼接上 ROLE_ 后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_ 这个前缀才可以。

            @RequestMapping("/hello")
            @PreAuthorize("hasAnyRole('admin','system:dept:list')")
            public String hello(){
                return "hello";
            }

        七、自定义权限校验方法

        我们也可以定义自己的权限校验方法,在@PreAuthorize注解中使用我们的方法。

         

        import com.fox.domain.LoginUser;
        import org.springframework.security.core.Authentication;
        import org.springframework.security.core.context.SecurityContextHolder;
        import org.springframework.stereotype.Component;
        import java.util.List;
        @Component("ex")
        public class FoxExpressionRoot {
            public boolean hasAuthority(String authority){
                //获取当前用户的权限
                Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
                LoginUser loginUser = (LoginUser) authentication.getPrincipal();
                List permissions = loginUser.getPermissions();
                //判断用户权限集合中是否存在authority
                return permissions.contains(authority);
            }
        }
        

        在SPEL表达式中使用 @ex相当于获取容器中bean的名字未ex的对象。然后再调用这个对象的

        hasAuthority方法:

            @RequestMapping("/hello")
            @PreAuthorize("@ex.hasAuthority('system:dept:list')")
            public String hello(){
                return "hello";
            }

        八、基于配置的权限控制

        我们也可以在配置类中使用使用配置的方式对资源进行权限控制。

        如何利用SpringSecurity进行认证与授权 第14张

         


    免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们,邮箱:ciyunidc@ciyunshuju.com。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!

    目录[+]