【深入浅出Spring原理及实战】「开发实战系列」SpringSecurity原理以及实战认证分析开发指南

news2024/11/19 17:46:40

前提介绍

承接上一篇文章,相信大家应该已经对SpringSecurity的原理有了一定的认识,而本篇文章给大家带来的则是在实际业务开发中的技术指南,希望对你有所帮助。所谓知彼知己方能百战百胜,用Spring Security来满足我们的需求最好了解其原理,这样才能随意拓展。

过滤器的原理机制

Spring Security基本都是通过过滤器来完成配置的身份认证、权限认证以及登出。

Spring Security在Servlet的过滤链(filter chain)中注册了一个过滤器 FilterChainProxy,它会把请求代理到Spring Security自己维护的多个过滤链,每个过滤链会匹配一些URL,如果匹配则执行对应的过滤器,过滤链是有顺序的,一个请求只会执行第一条匹配的过滤链。

Spring Security 的配置本质上就是新增、删除、修改过滤器。

默认情况下系统帮我们注入的这15个过滤器,分别对应配置不同的需求。

认证控制过滤器UsernamePasswordAuthenticationFilter

接下来我们重点是分析下 UsernamePasswordAuthenticationFilter 这个过滤器,他是用来使用用户名和密码登录认证的过滤器,但是很多情况下我们的登录不止是简单的用户名和密码,又可能是用到第三方授权登录,这个时候我们就需要使用自定义过滤器,当然这里不做详细说明,只是说下自定义过滤器怎么注入。

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.addFilterAfter(...);
    ...
 }

身份认证流程

在开始身份认证流程之前我们需要了解下几个基本概念:

SecurityContextHolder

SecurityContextHolder存储SecurityContext对象。SecurityContextHolder是一个存储代理,有三种存储模式分别是:

  • MODE_THREADLOCAL:SecurityContext 存储在线程中。
  • MODE_INHERITABLETHREADLOCAL:SecurityContext 存储在线程中,但子线程可以获取到父线程中的 SecurityContext。
  • MODE_GLOBAL:SecurityContext 在所有线程中都相同。

SecurityContextHolder默认使用MODE_THREADLOCAL模式,SecurityContext 存储在当前线程中。

调用SecurityContextHolder时不需要显示的参数传递,在当前线程中可以直接获取到SecurityContextHolder对象。

SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();

Authentication

Authentication即验证,表明当前用户是谁。什么是验证,比如,一组用户名和密码就是验证,当然错误的用户名和密码也是验证,只不过 Spring Security会校验失败。

Authentication接口
public interface Authentication extends Principal, Serializable {
       Collection getAuthorities();
       Object getCredentials();
       Object getDetails();
       Object getPrincipal();
       boolean isAuthenticated();
       void setAuthenticated(boolean isAuthenticated);
}
  • getAuthorities():获取用户权限,一般情况下获取到的是用户的角色信息。

  • getCredentials():获取证明用户认证的信息,通常情况下获取到的是密码等信息,不过登录成功就会被移除。

  • getDetails():获取用户的额外信息,比如 IP 地址、经纬度等

  • getPrincipal():获取用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails (暂时理解为,当前应用用户对象的扩展)

  • isAuthenticated():获取当前 Authentication 是否已认证

  • setAuthenticated:设置当前 Authentication 是否已认证

AuthenticationManager ProviderManager AuthenticationProvider

其实这三者很好区分:

  • AuthenticationManager:主要就是为了完成身份认证流程。
  • ProviderManager:AuthenticationManager 接口的具体实现类
    • ProviderManager 里面有个记录AuthenticationProvider对象的集合属性 providers
  • AuthenticationProvider接口类里有两个方法
public interface AuthenticationProvider {
    //实现具体的身份认证逻辑,认证失败抛出对应的异常
    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;
    //该认证类是否支持该 Authentication 的认证
    boolean supports(Class authentication);
}

接下来就是遍历 ProviderManager 里面的 providers 集合,找到和合适的 AuthenticationProvider 完成身份认证。

UserDetailsService UserDetails

在 UserDetailsService 接口中只有一个简单的方法

