Spring security(四)-spring boot +spring security短信认证+redis整合

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 现在主流的登录方式主要有 3 种:账号密码登录、短信验证码登录和第三方授权登录,前面一节Spring security(三)---认证过程已分析了spring security账号密码方式登陆,现在我们来分析一下spring security短信方式认证登陆。

现在主流的登录方式主要有 3 种:账号密码登录、短信验证码登录和第三方授权登录,前面一节Spring security(三)---认证过程已分析了spring security账号密码方式登陆,现在我们来分析一下spring security短信方式认证登陆。


Spring security 短信方式、IP验证等类似模式登录方式验证,可以根据账号密码方式登录步骤仿写出来,其主要以以下步骤进行展开:


  1. 自定义Filter:
  2. 自定义Authentication
  3. 自定义AuthenticationProvider
  4. 自定义UserDetailsService
  5. SecurityConfig配置


1. 自定义filter:


自定义filter可以根据UsernamePasswordAuthenticationFilter过滤器进行仿写,其实质即实现AbstractAuthenticationProcessingFilter抽象类,主要流程分为:


  1. 构建构造器,并在构造器中进行配置请求路径以及请求方式的过滤
  2. 自定义attemptAuthentication()认证步骤
  3. 在2步骤中认证过程中需要AuthenticationProvider进行最终的认证,在认证filter都需要将AuthenticationProvider设置进filter中,而管理AuthenticationProvider的是AuthenticationManager,因此我们创建过滤器filter的时候需要设置AuthenticationManager,这步具体详情在5.1 SecurityConfig配置步骤


在第2步中attemptAuthentication()认证方法主要进行以下步骤:


1).post请求认证;

2).request请求获取手机号码和验证码;

3).用自定义的Authentication对象封装手机号码和验证码;

4).使用AuthenticationManager.authenticate()方法进行验证。


自定义filter实现代码:


public class SmsAuthenticationfilter extends AbstractAuthenticationProcessingFilter {
    private boolean postOnly = true;
    public SmsAuthenticationfilter() {
      super(new AntPathRequestMatcher(SecurityConstants.APP_MOBILE_LOGIN_URL, "POST"));
   }
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        if (postOnly && !request.getMethod().equals("POST")) {
              throw new AuthenticationServiceException(
                   "Authentication method not supported: " + request.getMethod());
      }
        Assert.hasText(SecurityConstants.MOBILE_NUMBER_PARAMETER, "mobile parameter must not be empty or null");
         String mobile = request.getParameter(SecurityConstants.MOBILE_NUMBER_PARAMETER);
        String smsCode = request.ge+tParameter(SecurityConstants.MOBILE_VERIFY_CODE_PARAMETER);
        if (mobile == null) {
            mobile="";
        }
        if(smsCode == null){
            smsCode="";
        }
        mobile = mobile.trim();
        smsCode = smsCode.trim();
        SmsAuthenticationToken authRequest = new SmsAuthenticationToken(mobile,smsCode);
        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }protected void setDetails(HttpServletRequest request,
                              SmsAuthenticationToken authRequest) {
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }
    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }
}
复制代码


2. Authentication:


在filter以及后面的认证都需要使用到自定义的Authentication对象,自定义Authentication对象可以根据UsernamePasswordAuthenticationToken进行仿写,实现AbstractAuthenticationToken抽象类。


自定义SmsAuthenticationToken:


public class SmsAuthenticationToken extends AbstractAuthenticationToken {
    private final Object principal;
    private Object credentials;
    public SmsAuthenticationToken(Object principal,Object credentials ) {
        super(null);
        this.principal = principal;
        this.credentials=credentials;
        setAuthenticated(false);
    }
    public SmsAuthenticationToken(Object principal, Object credentials,Collection<? extends GrantedAuthority> authorities) {
        super(null);
        this.principal = principal;
        this.credentials=credentials;
        setAuthenticated(true);
    }
    @Override
    public Object getCredentials() {
        return this.credentials=credentials;
    }
    @Override
    public Object getPrincipal() {
        return this.principal;
    }
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException(
                    "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        }
        super.setAuthenticated(false);
    }
    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
    }
}
复制代码


3.AuthenticationProvider


