SpringSecurity多源认证之全部交给spring容器

文章目录

    • 一. 前言
    • 二. 配置流程
      • 2.1 SecurityConfig.class
      • 2.2 JwtAuthenticationTokenFilter
      • 2.3 AuthenticationManagerProcessingFilter
    • 疑问


一. 前言

相关文章:
认证/支付/优惠劵策略模式-security多源认证 这篇文章没有将自定义的认证管理器注入容器.

spring-security2.6.3+JWT认证授权 这篇文章描述了基本security架构.
如今这篇是全部交由spring security托管.但博主依然有一个问题不太清楚.放在文末.本篇文章基于认证/支付/优惠劵策略模式-security多源认证文章继续讲解


二. 配置流程

2.1 SecurityConfig.class

5.7版和旧版换汤不换药.我们以旧版继承WebSecurityConfigAdapter讲解.

/**
 * @Description: spring security 配置类
 */
@Configuration //配置类
// 开启注解 @PreAuthorize @PostAuthorize @Secured
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //    // 授权处理器
//    @Resource
//    private AccessDeniedHandlerImpl accessDeniedHandler;
    // jwt过滤器
    @Resource
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    // 手机号短信登录
    @Lazy
    @Resource
    private SmsCodeAuthenticationProvider smsCodeAuthenticationProvider;
    // 手机号账号登录
    @Lazy
    @Resource
    private MobileAccountAuthenticationProvider mobileAccountAuthenticationProvider;
    // 微信登录
    @Lazy
    @Resource
    private WeChatAuthenticationProvider weChatAuthenticationProvider;
    @Resource
    private DataSource dataSource;

    @Resource
    private AuthenticationManagerProcessingFilter authenticationManagerProcessingFilter;

    //把AuthenticationManager暴露到工厂里面,就可以在任何位置注入,如果想暴露一定要把这个方法给注入到容器中
    @Bean
    @Override
    //自定义登录过滤器交给工厂
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    //自定义AuthenticationManagerBuilder来选择自己的认证方式  推荐,AuthenticationManager并没有在工厂中暴露出来想使用得把她暴露之后再注入
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//        auth.userDetailsService(userDetailService); 不采用默认username认证方式
        auth
                .authenticationProvider(smsCodeAuthenticationProvider)
                .authenticationProvider(mobileAccountAuthenticationProvider)
                .authenticationProvider(weChatAuthenticationProvider);
    }

    // 配置拦截规则 也可以在AuthenticationManagerAuthenticationProcessingFilter中配置
//    private AuthenticationManagerProcessingFilter authenticationManagerProcessingFilter() throws Exception {
//        AuthenticationManagerProcessingFilter authenticationManagerProcessingFilter =
//                new AuthenticationManagerProcessingFilter();
//        //指定认证管理器
//        authenticationManagerProcessingFilter.setAuthenticationManager(authenticationManagerBean());
//        authenticationManagerProcessingFilter.setFilterProcessesUrl("/member/auth/v1/login");
//        //指定认证成功和失败处理
//        authenticationManagerProcessingFilter.setAuthenticationSuccessHandler(authenticationSuccessFilter);
//        authenticationManagerProcessingFilter.setAuthenticationFailureHandler(authenticationFailureFilter);
//        return authenticationManagerProcessingFilter;
//    }


    /**
     * anyRequest          |   匹配所有请求路径
     * access              |   SpringEl表达式结果为true时可以访问
     * anonymous           |   匿名可以访问
     * denyAll             |   用户不能访问
     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
     * permitAll           |   用户可以任意访问
     * rememberMe          |   允许通过remember-me登录的用户访问
     * authenticated       |   用户登录后可访问
     */
//    @Override
//    public void configure(WebSecurity web) {
//        // 放行路径,不走过滤器链
//        web.ignoring().antMatchers(
//                "/member/**");
//    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //获取工厂对象
        http
                .authorizeRequests()
                .mvcMatchers("/a/auth/**").permitAll()
                .mvcMatchers("/test/**").permitAll()
                .mvcMatchers("/a/book/v1/book/**").permitAll()
                .mvcMatchers("/b/user/v1/list/**").permitAll()
                .anyRequest().authenticated() // 除上述放行的url,其余全部鉴权认证
                .and()
                .cors()
                .and()
                .rememberMe()//记住我功能开启
                .rememberMeParameter("remember")//修改请求表单中的的记住我标签name属性
                .rememberMeServices(rememberMeServices())//指定remember的实现方式后端通过json
                .tokenRepository(persistentRememberMeToken())
                //用数据库的形式记住cookie
                .and()
                // 关闭csrf
                .csrf().disable();