public interface UserDetailsService {
    //根据用户名查到对应的 UserDetails 对象
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

执行认证流程

慢慢分析在运行到 UsernamePasswordAuthenticationFilter过滤器的时候。

首先是进入其父类AbstractAuthenticationProcessingFilter的doFilter()方法中

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)throws IOException, ServletException {
    ...
    //首先配对是不是配置的身份认证的URI,是则执行下面的认证,不是则跳过
    if (!requiresAuthentication(request, response)) {
        chain.doFilter(request, response);
        return;
    }
    ...
    Authentication authResult;
    try {
        //关键方法, 实现认证逻辑并返回 Authentication, 由其子类 UsernamePasswordAuthenticationFilter 实现
        authResult = attemptAuthentication(request, response);
        if (authResult == null) {
            // return immediately as subclass has indicated that it hasn't completed
            // authentication
            return;
        }
        sessionStrategy.onAuthentication(authResult, request, response);
    }
    catch (InternalAuthenticationServiceException failed) {
        //认证失败调用
        unsuccessfulAuthentication(request, response, failed);
        return;
   }catch (AuthenticationException failed) {
        //认证失败调用
        unsuccessfulAuthentication(request, response, failed);
        return;
    }
    // Authentication success
    if (continueChainBeforeSuccessfulAuthentication) {
        chain.doFilter(request, response);
    }
    //认证成功调
    successfulAuthentication(request, response, chain, authResult);
}

认证失败处理逻辑

protected void unsuccessfulAuthentication(HttpServletRequest request,HttpServletResponse response, AuthenticationException failed)throws IOException, ServletException {
    SecurityContextHolder.clearContext();
    ...
    rememberMeServices.loginFail(request, response);
    //该 handler 处理失败界面跳转和响应逻辑
    failureHandler.onAuthenticationFailure(request, response, failed);
}

默认配置的失败处理 handler是 SimpleUrlAuthenticationFailureHandler,可自定义。

public class SimpleUrlAuthenticationFailureHandler implements
        AuthenticationFailureHandler {
    ...
    public void onAuthenticationFailure(HttpServletRequest request,
            HttpServletResponse response, AuthenticationException exception)throws IOException, ServletException {
        //没有配置失败跳转的URL则直接响应错误
        if (defaultFailureUrl == null) {
            logger.debug("No failure URL set, sending 401 Unauthorized error");
            response.sendError(HttpStatus.UNAUTHORIZED.value(),
                HttpStatus.UNAUTHORIZED.getReasonPhrase());
        }else {
            //否则
            //缓存异常
            saveException(request, exception);
            //根据配置的异常页面是重定向还是转发进行不同方式跳转
            if (forwardToDestination) {
                logger.debug("Forwarding to " + defaultFailureUrl);
                request.getRequestDispatcher(defaultFailureUrl)
                        .forward(request, response);
            }else {
                logger.debug("Redirecting to " + defaultFailureUrl);
                redirectStrategy.sendRedirect(request, response, defaultFailureUrl);
            }
        }
    }
    //缓存异常,转发则保存在request里面,重定向则保存在session里面
    protected final void saveException(HttpServletRequest request,
            AuthenticationException exception) {
        if (forwardToDestination) {        
	    request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
        }else {
            HttpSession session = request.getSession(false);
            if (session != null || allowSessionCreation) {    
	       request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION,exception);
            }
        }
    }
}

这里做下小拓展:用系统的错误处理handler,指定认证失败跳转的URL,在MVC里面对应的URL方法里面可以通过key从request或session里面拿到错误信息,反馈给前端。

认证成功处理逻辑

protected void successfulAuthentication(HttpServletRequest request,
                                        HttpServletResponse response, FilterChain chain, Authentication authResult)
        throws IOException, ServletException {
    ...
    //这里要注意很重要,将认证完成返回的Authentication保存到线程对应的 `SecurityContext` 中
    SecurityContextHolder.getContext().setAuthentication(authResult);
    rememberMeServices.loginSuccess(request, response, authResult);
    // Fire event
    if (this.eventPublisher != null) {
        eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
    }
    //该 handler 就是为了完成页面跳转
    successHandler.onAuthenticationSuccess(request, response, authResult);
}

这里默认配置的成功处理 handler 是 SavedRequestAwareAuthenticationSuccessHandler,里面的代码就不做具体展开了,反正是跳转到指定的认证成功之后的界面,可自定义。