AuthenticationProvider最终认证策略入口,短信方式验证需自定义AuthenticationProvider。可以根据AbstractUserDetailsAuthenticationProvider进行仿写,实现AuthenticationProvider以及MessageSourceAware接口。认证逻辑可以定义实现。


自定义AuthenticationProvider:


public class SmsAuthenticationProvide implements AuthenticationProvider, MessageSourceAware {
  private UserDetailsService userDetailsService;
  private MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
    @Override
    public void setMessageSource(MessageSource messageSource) {
        this.messages = new MessageSourceAccessor(messageSource);
    }
    @Override
    public Authentication authenticate(Authentication authentication) {
        Assert.isInstanceOf(SmsAuthenticationToken.class, authentication,
                messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.onlySupports",
                        "Only UsernamePasswordAuthenticationToken is supported"));
        SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication;
        //将验证信息保存在SecurityContext以供UserDetailsService进行验证
        SecurityContext context = SecurityContextHolder.getContext();
        context.setAuthentication(authenticationToken);
        String mobile = (String) authenticationToken.getPrincipal();
        if (mobile == null) {
            throw new InternalAuthenticationServiceException("can't obtain user info ");
        }
        mobile = mobile.trim();
        //进行验证以及获取用户信息
        UserDetails user = userDetailsService.loadUserByUsername(mobile);
        if (user == null) {
            throw new InternalAuthenticationServiceException("can't obtain user info ");
        }
        SmsAuthenticationToken smsAuthenticationToken = new SmsAuthenticationToken(user, user.getAuthorities());
        return smsAuthenticationToken;
    }
    @Override
    public boolean supports(Class<?> authentication) {
        return (SmsAuthenticationToken.class.isAssignableFrom(authentication));
    }
    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
    public UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }
}
复制代码


4. UserDetailsService


在AuthenticationProvider最终认证策略入口,认证方式实现逻辑是在UserDetailsService。可以根据自己项目自定义认证逻辑。


自定义UserDetailsService:


public class SmsUserDetailsService implements UserDetailsService {
    @Autowired
    private RedisUtil redisUtil;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //从SecurityContext获取认证所需的信息(手机号码、验证码)
        SecurityContext context = SecurityContextHolder.getContext();
        SmsAuthenticationToken authentication = (SmsAuthenticationToken) context.getAuthentication();
        if(!additionalAuthenticationChecks(username,authentication)){
            return null;
        }
        //获取用户手机号码对应用户的信息,包括权限等
        return new User("admin", "123456", Arrays.asList(new SimpleGrantedAuthority("admin")));
    }
    public boolean additionalAuthenticationChecks(String mobile, SmsAuthenticationToken smsAuthenticationToken) {
        //获取redis中手机键值对应的value验证码
        String smsCode = redisUtil.get(mobile).toString();
        //获取用户提交的验证码
        String credentials = (String) smsAuthenticationToken.getCredentials();
        if(StringUtils.isEmpty(credentials)){
            return false;
        }
        if (credentials.equalsIgnoreCase(smsCode)) {
            return true;
        }
        return false;
    }
}
复制代码


5.SecurityConfig


5.1 自定义Sms短信验证组件配置SecurityConfig


将自定义组件配置SecurityConfig中,可以根据AbstractAuthenticationFilterConfigurer(子类FormLoginConfigurer)进行仿写SmsAuthenticationSecurityConfig,主要进行以下配置:


  1. 将默认AuthenticationManager(也可以定义的)设置到自定义的filter过滤器中
  2. 将自定义的UserDetailsService设置到自定义的AuthenticationProvide中以供使用
  3. 将过滤器添加到过滤链路中,实施过滤操作。(一般以加在UsernamePasswordAuthenticationFilter前)


配置SmsAuthenticationSecurityConfig:


@Component
 public class SmsAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    @Autowired
    private UserDetailsService userDetailsService;
    @Override
    public void configure(HttpSecurity http) throws Exception {
        //创建并配置好自定义SmsAuthenticationfilter,
        SmsAuthenticationfilter smsAuthenticationfilter = new SmsAuthenticationfilter();
        smsAuthenticationfilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        smsAuthenticationfilter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler());
        smsAuthenticationfilter.setAuthenticationFailureHandler(customAuthenticationFailureHandler());
        //创建并配置好自定义SmsAuthenticationProvide
        SmsAuthenticationProvide smsAuthenticationProvide=new SmsAuthenticationProvide();
        smsAuthenticationProvide.setUserDetailsService(userDetailsService);
        http.authenticationProvider(smsAuthenticationProvide);
        //将过滤器添加到过滤链路中
        http.addFilterAfter(smsAuthenticationfilter, UsernamePasswordAuthenticationFilter.class);
    }
    @Bean
    public CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler() {
        return new CustomAuthenticationSuccessHandler();
    }
    @Bean
    public CustomAuthenticationFailureHandler customAuthenticationFailureHandler() {
        return new CustomAuthenticationFailureHandler();
    }
}
复制代码


5.2 SecurityConfig主配置


SecurityConfig主配置可以参照第二节Spring Security(二)--WebSecurityConfigurer配置以及filter顺序进行配置。


SecurityConfig主配置:


@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private SmsAuthenticationSecurityConfig smsAuthenticationSecurityConfig;
    @Autowired
    private CustomAuthenticationSuccessHandler authenticationSuccessHandler;
    @Autowired
    private CustomAuthenticationFailureHandler authenticationFailureHandler;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.headers().frameOptions().disable().and()
                .formLogin()
                .loginPage(SecurityConstants.APP_FORM_LOGIN_PAGE)
                //配置form登陆的自定义URL
                .loginProcessingUrl(SecurityConstants.APP_FORM_LOGIN_URL)
                .successHandler(authenticationSuccessHandler)
                .failureHandler(authenticationFailureHandler)
                .and()
                //配置smsAuthenticationSecurityConfig
                .apply(smsAuthenticationSecurityConfig)
                .and()
                //运行通过URL
                .authorizeRequests()
                .antMatchers(SecurityConstants.APP_MOBILE_VERIFY_CODE_URL,
                             SecurityConstants.APP_USER_REGISTER_URL)
                .permitAll()
                .and()
                .csrf().disable();
    }
    @Bean
    public ObjectMapper objectMapper(){
        return new ObjectMapper();
    }
} 
复制代码


6.其他


6.1 redis


RedisUtil工具类:


@Component
public class RedisUtil {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    /**
     * 普通缓存获取
     *
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }
    /**
     * 普通缓存放入
     *
     * @param key   键
     * @param value 值
     * @return true成功 false失败
     */
    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 普通缓存放入并设置时间
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */
    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
 }
复制代码


redisConfig配置类:


@Configuration
public class RedisConfig {
@Autowired
private RedisProperties properties;
@Bean
@SuppressWarnings("all")
@ConditionalOnClass(RedisConnectionFactory.class)
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        template.setConnectionFactory(factory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
    @Bean
    @Qualifier("redisConnectionFactory")
    public RedisConnectionFactory redisConnectionFactory(){
        RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration();
        redisConfig.setHostName(properties.getHost());
        redisConfig.setPort(properties.getPort());
        redisConfig.setPassword(RedisPassword.of(properties.getPassword()));
        redisConfig.setDatabase(properties.getDatabase());
        //redis连接池数据设置
        JedisClientConfiguration.JedisClientConfigurationBuilder builder = JedisClientConfiguration.builder();
        if (this.properties.getTimeout() != null) {
            Duration timeout = this.properties.getTimeout();
            builder.readTimeout(timeout).connectTimeout(timeout);
        }
        RedisProperties.Pool pool = this.properties.getJedis().getPool();
        if (pool != null) {
            builder.usePooling().poolConfig(this.jedisPoolConfig(pool));
        }
        JedisClientConfiguration jedisClientConfiguration = builder.build();
        //根据两个配置类生成JedisConnectionFactory
        return new JedisConnectionFactory(redisConfig,jedisClientConfiguration);
    }
    private JedisPoolConfig jedisPoolConfig(RedisProperties.Pool pool) {
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(pool.getMaxActive());
        config.setMaxIdle(pool.getMaxIdle());
        config.setMinIdle(pool.getMinIdle());
        if (pool.getMaxWait() != null) {
            config.setMaxWaitMillis(pool.getMaxWait().toMillis());
        }
        return config;
    }
}
复制代码


7.总结


可以根据短信验证登陆模式去实现类似的验证方式,可以结合本节的例子进行跟项目结合起来,减少开发时间。后续还有第三方登陆方式分析以案例。最后错误请评论指出!


代码下载:demo:github.com/Ccww-lx/spr…


各位看官还可以吗?喜欢的话,动动手指点个💗,点个关注呗!!谢谢支持!



目录
相关文章
|
21天前
|
NoSQL Java 网络安全
SpringBoot启动时连接Redis报错:ERR This instance has cluster support disabled - 如何解决?
通过以上步骤一般可以解决由于配置不匹配造成的连接错误。在调试问题时,一定要确保服务端和客户端的Redis配置保持同步一致。这能够确保SpringBoot应用顺利连接到正确配置的Redis服务,无论是单机模式还是集群模式。
141 5
|
29天前
|
前端开发 Java 应用服务中间件
《深入理解Spring》 Spring Boot——约定优于配置的革命者
Spring Boot基于“约定优于配置”理念,通过自动配置、起步依赖、嵌入式容器和Actuator四大特性,简化Spring应用的开发与部署,提升效率,降低门槛,成为现代Java开发的事实标准。
|
29天前
|
前端开发 Java 微服务
《深入理解Spring》:Spring、Spring MVC与Spring Boot的深度解析
Spring Framework是Java生态的基石,提供IoC、AOP等核心功能;Spring MVC基于其构建,实现Web层MVC架构;Spring Boot则通过自动配置和内嵌服务器,极大简化了开发与部署。三者层层演进,Spring Boot并非替代,而是对前者的高效封装与增强,适用于微服务与快速开发,而深入理解Spring Framework有助于更好驾驭整体技术栈。
|
1月前
|
XML Java 应用服务中间件
【SpringBoot(一)】Spring的认知、容器功能讲解与自动装配原理的入门,带你熟悉Springboot中基本的注解使用
SpringBoot专栏开篇第一章,讲述认识SpringBoot、Bean容器功能的讲解、自动装配原理的入门,还有其他常用的Springboot注解!如果想要了解SpringBoot,那么就进来看看吧!
310 2
|
2月前
|
人工智能 Java 机器人
基于Spring AI Alibaba + Spring Boot + Ollama搭建本地AI对话机器人API
Spring AI Alibaba集成Ollama,基于Java构建本地大模型应用,支持流式对话、knife4j接口可视化,实现高隐私、免API密钥的离线AI服务。
1470 1
基于Spring AI Alibaba + Spring Boot + Ollama搭建本地AI对话机器人API
存储 JSON Java
441 0
|
2月前
|
NoSQL Java 调度
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
分布式锁是分布式系统中用于同步多节点访问共享资源的机制,防止并发操作带来的冲突。本文介绍了基于Spring Boot和Redis实现分布式锁的技术方案,涵盖锁的获取与释放、Redis配置、服务调度及多实例运行等内容,通过Docker Compose搭建环境,验证了锁的有效性与互斥特性。
169 0
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
|
2月前
|
人工智能 Java 开发者
【Spring】原理解析:Spring Boot 自动配置
Spring Boot通过“约定优于配置”的设计理念,自动检测项目依赖并根据这些依赖自动装配相应的Bean,从而解放开发者从繁琐的配置工作中解脱出来,专注于业务逻辑实现。
|
1月前
|
缓存 负载均衡 监控
135_负载均衡:Redis缓存 - 提高缓存命中率的配置与最佳实践
在现代大型语言模型(LLM)部署架构中,缓存系统扮演着至关重要的角色。随着LLM应用规模的不断扩大和用户需求的持续增长,如何构建高效、可靠的缓存架构成为系统性能优化的核心挑战。Redis作为业界领先的内存数据库,因其高性能、丰富的数据结构和灵活的配置选项,已成为LLM部署中首选的缓存解决方案。
|
1月前
|
缓存 运维 监控
Redis 7.0 高性能缓存架构设计与优化
🌟蒋星熠Jaxonic,技术宇宙中的星际旅人。深耕Redis 7.0高性能缓存架构,探索函数化编程、多层缓存、集群优化与分片消息系统,用代码在二进制星河中谱写极客诗篇。