//        http.exceptionHandling()
//                .accessDeniedHandler(accessDeniedHandler);
        http.addFilterBefore(authenticationManagerProcessingFilter,
                        UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(jwtAuthenticationTokenFilter,
                        AuthenticationManagerProcessingFilter.class);
    }

    //数据库实现记住我
    @Bean
    public PersistentTokenRepository persistentRememberMeToken() {
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        tokenRepository.setCreateTableOnStartup(false);
        return tokenRepository;
    }

    //自定义记住我操作主要是前后端分离使用
    @Bean
    public RememberMeServices rememberMeServices() {
        return new RememberConfig(UUID.randomUUID().toString(), userDetailsService(), persistentRememberMeToken());
    }

}

上述几个重要的配置

描述
authenticationManagerBean()暴露认证管理器对象.使其可以被其他类注入
passwordEncoder()使用BCryptPasswordEncoder加密
configure(AuthenticationManagerBuilder auth)自定义AuthenticationManagerBuilder来选择自己的认证方式或者配置自定义认证处理类
configure(HttpSecurity http)配置拦截路径以及过滤器顺序

2.2 JwtAuthenticationTokenFilter

JwtAuthenticationTokenFilter 第一个过滤器.用来判断用户是否登录

@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Resource
    private RedissonCache redisCache;

    @Resource
    private JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 重复可读request
        RepeatedlyRequestWrapper repeatedlyRequestWrapper =
                new RepeatedlyRequestWrapper(request, response);
        //获取token
        String token = repeatedlyRequestWrapper.getHeader(JwtUtil.JWT_HEADER);
        if (StringUtils.isBlank(token)) {
            //放行
            filterChain.doFilter(repeatedlyRequestWrapper, response);
            return;
        }
        //解析token
        token = token.replace(JwtUtil.JWT_TOKEN_PREFIX, "");
        Claims claims = null;
        String userId = null;
        try {
            claims = jwtUtil.parseJWT(token);
            //获取userId
            userId = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new BadCredentialsException(HttpCodeEnum.TOKEN_ERROR.getMsg());
        }
        //从redis中获取用户信息
        String redisKey = CacheConstants.LOGIN_TOKEN_KEY + userId;
        UserAuth userAuth = redisCache.getCacheObject(redisKey);
        if (Optional.ofNullable(userAuth).isEmpty()) {
            throw new UsernameNotFoundException(HttpCodeEnum.TOKEN_EXPIRED.getMsg());
        }
        // 用户信息续期
        redisCache.setCacheObject(redisKey, userAuth, SystemConstants.TOKEN_EXPIRE_TIME, TimeUnit.MILLISECONDS);
        //存入SecurityContextHolder   token都一样,但要区别认证方式
        MobileAccountAuthenticationToken mobileAccountAuthenticationToken =
                new MobileAccountAuthenticationToken(userAuth, userAuth.getAuthorities());
        SecurityContextHolder.getContext()
                .setAuthentication(mobileAccountAuthenticationToken);
        //放行
        filterChain.doFilter(repeatedlyRequestWrapper, response);
    }
}

2.3 AuthenticationManagerProcessingFilter

AuthenticationManagerProcessingFilter是最重要的多源认证接口.他拦截指定路径请求. 可以通过此处分配不同的认证处理

