SpringSecurity详解,实现自定义登录接口

news2024/9/23 13:26:50

目录

  • 1 SpringSecurity概述
    • 1.1 权限框架
      • 1.1.1 Apache Shiro
      • 1.1.2 SpringSecurity
    • 1.2 授权和认证
    • 1.3 SpringSecurity的功能
  • 2 认证原理及流程
    • 2.1 项目引入SpringSecurity
    • 2.2 认证流程详解
  • 3 自定义登录接口
    • 3.1 理论讲解
    • 3.2 代码实战
    • 3.3 接口测试

1 SpringSecurity概述

1.1 权限框架

目前市面上比较流行的权限框架主要实ShiroSpring Security,这两个框架各自侧重点不同,各有各的优劣。

1.1.1 Apache Shiro

Apache Shiro是一个开源安全框架,提供身份验证、授权、密码学和会话管理。Shiro框架直观、易用,同时也能提供健壮的安全性。

特点:

Shiro的特点:

  1. 易于理解的Java Security APl
  2. 简单的身份认证(登录),支持多种数据源(LDAP,JDBC,Kerberos,ActiveDirectory等)
  3. 对角色的简单的签权(访问控制),支持细粒度的签权
  4. 支持一级缓存,以提升应用程序的性能
  5. 内置的基于POJO企业会话管理,适用于Web 以及非 Web的环境
  6. 异构客户端会话访问
  7. 非常简单的加密API
  8. 不跟任何的框架或者容器捆绑,可以独立运行

1.1.2 SpringSecurity

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

Spring Security是Spring家族中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比shiro丰富。一般Web应用的需要进行认证和授权。而认证和授权也是SpringSecurity作为安全框架的核心功能。

SpringSecurity的特点:

  1. 与Spring Boot集成非常简单。
  2. 功能强大,高度可定制化。
  3. 支持OAuth2.0。
  4. 强大的加密ARI。
  5. 防止跨站请求伪造攻击(CSRF)。
  6. 提供Spring Cloud分布式组件。

1.2 授权和认证

一般来说,Web应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分,这两点也是SpringSecurity重要核心功能。

1)用户认证:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。

(2)用户授权:是验证某个用户是否有权限执行某个操作。经过认证后判断当前用户是否有权限进行某个操作。

RBAC (Role Based Access Control)基于角色的访问控制,通过抽象出“用户、角色、权限”"三个概念,实现用户分配角色,角色分配权限的权限管理方式,也是目前企业中权限管理主要实现方案。

案例如下图所示:

在这里插入图片描述

1.3 SpringSecurity的功能

Spring Security对Web安全性的支持大量地依赖于Servlet过滤器。这些过滤器拦截进入请求,并且在应用程序处理该请求之前进行某些安全处理。 Spring Security提供有若干个过滤器,它们能够拦截Servlet请求,并将这些请求转给认证和访问决策管理器处理,从而增强安全性。

如今的Spring Security已经成为Spring Framework下最成熟的安全系统,它为我们提供了强大而灵活的企业级安全服务,如:

  • 认证授权机制

  • Web资源访问控制

  • 业务方法调用访问控制

  • 领域对象访问控制Access Control List(ACL)

  • 单点登录(Central Authentication Service)

  • 信道安全(Channel Security)管理等功能

2 认证原理及流程

2.1 项目引入SpringSecurity

项目中单纯整合SpringSecurity很简单,先添加spring-boot-starter-security依赖

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>

引入依赖后我们在尝试访问系统的任何接口就会自动跳转到一个SpringSecurity的默认登陆页面,默认用户名是user,密码会输出在控制台。必须登陆之后才能对接口进行访问。

在这里插入图片描述

那是添加了该依赖如何就能实现对所有接口的拦截呢?

SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。我们看下一个简单的过滤器链:

在这里插入图片描述

UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。

ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException。

FilterSecurityInterceptor:负责权限校验的过滤器。

以上只是列举了主要的几个过滤器,详细流程如下。

2.2 认证流程详解

在这里插入图片描述

Authentication接口:它的实现类,表示当前访问系统的用户,封装了用户相关信息。