身份认证详情

public class UsernamePasswordAuthenticationFilter extends
        AbstractAuthenticationProcessingFilter {
    ...
    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
    private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
    private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
    private boolean postOnly = true;
    ...

    //开始身份认证逻辑
    public Authentication attemptAuthentication(HttpServletRequest request,
            HttpServletResponse response) throws AuthenticationException {
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
        String username = obtainUsername(request);
        String password = obtainPassword(request);
        if (username == null) {
            username = "";
        }
        if (password == null) {
            password = "";
        }
        username = username.trim();
        //先用前端提交过来的 username 和 password 封装一个简易的 AuthenticationToken
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                username, password);
        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);
        //具体的认证逻辑还是交给 AuthenticationManager 对象的 authenticate(..) 方法完成,接着往下看
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

由源码断点跟踪得知,最终解析是由 AuthenticationManager 接口实现类 ProviderManager 来完成

public class ProviderManager implements AuthenticationManager, MessageSourceAware,InitializingBean {
    ...
    private List providers = Collections.emptyList();
    ...
    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        ....
        //遍历所有的 AuthenticationProvider, 找到合适的完成身份验证
        for (AuthenticationProvider provider : getProviders()) {
            if (!provider.supports(toTest)) {
                continue;
            }
            ...
            try {
                //进行具体的身份验证逻辑, 这里使用到的是 DaoAuthenticationProvider, 具体逻辑记着往下看
                result = provider.authenticate(authentication);
                if (result != null) {
                    copyDetails(authentication, result);
                    break;
                }
            }catch
            ...
        }
        ...
        throw lastException;
    }
}

DaoAuthenticationProvider

DaoAuthenticationProvider继承自 AbstractUserDetailsAuthenticationProvider实现AuthenticationProvider接口

public abstract class AbstractUserDetailsAuthenticationProvider implements
        AuthenticationProvider, InitializingBean, MessageSourceAware {
    ...
    private UserDetailsChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks();
    private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks();
    ...
    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        ...
        // 获得提交过来的用户名
        String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
        //根据用户名从缓存中查找 UserDetails
        boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);
        if (user == null) {
            cacheWasUsed = false;
            try {
                //缓存中没有则通过 retrieveUser(..) 方法查找 (看下面 DaoAuthenticationProvider 的实现)
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
            }catch
            ...
        }
        try {
            //比对前的检查,例如账户以一些状态信息(是否锁定, 过期...)
            preAuthenticationChecks.check(user);
            //子类实现比对规则 (看下面 DaoAuthenticationProvider 的实现)
            additionalAuthenticationChecks(user,
                    (UsernamePasswordAuthenticationToken) authentication);
        }catch (AuthenticationException exception) {
            if (cacheWasUsed) {
                // There was a problem, so try again after checking
                // we're using latest data (i.e. not from the cache)
                cacheWasUsed = false;
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
                preAuthenticationChecks.check(user);
                additionalAuthenticationChecks(user,
                        (UsernamePasswordAuthenticationToken) authentication);
            }else {
                throw exception;
            }
        }
        postAuthenticationChecks.check(user);
        if (!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }
        Object principalToReturn = user;
        if (forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }
        //根据最终user的一些信息重新生成具体详细的 Authentication 对象并返回
        return createSuccessAuthentication(principalToReturn, authentication, user);
    }
    //具体生成还是看子类实现
    protected Authentication createSuccessAuthentication(Object principal,Authentication authentication, UserDetails user) {
        // Ensure we return the original credentials the user supplied,
        // so subsequent attempts are successful even with encoded passwords.
        // Also ensure we return the original getDetails(), so that future
        // authentication events after cache expiry contain the details
        UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
                principal, authentication.getCredentials(),
                authoritiesMapper.mapAuthorities(user.getAuthorities()));
        result.setDetails(authentication.getDetails());
        return result;
    }
}