@Slf4j
@Component
public class AuthenticationManagerProcessingFilter extends AbstractAuthenticationProcessingFilter {
    private boolean postOnly = false;
    private final ObjectMapper objectMapper = JacksonSingleton.getInstance();
    @Resource
    private JwtUtil jwtUtil;
    @Resource
    private RedissonCache redissonCache;
    @Resource
    private AuthStrategyContent authStrategyContent;
    @Resource
    private SecurityThreadPoolConfig securityThreadPoolConfig;
    @Lazy
    @Autowired
    public AuthenticationManagerProcessingFilter(AuthenticationManager authenticationManager,
                                                 AuthenticationSuccessFilter authenticationSuccessFilter,
                                                 AuthenticationFailureFilter authenticationFailureFilter) {
        super(new AntPathRequestMatcher("/a/auth/v1/login", "POST"));

        //指定认证管理器
        this.setAuthenticationManager(authenticationManager);
//        this.setFilterProcessesUrl("/member/auth/v1/login");
        //指定认证成功和失败处理
        this.setAuthenticationSuccessHandler(authenticationSuccessFilter);
        this.setAuthenticationFailureHandler(authenticationFailureFilter);
    }

    /**
     * 使用相同的凭证执行身份验证
     *
     * @param request  从中提取参数并执行身份验证
     * @param response 如果实现必须将重定向作为多阶段身份验证过程的一部分(例如OpenID),则可能需要此响应。
     * @return 通过身份验证的用户。返回的对象必须是Authentication的实现
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException, IOException {
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
        // 获取用户信息
        UserLogin userLogin = getUserByRequest(request);
        if (userLogin == null) {
            throw new AuthenticationServiceException(HttpCodeEnum.BAD_REQUEST.getMsg() + "用户信息不能为空!");
        }
        // 验证用户信息
        Authentication authentication = verifyAuthInfo(userLogin);

        UserAuth userAuth = (UserAuth) authentication.getPrincipal();
        if (userAuth == null) {
            throw new AuthenticationServiceException(HttpCodeEnum.LOGIN_ERROR.getMsg());
        }

        // 登录成功后将密码置空
        userAuth.setPassword(null);
        // 统计登录次数
        loginCount(request, userAuth);
        //获取userId
        Long id = userAuth.getId();
        try {
            redissonCache.setCacheObject(CacheConstants.LOGIN_TOKEN_KEY + id,
                    userAuth,
                    jwtUtil.getExpire(),
                    TimeUnit.MILLISECONDS);
        } catch (Exception e) {
            log.error("redisson初始化失败", e);
            throw new AuthenticationServiceException(HttpCodeEnum.SYSTEM_ERROR.getMsg());
        }
        //通过后用userid生成一个jwt存入ResponseResult
        String jwt = jwtUtil.createJWT(String.valueOf(id));
        userAuth.setTokenHead(JwtUtil.JWT_TOKEN_PREFIX);
        userAuth.setToken(jwt);
        return authentication;
    }

    /**
     * 获取request中的json用户信息
     *
     * @param request 请求
     * @return 用户信息
     * @throws IOException IO异常
     */
    private UserLogin getUserByRequest(HttpServletRequest request) throws IOException {
        StringBuffer sb = new StringBuffer();
        InputStream is = request.getInputStream();
        InputStreamReader isr = new InputStreamReader(is);
        BufferedReader br = new BufferedReader(isr);
        String s = "";
        while ((s = br.readLine()) != null) {
            sb.append(s);
        }
        String userInfo = sb.toString();
        UserLogin userLogin = null;
        try {
            userLogin = objectMapper.readValue(userInfo, UserLogin.class);
        } catch (JsonProcessingException e) {
            log.info("json转换异常: {}", e.getMessage());
            throw new AuthenticationServiceException(HttpCodeEnum.BAD_REQUEST.getMsg() + "json转换异常");
        }
        return userLogin;
    }

    /**
     * 验证用户信息
     *
     * @param userLogin 用户信息
     * @return 认证信息
     */
    private Authentication verifyAuthInfo(UserLogin userLogin) {
        String authType = userLogin.getAuthType();
        // 认证策略模式
        AuthEnums enumerateInstances = AuthEnums.getEnum(authType);
        if (enumerateInstances == null) {
            throw new AuthenticationServiceException(HttpCodeEnum.BAD_REQUEST.getMsg() + "authType认证方式错误!");
        }
        return authStrategyContent.authType(enumerateInstances, userLogin, this.getAuthenticationManager());
    }