AuthenticationManager接口:定义了认证Authentication的方法。

UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法loadUserByUsername(String username)

UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。

可以看到整个过程过滤器UsernamePasswordAuthenticationFilter先拦截用户的请求,调用AuthenticationManager接口的authenticate方法将用户信息传递;调用AuthenticationProvider接口(AbstractUserDetailsAuthenticationProvider是它的实现类,DaoAuthenticationProvider是实现类的实现类);在调用userDetailsServiceloadUserByUsername()方法根据用户名查询对应用户和权限信息,默认走的是InMemoryUserDetailsManager实现类,及在内存中查找;将信息封装成对象一层层返回。

可以看到众多过滤器一层接一层。那我们如果要自己实现登录接口,从哪介入呢?

能介入最主要的地方一个是开头一个是UserDetailsService层。

3 自定义登录接口

3.1 理论讲解

要实现自定义登录,最主要是两个地方:

  • 要在自己写的页面手动调用登录接口同时还要进行附加操作。
  • UserDetailsService层不可能从内存中查询用户,需要跟实际的权限数据库关联。

对应的解决方法如下:

  • 编写登录接口,在其中调用ProviderManager.authenticate()方法。该接口需要再未登录的情况下可调用,所以要放开权限。
  • 编写个UserDetailsService的实现类,重写loadUserByUsername()方法,在改方法中根据传来的用户名去自己的权限数据库查询用户信息。封装到用户实体类中。

完整流程如下:

在前后端分离中我们一般采用token验证,先调用我们要自定义登录接口,通过自定义接口调用调用ProviderManager的方法进行认证 ,如果认证通过生成jwt,然后将jwt返回给前端,同时将用户信息包括用户权限信息等存入到redis数据库中,redis数据库作为缓存读取速度远远大于从数据库中进行读取用户信息。然后通过我们创建的UserDetailsServiceImpl实现类中去查询数据库。除此之外我们还需要定义JWT认证过滤器,从前端请求头中获取token,解析token获取其中的userid,然后根据userid从redis中获取用户信息存入SecurityContextHolder。具体流程如下图:

在这里插入图片描述