接下来我们来看下 DaoAuthenticationProvider 里面的三个重要的方法,比对方式、获取需要比对的 UserDetails 对象以及生产最终返回 Authentication 的方法。

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
    ...
    //密码比对
    @SuppressWarnings("deprecation")
    protected void additionalAuthenticationChecks(UserDetails userDetails,UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            logger.debug("Authentication failed: no credentials provided");
            throw new BadCredentialsException(messages.getMessage(
 "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }
        String presentedPassword = authentication.getCredentials().toString();
        //通过 PasswordEncoder 进行密码比对, 注: 可自定义
        if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
            logger.debug("Authentication failed: password does not match stored value");
            throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }
    }
    //通过 UserDetailsService 获取 UserDetails
    protected final UserDetails retrieveUser(String username,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        prepareTimingAttackProtection();
        try {
            //通过 UserDetailsService 获取 UserDetails
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException(
                        "UserDetailsService returned null, which is an interface contract violation");
            }
            return loadedUser;
        }
        catch (UsernameNotFoundException ex) {
            mitigateAgainstTimingAttack(authentication);
            throw ex;
        }
        catch (InternalAuthenticationServiceException ex) {
            throw ex;
        }
        catch (Exception ex) {
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
        }
    }
    //生成身份认证通过后最终返回的 Authentication, 记录认证的身份信息
    @Override
    protected Authentication createSuccessAuthentication(Object principal,Authentication authentication, UserDetails user) {
        boolean upgradeEncoding = this.userDetailsPasswordService != null && this.passwordEncoder.upgradeEncoding(user.getPassword());
        if (upgradeEncoding) {
            String presentedPassword = authentication.getCredentials().toString();
            String newPassword = this.passwordEncoder.encode(presentedPassword);
            user = this.userDetailsPasswordService.updatePassword(user, newPassword);
        }
        return super.createSuccessAuthentication(principal, authentication, user);
    }
}

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

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

相关文章

我们国家都有哪些信息安全标准

写在前面 早年刚参加信息安全工作更多的学点皮毛技术,到处找安全工具,跟踪poc,拿到一个就全网扫一遍,从来没有想过,系统化的安全工作应该怎样搞?我做的工作在安全体系中处于哪个阶段? 后来有机会做企业安全建设&…

503.下一个更大元素II,42. 接雨水

503. 下一个更大元素 II 给定一个循环数组 nums ( nums[nums.length - 1] 的下一个元素是 nums[0] ),返回 nums 中每个元素的 下一个更大元素 。 数字 x 的 下一个更大的元素 是按数组遍历顺序,这个数字之后的第一个比它更大的数…

C++:vector操作笔记

本文记录了对C中的vector容器的简单操作,包括给vector添加元素、按指定列排序、取值。 文章目录一、vector 和普通数组之间的转化二、根据vector数组的某一列数据排序三、删除二维 vector 数组最后一列数据一、vector 和普通数组之间的转化 1,数组转vec…

Mysql日志体系

3、Mysql日志体系 bing log :在server记录mysql所有的DDL和DML语句,主要用来做主从复制和数据恢复。内容追加,没有固定大小限制。redo log :undo log : 3.1、bin log 3.1.1、binlog日志作用 binlog记录所有的DDL和…

Unity CEO:玩家不在乎AR还是VR,他们只想要优质内容

近期,Unity CEO John Riccitiello在接受英国金融时报采访时,透露了自己对于游戏的前生今世,以及AR/VR和未来的看法。他认为,打造一个新平台需要大量优质内容,创意对于新平台很重要,不管AR和VR技术有什么区别…

java医院挂号系统挂号预约网站挂号网站预约网站源码

主要实现门诊挂号和用户网站预约挂号的功能 演示视频: https://www.bilibili.com/video/BV1nN411d7HT/?share_sourcecopy_web&vd_sourceed0f04fbb713154db5cc611225d92156 环境:jdk7/8mysql5.5tomcat7/8 技术:ssm(springspringmvcmyi…

读书笔记---Bioinformatics Data Skills