    /**
     * 登录次数统计
     */
    private void loginCount(HttpServletRequest request, UserAuth userAuth) {
        Executor asyncExecutor = securityThreadPoolConfig.getAsyncExecutor();
        CompletableFuture.runAsync(() -> {
            try {
                log.info("当前线程id: {},当前线程名称: {}",
                        Thread.currentThread().getId(),
                        Thread.currentThread().getName());
                // 登录次数统计 联网查询ip地址
                String ip = IpUtil.getIp(request);
                String ipAddr = String.valueOf(IpUtil.getIpAddr(ip));
                userAuth.setIpAddr(ipAddr);
                userAuth.setStatus(SystemLogConstants.USER_LOGIN_STATUS_VALUE);
                log.info("{}", objectMapper.writeValueAsString(userAuth));
            } catch (JsonProcessingException e) {
                log.error("登录次数统计异常: {}", e.getMessage());
            }
        }, asyncExecutor);
    }
}

上述代码种attemptAuthentication(HttpServletRequest request, HttpServletResponse response)方法就是执行认证的入口.
在这里插入图片描述
这部分也是将起注入容器的重要部分. 如图的策略模式认证流程可以在开头的第一篇文章看到

疑问

那么我的疑惑是 为什么@Lazy注解不能反过来标记
在这里插入图片描述
如果反过来
SecurityConfig.class
在这里插入图片描述
SecurityConfig.class
在这里插入图片描述
AuthenticationManagerProcessingFilter.class
在这里插入图片描述
这样就会报错
在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

CSS基础学习--7 fonts字体

一、CSS 字体 CSS字体属性定义字体系列,加粗,大小,文字样式。 二、字体系列 font-family 属性设置文本的字体系列 font-family 属性应该设置几个字体名称作为一种"后备"机制,如果浏览器不支持第一种字体,…

C++入门前必看,超详细

目录 前言 一.C的关键字 二.命名空间 2.1命名空间定义 2.2命名空间的使用 三.C的输入及输出 四.缺省参数 4.1概念 4.2缺省参数分类 4.3缺省参数的注意点 五.引用 5.1 概念 5.2引用的特性 六.内联函数 6.1概念 6.2内联函数的特性 七.auto 7.1auto概念 7.2auto的…

Unity入门5——Camera

一、参数面板 二、参数介绍 1. Clear Flags:清除背景 Skybox:天空盒背景(通常用来做 3D 游戏) Solid Color:使用 Background 颜色填充(通常设置为全黑或全白,2D 使用) Depth Only&am…

APP测试面试题快问快答(四)

16.App测试的实时日志如何获取? 考察点:是否有移动app测试的日志获取相关经验 一般可以通过以下方式获取: 1.可以使用adb命令:adb logcat | findstr "com.sankuai.meituan" >d:\test.txt 2.用ddms抓取&#xff0…

Postgresql源码(106)Generic Plan与Custom Plan的区别(以分区表为例)