3.2 代码实战

  1. 定义一个UserDetails的实体类用来存放用户信息。

    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.SpringSecurityCoreVersion;
    import org.springframework.security.core.userdetails.UserDetails;
    
    import java.io.Serializable;
    import java.util.Collection;
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User implements UserDetails, Serializable {
        private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
    
        private static final Log logger = LogFactory.getLog(org.springframework.security.core.userdetails.User.class);
    
        private LoginUser loginUser;
    
    
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return null;
        }
    
        @Override
        public String getPassword() {
            return loginUser.getPassword();
        }
    
        @Override
        public String getUsername() {
            return loginUser.getUsername();
        }
    
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        @Override
        public boolean isEnabled() {
            return true;
        }
    }
    
  2. 自己定义UserDetailsService的实现类,重写loadUserByUsername方法,在其中查询自己的数据库。

    import com.project.business.userDetailsService.entity.User;
    import com.project.business.userDetailsService.mapper.UserMapper;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    
    import javax.annotation.Resource;
    
    public class UserDetailsServiceImpl implements UserDetailsService {
    
        @Resource
        private UserMapper userMapper;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            LoginUser loginUser = userMapper.loadUserByUsername(username);
            //查询不到该用户信息抛异常
            if(loginUser == null) {
                throw new RuntimeException("用户名或者密码错误");
            }
            User user = new User(loginUser);
            return user;
        }
    }
    
  3. 自定义登录接口

    我们需要自定义登陆接口,然后让SpringSecurity对这个接口放行,让用户访问这个接口的时候不用登录也能访问。

    在接口中我们通过AuthenticationManagerauthenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。认证成功的话要生成一个jwt即token,放入响应中返回。并且为了让用户下回请求时能通过jwt识别出具体的是哪个用户,我们需要把用户信息存入redis有效时间为30分钟,可以把用户id作为key。登录后在访问时,后台会根据token解析出用户的userid去redis中查询,判断用户是否登陆过。

    登录接口代码如下:

    @RestController
    @RequestMapping("/user")
    public class LoginController {
    
        @Autowired
        private RedisTemplate redisTemplate;
    
        @Autowired
        private AuthenticationManager authenticationManager;
    
        private static final String USER_PREFIX = "login:";
    
    
        @PostMapping("/login")
        public Result login(@RequestBody LoginUser loginUser) {
            //通过AuthenticationManager的authenticate方法来进行用户认证
            UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(loginUser.getUsername(), loginUser.getPassword());
            Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
            if (authenticate == null) {
                return Result.error(401, "登录校验失败");
            } else {
                //获取用户信息
                User user = (User) authenticate.getPrincipal();
                //获取用户id
                Long id = user.getLoginUser().getId();
                //根据用户id生成token
                String token = JwtUtil.createJWT(id.toString());
                //将token放在redis中
                redisTemplate.opsForValue().set(USER_PREFIX + String.valueOf(id),user,30, TimeUnit.MINUTES);
                return Result.ok("登录成功,返回token").put("token", token);
            }
        }
    }
    

    让Security放开user/login访问权限

    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
    
        }
    
        /**
         * @param http
         * @throws Exception
         */
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
    
            http
                    //关闭csrf
                    .csrf().disable()
                    //不通过Session获取SecurityContext
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                    .authorizeRequests()
                    // 对于登录接口 允许匿名访问
                    .antMatchers("/user/login").anonymous()
                    // 除上面外的所有请求全部需要鉴权认证
                    .anyRequest().authenticated();
        }
    }
    
  4. 后续访问接口token认证

    至此第一次登录就成功了,那之后调用接口怎么能判断是登陆过的不会再被springsecurity拦截了呢。

    思路:

    之后所有发送的请求,请求头中会带着token,需要自定义一个优先级靠前的过滤器,这个过滤器会去获取请求头中的token,对token进行解析取出其中的userid,去上一步存到的redis(或其他库)中查询,如果能查到代表之前登陆过了不需要再认证,封装Authentication对象存入SecurityContextHolder,之后springsecurity自己的过滤器会通过SecurityContextHolder获取信息判断是否登陆过;如果没查到说明没登录或登录过期,需要再次认证。

    过滤器

    @Component
    public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    
        @Resource
        private RedisTemplate redisTemplate;
    
        private static final String USER_PREFIX = "login:";
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            //获取token
            String token = request.getHeader("token");
            if (!StringUtils.hasText(token)) {
                //放行
                filterChain.doFilter(request, response);
                return;
            }
            //解析token
            String userid;
            try {
                Claims claims = JwtUtil.parseJWT(token);
                userid = claims.getSubject();
            } catch (Exception e) {
                e.printStackTrace();
                throw new RuntimeException("token非法");
            }
            //从redis中获取用户信息
            String redisKey = USER_PREFIX + userid;
            JSONObject jsonObject = (JSONObject) redisTemplate.opsForValue().get(redisKey);
            User user = JSONObject.parseObject(jsonObject.toJSONString(), User.class);
            if(Objects.isNull(user)){
                throw new RuntimeException("用户未登录");
            }
            //存入SecurityContextHolder
            //获取权限信息封装到Authentication中
            UsernamePasswordAuthenticationToken authenticationToken =
                    new UsernamePasswordAuthenticationToken(user,null,null);
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            //放行
            filterChain.doFilter(request, response);
        }
    }
    

    要把过滤器添加进配置,并且放到UsernamePasswordAuthenticationFilter前面,因为UsernamePasswordAuthenticationFilter过滤器是security过滤器链首位的,在他之前就要把用户状态存进SecurityContextHolder

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Resource
        private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            //此行要和上面配置文件中该方法中的内容写在一块,此处为了省略篇幅不都粘过来了
            http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        }
    }
    

3.3 接口测试

在调用user/login登录接口之前其他接口都访问不了,汇报403,Forbidden。此时调用登录接口。
在这里插入图片描述
在这里插入图片描述

可以看到,登录接口调用成功,返回一个根据userId生成的token,且redis中存储了登录用户的信息,30分钟过期。在这30分钟内,携带请求头token就可以正常访问其他接口。
在这里插入图片描述
更换加密方式