最近阅读了《Bioinformatics Data Skills》,是不错的生物信息学入门书,本文作者简介如下: 全书以下面三个方向展开,为生物信息学的继续学习指明了道路: (1)生物信息学数据技巧(第一章…

动态DNS与DPDK高性能DNS -DPDK环境搭建

工具准备: vmware Ubuntu 16.04 Server 版本 1.vmware 添加两块网卡 桥接网卡作为 DPDK 运行的网卡 NAT 网卡作为 ssh 连接的网卡 2.修改网卡配置信息 将 ethernet0.virtualDev 由 e1000 修改 vmxnet3,因为 vmware 的 vmxnet3 支持多队 列网卡 3.修…

Qt对象树

对象模型(对象树) 类似于c中虚析构实现的功能一样,在释放父类对象的同时调用子类的析构函数释放子类对象 也c调用析构函数的区别是:会先调用父类的析构函数,然后一层层往下调用子类析构,直到调用到底层子…

10.1、Django框架简介、创建第一个应用

文章目录预备知识MVC模式和MTV模式MVC模式MTV 模式Django框架Django框架简介Django框架的应用启动后台admin站点管理数据库迁移创建管理员用户管理界面本地化创建并使用一个应用bookapp项目的数据库模型创建数据库模型生成数据库表数据库上的基本操作启用后台admin站点管理自定…

Vue框架--Ruoyi解析(前端)

路由router注册 router目录下的 index.js 配合 permission.js 是整套vue前端项目的权限判断核心. index.js 里面的path 配置都是一些不会与权限挂钩的路由,例如: 404、登录页面路径等. permission.js 中的 router.beforeEach 是路由拦截,在访问某个链接之前会进行权限判断,但是…

Mob教你汇总常见的用户注册和登录方式

目前市面上APP的常见的用户注册和登录方式有三种,第一种是最早期的手机号码密码登录,第二种是常见的 手机号码短信验证码,第三者是最新推出,且正被各大应用APP接入采用的" 一键登录"秒验方式。本文从运行方式、优势、劣…

【Leetcode】965. 单值二叉树、100. 相同的树、572. 另一棵树的子树

作者:一个喜欢猫咪的的程序员 专栏:《Leetcode》 喜欢的话:世间因为少年的挺身而出,而更加瑰丽。 ——《人民日报》 目录 965. 单值二叉树 100. 相同的树 572. 另一棵树的子树 965. 单值二叉树…

蓝桥杯,我劝你不要参加的8个完美理由

蓝桥杯,是一个全国高校的IT技术比拼,如果你参加了,可能不止是刷题数量的剧增,还有你的软件人生 我劝你不要参加,因为如果你参加了,可能会有以下烦恼: 目录 1、会让你变得上进 2、会提前感受码…

OpenCV inRange 函数使用详解

本文是 OpenCV图像视觉入门之路的第6篇文章,本人详细的解决了RGB转HSV,HSV通过AI来进行HSV转 inRange() 函数的范围值操作,简单全面的解决了OpenCV对于图像中某个颜色的分析工作,本文通过识别红色区域和蓝色区域来编写示例程序和博…

从交互设计师的角度,聊聊设计工具的那些事

工欲善其事,必先利其器,这句话是出自论语,讲的是要做好一件事,工具是非常重要的,作为一个设计师,设计工具对于我们的重要性毋庸置疑,每天都在接触,也有很多感悟和心得。 我从事设计…

羟基生物素(Biotin-OH),生物素引发剂(Biotin-Cl)

产品名称:羟基生物素,生物素引发剂 英文名称:Biotin-Cl,Biotin-OH 含biotin-DSPE的胰岛素脂质体 合成生物素一硬脂酥磷脂酥乙醇胺(biotin-DSPE),制备含 biotin-DSPE 的胰岛素脂质体,以粒径和包封率为指标 制备生物素修饰的胰岛素…

如何通过视频推广的方式帮助外贸B2B企业获得询盘

会有外贸企业比较烦恼,总是寻找不到精准客户,也很少有客户向自己询盘,企业需要订单,自己的品牌想要在目标市场被知晓,如何解决这些问题呢?我们可以跟随现阶段流行的趋势——视频推广,接下来请慢…

函数作图的技巧(高数)

前言 从初中开始,我们就开始用笔在平面直角坐标系上作函数图象。随着对函数研究的不断深入,对作出的函数图象的精准度的要求也越来越高。以往我们只需要描一下点,在将点连起来即可。但用这种方法的话偏差可能会很大,所以我们不妨…

数据仓库基础与Apache Hive入门

数据仓库基本概念 数据仓库,简称数仓,用于存储、分析、报告的数据系统。数据仓库的目的是构建面向分析的集成化数据环境,分析结果为企业提供决策支持。 数据仓库本身并不生产任何数据,其数据来源于不同的外部系统同时数据仓库自…