相关: 《Postgresql源码(105)分区表剪枝代码分析》 《Postgresql源码(106)Generic Plan与Custom Plan的区别(以分区表为例)》 实例 CREATE TABLE measurement (city_id int not null,l…

FFmpeg音视频处理工具介绍及应用

1 FFmpeg介绍 FFmpeg项目由 Fabrice Bellard在2000年创立。到目前为止,FFmpeg项目的开发者仍然与VLC、MPV、dav1d、x264等多媒体开源项目有着广泛的重叠。Ffmpeg(FastForward Mpeg)是一款遵循GPL的开源软件,在音视频处理方面表现…

UDS关于0x37服务退出传输学习笔记

1.服务说明 客户端使用此服务来终止客户端和服务器之间的数据传输(上传或下载)。 2.请求消息 2.1请求消息子功能参数$Level(LEV_)定义 此服务不使用子函数参数。 2.2请求消息数据参数定义 transferRequestParameterRecord&a…

《微服务实战》 第二十九章 分布式事务框架seata AT模式

前言 本章节介绍微服务分布式项目中,使用的事务框架seata。 官网:http://seata.io/zh-cn/ springcloud-nacos-seata:https://github.com/seata/seata-samples/tree/master/springcloud-nacos-seata 1、概念 Seata 是一款开源的分布式事务解…

使用docker快速搭建redis哨兵模式

说明 本文主要参考: https://www.cnblogs.com/coderaniu/p/15352323.html https://developer.aliyun.com/article/892805 但是这两篇博客均缺失部分关键性细节,所以重新撰文。读者可以结合本文和上述文章一起阅读。 安装步骤 安装docker和docker-co…

小作文--流程图(练习1)

【【雅思写作】带你打破小作文‘流程图’的传说】 https://www.bilibili.com/video/BV1QP411Q7Gh/?share_sourcecopy_web&vd_source78768d4ae65c35ff26534bbaa8afc267 雅思小作文-流程图, 看这一篇就够了! - 冯凯文的文章 - 知乎 https://zhuanlan.zhihu.com/p/35868880 …

算法模板(3):搜索(3):图论提高

图论提高 最小生成树 (1)朴素版prim算法( O ( n 2 ) O(n ^ 2) O(n2)) 适用范围:稠密图易错:注意有向图还是无向图;注意有没有重边和负权边。从一个集合向外一个一个扩展,最开始只…

记录基于Vue.js的移动端Tree树形组件

目录 一、Liquor Tree 入门 : Development Component Options 组件选项 Structure 结构 二、vue-treeselect Introduction 介绍 Getting Started 入门 Vue 树形选择器( Vue tree select )组件在搭建 Vue 的 app 中特别常用&#xff0…

【Java|golang】2611. 老鼠和奶酪

有两只老鼠和 n 块不同类型的奶酪,每块奶酪都只能被其中一只老鼠吃掉。 下标为 i 处的奶酪被吃掉的得分为: 如果第一只老鼠吃掉,则得分为 reward1[i] 。 如果第二只老鼠吃掉,则得分为 reward2[i] 。 给你一个正整数数组 reward1…

【TypeScript】枚举类型和泛型的详细介绍

目录 TypeScript枚举类型 TypeScript泛型介绍 🎲泛型的基本使用 🎲泛型接口的使用 🎲泛型类的使用 🎲泛型的类型约束 枚举类型 枚举类型是为数不多的TypeScript特有的特性之一, JavaScript是没有的: 枚举其实就…

【FPGA零基础学习之旅#8】阻塞赋值与非阻塞赋值讲解

🎉欢迎来到FPGA专栏~阻塞赋值与非阻塞赋值 ☆* o(≧▽≦)o *☆嗨~我是小夏与酒🍹 ✨博客主页:小夏与酒的博客 🎈该系列文章专栏:FPGA学习之旅 文章作者技术和水平有限,如果文中出现错误,希望大家…

解决elementUI弹出框关闭后再打开el-select下拉框无法选中的问题

文章目录 一、问题描述:二、问题解决 一、问题描述: 使用的前端UI框架为elementUI。 el-select组件在一个弹框中,打开该弹框,el-select可以正常选中,但是保存弹框中的表单信息关闭弹框后,再打开弹框&…

Spark大数据处理讲课笔记4.1 Spark SQL概述、数据帧与数据集

零、本讲学习目标 了解Spark SQL的基本概念掌握DataFrame的基本概念掌握Dataset的基本概念会基于DataFrame执行SQL查询 在很多情况下,开发工程师并不了解Scala语言,也不了解Spark常用API,但又非常想要使用Spark框架提供的强大的数据分析能力…

物联网Lora模块从入门到精通(二) LED灯泡闪烁与呼吸灯

目录 一、前言 二、实践与代码 1.电亮LED1 2.熄灭LED1 3.翻转LED电平 4.LED1与LED2交替闪烁 5.LED1呼吸灯 三、程序代码 一、前言 本篇内容属于新大陆物联网Lora模块开发,使用给定的Lora基础例程,并在其基础上开发完成,并可为其他版本的Lo…

亚马逊云科技Serverless构建的实时数仓解决方案,助力猎豹降低30%成本

也许你也听过这样一句话:“21世纪什么最贵?人才!”当数字经济全面席卷而来,这个问题的答案不可置否地变为了“数据”。通过数据分析获取近乎实时的洞察,以驱动业务的全流程,是企业数字化转型的必经之路。借…

Linux操作系统学习——启动

概要 Linux操作系统内核是服务端学习的根基,也是提高编程能力、源码阅读能力和进阶知识学习能力的重要部分,本文开始将记录Linux操作系统中的各个部分源码学习历程。 1. 理解代码的组织结构 以Linux源码举例,首先你得知道操作系统分为哪几个部…
最新文章