上述示例中采用的是明文方式比对。测试的时候在密码前加{noop}代表明文比对。

  1. 实际项目中一般不会在数据库中存储明文,会对其进行加密,就需要替换PasswordEncoder。

  2. 我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder

  3. 我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验。

  4. 我们可以在刚才创建的SecurityConfig配置类中注入BCryptPasswordEncoder对象。

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Bean
        public PasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    }
    

​ 可以先用其加密入库,再次登录依旧对比成功。
在这里插入图片描述

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

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

相关文章

【计算机视觉】相机

文章目录 一、原始的相机&#xff1a;针孔相机&#xff08;Pinhole Camera&#xff09;二、针孔相机的数学模型三、真实相机四、透镜的缺陷 我的《计算机视觉》系列参考UC Berkeley的CS180课程&#xff0c;PPT可以在课程主页看到。 成像原理 一、原始的相机&#xff1a;针孔相机…

C++11的std::function和bind绑定器

可调用对象 在C中&#xff0c;存在“可调用对象”这么一个概念。准确来说&#xff0c;可调用对象有如下几种定义&#xff1a; 1、是一个函数指针 2、是一个具有operator()成员函数的类对象&#xff08;仿函数&#xff09; 3、是一个可转换为函数指针的类对象 4、是一个类成员&a…

基于SSM和VUE的留守儿童信息管理系统

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;Vue 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是 目录…

数据链路层和DNS之间的那些事~

数据链路层&#xff0c;考虑的是两个节点之间的传输。这里面的典型协议也很多&#xff0c;最知名的就是“以太网”。我们本篇主要介绍的就是以太网协议。这个协议规定了数据链路层&#xff0c;也规定了物理层的内容。 目录 以太网帧格式 帧头 载荷 帧尾 DNS 从输入URL到…

中软国际:战略携手三大伙伴,三线出击收割AI红利

【科技明说 &#xff5c; 重磅专题】 2023年&#xff0c;当我看到中软国际成立AIGC研究院的消息后&#xff0c;认为基于解放号平台全面能力&#xff0c;去推进政企数智化服务&#xff0c;在很大程度上还是需要生态伙伴的技术力量。 为什么呢&#xff1f;这里简单说一下中软国际…

【Bug—eNSP】华为eNsp路由器设备启动一直是0解决方案!

问题描述 在上机实验时&#xff0c;打开ensp软件&#xff0c;添加AR设备时启动异常&#xff0c;最开始错误代码是40&#xff0c;最后通过重新安装&#xff0c;又出现了新的问题&#xff0c;启动AR设备一直是0&#xff0c;而且界面卡住。 解决方法 打开VirtualBox&#xff0c;将…

LeetCode刷题:88. 合并两个有序数组

文章目录 写在前面⭐️88. 合并两个有序数组⭐️&#x1f510;题目描述&#x1f4a1;解题思路&#x1f511;代码 写在前面 本题的题解代码是用C语言编写的。 &#x1f4d2;博客主页&#xff1a;2023Fighting的博客主页 &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收…

C#开发DLL,CAPL调用(CAPL>> .NET DLL)

文章目录 展示说明新建类库工程C# 代码生成dllCAPL脚本调用dll,输出结果展示 ret为dll里函数返回的值。 说明 新建类库工程 在visual studio中建立。 C# 代码 using

ruoyi中xxl-job配置使用

xxl-job配置使用目录 前言必须了解的概念1、调度中心2、执行器 环境搭建一、xxl-job环境搭建测试1、下载xxl-job源码2、执行SQL文件&#xff0c;生成对应表结构3、修改xxl-job-admin服务的 application.properties数据库地址可直接启动至此调度中心服务配置&#xff0c;启动完成…

软通动力:打造AI第二增长曲线,图谋新发展

【科技明说 &#xff5c; 重磅专题】 软通动力对于AI的想法还是比较久了&#xff0c;之前在与业内朋友聊到软通动力之时&#xff0c;曾提到软通动力的根基还是在于其多年来的软件服务能力&#xff0c;目前借助AI技术创新的机遇将软件服务能力进一步放大&#xff0c;扩展到更多行…

【C++初探:简单易懂的入门指南】一

【C初探&#xff1a;简单易懂的入门指南】一 1. 命名空间1.1 命名空间的定义1.2 命名空间的使用方法 2. C的输入、输出2.1 为什么使用输入、输出要引用一个<iostream>的头文件&#xff1f;2.2 为什么代码里面开放了一个叫std的命名空间2.3 代码中出现的<<和>>…

C++ 重载

C 允许在同一作用域中的某个函数和运算符指定多个定义&#xff0c;分别称为函数重载和运算符重载。 重载声明 是指一个与之前已经在该作用域内声明过的函数或方法具有相同名称的声明&#xff0c;但是它们的参数列表和定义&#xff08;实现&#xff09;不相同。 重载声明 当…

基于Spring AOP和CGLIB代理实现引介增强(Introduction Advice)示例

一、Spring AOP相关概念 1. Spring AOP与AspectJ区别 Spring AOP提供跨Spring IOC的简单AOP实现&#xff0c;以解决程序员面临的最常见问题。它并不打算作为一个完整的AOP解决方案——它只能应用于由Spring容器管理的bean。 AspectJ是最初的AOP技术&#xff0c;提供完整的AO…

Linux系统编程_网络编程:字节序、socket、serverclient、ftp 云盘

1. 网络编程概述&#xff08;444.1&#xff09; TCP/UDP对比 TCP 面向连接&#xff08;如打电话要先拨号建立连接&#xff09;&#xff1b;UDP 是无连接的&#xff0c;即发送数据之前不需要建立连接TCP 提供可靠的服务。也就是说&#xff0c;通过 TCP 连接传送的数据&#xf…

可以用商城源码做什么?

商城源码是一个基于电子商务流程的现代化的商城网站程序。它提供了一个完整的电子商务解决方案&#xff0c;包括产品目录、购物车、结账、订单和付款等功能。商城源码可以用于创建各种类型的电子商务网站&#xff0c; 在这篇文章中&#xff0c;我将介绍商城源码可能带来的各种…

【Python3】【力扣题】202. 快乐数

【力扣题】题目描述&#xff1a; 【Python3】代码&#xff1a; 1、解题思路&#xff1a;用哈希集合检测循环。设置集合记录每次结果&#xff0c;判断结果是否为1。若计算结果已在集合中则进入循环&#xff0c;结果一定不为1。 &#xff08;1-1&#xff09;知识点&#xff1a;…

day53--动态规划12

309.最佳买卖股票时机含冷冻期 714.买卖股票的最佳时机含手续费 第一题&#xff1a;.最佳买卖股票时机含冷冻期 给定一个整数数组&#xff0c;其中第 i 个元素代表了第 i 天的股票价格 。 设计一个算法计算出最大利润。在满足以下约束条件下&#xff0c;你可以尽可能地完…

电感基础复盘

1、在高速电路中&#xff0c;我们通常选用SMD贴片电阻&#xff0c;有薄膜和厚膜之分。 2、电容的性质主要为“充放电”和”隔直通交“。获得电荷为充电&#xff0c;反之为放电。隔离直流电不能通过电容器&#xff0c;⽽交流电能通过电容器。充电时直流电相当于导通&#xff0c;…

Java学习 5.习题2.

练习题1&#xff1a;判断一个数字是偶数还是奇数 int num110;if(num1%20) {System.out.println("num1是一个偶数");}else{System.out.println("num1是一个奇数");} 练习题2&#xff1a;判断一个数是正数还是负数还是0 int num2-5;if(num2>0) {System.ou…

【Apache Flink】基于时间和窗口的算子-配置时间特性

文章目录 前言配置时间特性将时间特性设置为事件时间时间戳分配器周期性水位线分配器创建一个实现AssignerWithPeriodicWatermarks接口的类&#xff0c;目的是为了周期性生成watermark 定点水位线分配器示例 参考文档 前言 Apache Flink 它提供了多种类型的时间和窗口概念&…