SpringSecurity6.x整合手机短信登录授权

前言:如果没有看过我的这篇文章的Springboot3.x.x使用SpringSecurity6(一文包搞定)_springboot3整合springsecurity6-CSDN博客需要看下,大部分多是基于这篇文章的基础上实现的。

明确点我们的业务流程:

  1. 需要有一个发送短信的接口,可结合阿里云的sms
  2. 需要一个短信登录接口,参数为手机号和验证码
  3. 结合到Security做用户认证 
  4. 最终返回token

了解业务流程后我们,就可以开始动手了!

验证码接口

验证码这里我只展示业务逻辑,后续会发文字专门说明Springboot如何调用阿里的SMS发现短信。 

控制层

   /**
     * 获取手机验证码
     * @return
     */
    @Operation(summary = "获取手机验证码")
    @GetMapping("/sendCode")
    public Result sendCode(@RequestParam(name = "phone") String phone){
    return userService.sendCode(phone);
    }

impl

   @Override
    public Result sendCode(String phone) {
        //采用正则校验手机号
        boolean phoneInvalid = RegexUtils.isPhoneInvalid(phone);
        if (phoneInvalid) {
            return Result.fail(500, "手机格式错误");
        }
        //设置校验60秒
        if (stringRedisTemplate.hasKey(RedisConstants.PHONE_CHECK_KEY + phone)) {
            Long expire = stringRedisTemplate.getExpire(RedisConstants.PHONE_CHECK_KEY + phone, TimeUnit.SECONDS);
            return Result.fail(500, "请耐心等待" + expire + "秒");
        }
        //采用工具类生成随机验证码
        int number = RandomUtil.randomInt(100000, 999999);
        String code = String.valueOf(number);
        //TODO 后续改为第三方短信平台
        try {
            smsUtils.sendSms(phone, code);
            stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone, code, RedisConstants.LOGIN_CODE_TTL, TimeUnit.SECONDS);
//            设置倒计时间
            stringRedisTemplate.opsForValue().set(RedisConstants.PHONE_CHECK_KEY + phone, code, RedisConstants.PHONE_CHECK_TTL, TimeUnit.SECONDS);
            log.info("验证码:{}", code);
        } catch (Exception e) {
            e.printStackTrace();
            return Result.fail(500, "发送失败,请联系管理员~");
        }
        return Result.success("请在五分钟内输入验证码~");
    }

上面的我们将验证码存到redis且设置了时效,那么业务的第一不我们就完成了。

短信登陆接口

   @Operation(summary = "手机号登录接口")
    @PostMapping("/phoneLogin")
    public Result phoneLogin(@RequestParam(name = "phone")String phone,
                             @RequestParam(name = "code")String code
                             ){
        return userService.phoneLogin(new LoginDto().setPhone(phone).setCode(code));
    }

这里为了在Security好取参数我们这里用Param传参数。实现类我们直接返回成功即可,因为结合Security后请求不会进到方法体里面。为什么呢?我们后面来解释。

问题1:为什么手机短信接口结合Security后不会进度lmpl层方法体里面呢?

结合Security实现认证

在这里之前我们需要了解Security里面的几个类:

  1. UsernamePasswordAuthenticationToken
  2. UsernamePasswordAuthenticationFilter
  3. AuthenticationProvider

1.UsernamePasswordAuthenticationToken

UsernamePasswordAuthenticationToken首先我们需要了解这哥们是干嘛用的,我们看下官网是怎么解释的表单登录(Form Login) :: Spring Security Reference

a86ec7278b674e748906d06154a52e90.png

 上图中他说会从UsernamePasswordAuthenticationFilter这个过滤器中去拿账号密码,给UsernamePasswordAuthenticationToken 那么我们就可以猜测下这东西应该是存的是账号和密码有点相当于实体。

6e71e22a1fc74234b1ed476402ae2421.png

我们来看下源码:

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	private final Object principal;

	private Object credentials;
        
/**
*任何希望创建构造函数的代码都可以安全地使用此构造函数
*<code>用户名密码认证令牌</code>,作为{@link#isAuthenticated()}
*将返回<code>false</code>。
*
*/
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
		super(null);
		this.principal = principal;
		this.credentials = credentials;
		setAuthenticated(false);
	}

/**
*此构造函数只能由<code>AuthenticationManager</code>或
*<code>AuthenticationProvider</code>满足以下条件的实现
*生成一个可信的(即{@link#isAuthenticated()}=<code>true</code>)
*身份验证令牌。
*@param主体
*@param凭据
*@param权限
*/
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
			Collection<? extends GrantedAuthority> authorities) {
		super(authorities);
		this.principal = principal;
		this.credentials = credentials;
		super.setAuthenticated(true); // must use super, as we override
	}

/**
*任何希望创建工厂方法的代码都可以安全地使用此工厂方法
*未经身份验证的<code>用户名密码认证令牌</code>。
*@param主体
*@param凭据
*@返回用户名密码认证令牌,结果为false isAuthenticated()
*
*@自5.7以来
*/

	public static UsernamePasswordAuthenticationToken unauthenticated(Object principal, Object credentials) {
		return new UsernamePasswordAuthenticationToken(principal, credentials);
	}

	/**
*任何希望创建工厂方法的代码都可以安全地使用此工厂方法
*经过身份验证的<code>用户名密码身份验证令牌</code>。
*@param主体
*@param凭据
*@返回用户名密码认证令牌,结果为true isAuthenticated()
*
*@自5.7以来
*/
	public static UsernamePasswordAuthenticationToken authenticated(Object principal, Object credentials,
			Collection<? extends GrantedAuthority> authorities) {
		return new UsernamePasswordAuthenticationToken(principal, credentials, authorities);
	}

	@Override
	public Object getCredentials() {
		return this.credentials;
	}

	@Override
	public Object getPrincipal() {
		return this.principal;
	}

	@Override
	public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
		Assert.isTrue(!isAuthenticated,
				"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
		super.setAuthenticated(false);
	}

	@Override
	public void eraseCredentials() {
		super.eraseCredentials();
		this.credentials = null;
	}

}

通过上面我们确认了principal前面是账号,credentials就是密码,返回的是一个Authentication,因为他继承了AbstractAuthenticationToken

c3509a5b237a4c1eba988e2360a7a5a1.png

2.UsernamePasswordAuthenticationFilter

上面我们就提到UsernamePasswordAuthenticationFilter这个过滤器了,他主要是拦截账号密码封装整UsernamePasswordAuthenticationToken交给AuthenticationManager去做认证的,我是怎么知道的呢?我们来看下重要源码:

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";

    // 默认的认证请求URL和HTTP方法
    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = 
        new AntPathRequestMatcher("/login", "POST");

    // 用于存储用户名和密码参数名的字段
    private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
    private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
    
    // 标志是否只允许POST请求
    private boolean postOnly = true;

    // 使用默认请求匹配器的构造函数
    public UsernamePasswordAuthenticationFilter() {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
    }

    // 使用自定义认证管理器的构造函数
    public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        
        // 如果postOnly为true,检查请求方法是否为POST
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("不支持的认证方法: " + request.getMethod());
        }

        // 从请求中获取用户名
        String username = obtainUsername(request);
        username = (username != null) ? username.trim() : "";

        // 从请求中获取密码
        String password = obtainPassword(request);
        password = (password != null) ? password : "";

        // 创建认证令牌
        UsernamePasswordAuthenticationToken authRequest = 
            UsernamePasswordAuthenticationToken.unauthenticated(username, password);

        // 为认证请求设置附加详情
        setDetails(request, authRequest);

        // 将认证委托给认证管理器
        return this.getAuthenticationManager().authenticate(authRequest);
    }

    // 从请求中获取用户名的方法
    protected String obtainUsername(HttpServletRequest request) {
        return request.getParameter(usernameParameter);
    }

    // 从请求中获取密码的方法
    protected String obtainPassword(HttpServletRequest request) {
        return request.getParameter(passwordParameter);
    }

    // 为认证请求设置附加详情的方法
    protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }
}
重要看这段汉

我们看到这哥们把UsernamePasswordAuthenticationToken给了AuthenticationManager,那么问题又来了AuthenticationManager是如果完成验证的呢?

3.AuthenticationManage

我们点击源码后发现AuthenticationManager是个接口,接口怎么实现认证呢?不可能的嘛,所以我们找下实现类:

我们重点关注这里类

868ff61b384948778688dd824d58db44.png021651d5450146ba91611c966c9da71c.png

我们发现这哥们是 AuthenticationManager 的实现子类之一,也是我们最常用的一个实现。我们来看下实际做验证的代码,源码重点的部分0760e225c7fc487e9be4e0ba0671e611.png

上图中我们发现循环了AuthenticationProvider provider,说明我们可能出现多个provider,那么AuthenticationProvider是个接口所以主要用于认证的是ProviderManager 子类,如果我们有多种认证方式,那么只依靠一个ProviderManager 本身来实现 authenticate() 接口是完全不够的,所以上面我们看到了循环去调用authenticate()。

了解上面3个类后我们发现这三个类是一个链路,也就是张老演员图了:

453293e06f91467b9b9b2ef13f74f03e.png

上图中我们看到UsernamePasswordAuthenticationToken需要账号密码,那么我们要实现的是手机验证码,如果你重上面看到这里应该知道怎么实现了吧?

手机验证整合Security

思路:

1.既然UsernamePasswordAuthenticationToken的爹是它AbstractAuthenticationToken,那么我们就照猫画虎重写一个

2.读过上面的都知道UsernamePasswordAuthenticationFilter是拦截账号和密码的,而且这孩子还继承了家产AbstractAuthenticationProcessingFilter,那么我们是要拿手机号和验证码的,所以照猫画虎重写一个

3.然后这哥们UsernamePasswordAuthenticationFilter是不是要把UsernamePasswordAuthenticationToken给AuthenticationManager然后AuthenticationManager又是个接口,他需要实现了ProviderManger,调用authenticate验证,那么里面遍历了ProviderManger,那么我们是不是自己做一个ProviderManger让它去调用authenticate验证不就完了?所以要整了类实现下AuthenticationProvider接口。

开整

SmsCodeAuthenticationToken

/*
 *这一步的作用是为了替换原有系统的 UsernamePasswordAuthenticationToken 用来做验证
 *
 * 代码都是从UsernamePasswordAuthenticationToken 里粘贴出来的
 *
 */
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    /**
     * 在 UsernamePasswordAuthenticationToken 中该字段代表登录的用户名,
     * 在这里就代表登录的手机号码
     */
    private final Object principal;

    /**
     * 构建一个没有鉴权的 SmsCodeAuthenticationToken
     */
    public SmsCodeAuthenticationToken(Object principal) {
        super(null);
        this.principal = principal;
        setAuthenticated(false);
    }

    /**
     * 构建拥有鉴权的 SmsCodeAuthenticationToken
     */
    public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true); // must use super, as we override
    }

    public Object getCredentials() {
        return null;
    }

    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();
    }
}

这样第一个就完成了 

SmsCodeAuthenticationFilter


/**
 * 短信登录的鉴权过滤器,模仿 UsernamePasswordAuthenticationFilter 实现
*/
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    /**
     * form表单中手机号码的字段name
     */
    public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "phone";

    private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY;
    /**
     * 是否仅 POST 方式
     */
    private boolean postOnly = true;

    public SmsCodeAuthenticationFilter(AuthenticationManager authenticationManager,
                                       AuthenticationSuccessHandler authenticationSuccessHandler,
                                       AuthenticationFailureHandler authenticationFailureHandler
                                       ) {
        super(new AntPathRequestMatcher(PHONE_LOGIN_PATH, "POST"));
        setAuthenticationManager(authenticationManager);
        setAuthenticationSuccessHandler(authenticationSuccessHandler);
        setAuthenticationFailureHandler(authenticationFailureHandler);
    }

    @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());
        }
        // 电话号码
        String mobile = request.getParameter("phone");
        if (StringUtils.isEmpty(mobile)) {
            throw new CustomException(500,"手机号码不能为空");
        }
        return this.getAuthenticationManager().authenticate(new SmsCodeAuthenticationToken(mobile));
    }


    protected String obtainUsername(HttpServletRequest request) {
        StringBuilder data = new StringBuilder();
        String line;
        BufferedReader reader;
        try {
            reader = request.getReader();
            while ((null!=(line=reader.readLine()))){
                data.append(line);
            }
        }catch (IOException e){
            return null;
        }
        User user = JSONUtil.toBean(data.toString(), User.class);
        return user.getPhone();
    }

    protected String obtainPassword(HttpServletRequest request) {
        return request.getParameter(mobileParameter);
    }
}

 SmsCodeAuthenticationProvider

**
 * 短信登陆鉴权 Provider,要求实现 AuthenticationProvider 接口
 */
@Configuration
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private UserDetailsService userDetailsService;

    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;

        String mobile = (String) authenticationToken.getPrincipal();
//       //校验验证码
        checkSmsCode(mobile);
        UserDetails userDetails = userDetailsService.loadUserByUsername(mobile);
        if (userDetails == null) {
            throw new CustomException(400, "用户不存在");
        }
        // 此时鉴权成功后,应当重新 new 一个拥有鉴权的 authenticationResult 返回  role_code
        SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());
        authenticationResult.setDetails(authenticationToken.getDetails());
        return authenticationResult;
    }

    /**
     * 校验短信验证码
     *
     * @param mobile
     */
    private void checkSmsCode(String mobile) {
        boolean phoneInvalid = RegexUtils.isPhoneInvalid(mobile);
        if (phoneInvalid) {
            throw new CustomException(400, "手机号格式不正确~");
        }
        Boolean isHaveKey = stringRedisTemplate.hasKey(RedisConstants.LOGIN_CODE_KEY + mobile);
        if (!isHaveKey) {
            throw new CustomException(401, "验证码失效~");
        }
//        获取redis进行比较
        String code = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + mobile);
        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        String inputCode = request.getParameter("code");
        if (!code.equals(inputCode)) {
            throw new CustomException(500, "验证码错误~");
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        // 判断 authentication 是不是 SmsCodeAuthenticationToken 的子类或子接口
        return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }

    public UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    public StringRedisTemplate getStringRedisTemplate() {
        return stringRedisTemplate;
    }

    public void setStringRedisTemplate(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 获取请求中的验证码
     *
     * @param request
     * @return HttpServletRequest
     */
    protected String obtainCode(HttpServletRequest request) {
        StringBuilder data = new StringBuilder();
        String line;
        BufferedReader reader;
        try {
            reader = request.getReader();
            while ((null != (line = reader.readLine()))) {
                data.append(line);
            }
        } catch (IOException e) {
            return null;
        }
        LoginDto loginDto = JSONUtil.toBean(data.toString(), LoginDto.class);
        return loginDto.getCode();
    }
}

 大功告成了,然后我们发现上面的代码使用UserDetail,我们还是来看老演员

09e347a694cc4e89ba500dae6cd3f0e3.png

其实你上面重新的三个就到这了。

注意: 需结合我上篇文章的修改着写

UserDetail

@Service
@RequiredArgsConstructor
@Slf4j
public class SysUserDetailsService implements UserDetailsService {


    private final UserMapper userMapper;

    private final RoleService roleService;

    private final MenuService menuService;


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//        判断是否是账密密码登录还是手机号登录
        UserAuthInfo userAuthInfo = null;
        if (isMobileNO(username)) {
            userAuthInfo = this.userMapper.getUserNameByPhone(username);
//         如果查询不到那么为手机注册的用户
            if (Objects.isNull(userAuthInfo)) {
                userAuthInfo = this.register(username);
            }
        } else {
            userAuthInfo = this.userMapper.getUserAuthInfo(username);
        }
        if (userAuthInfo == null) {
            throw new UsernameNotFoundException("未找到对应账号,请检查输入信息~");
        }
        Set<String> roles = userAuthInfo.getRoles();
        if (CollectionUtil.isNotEmpty(roles)) {
            Set<String> perms = menuService.listRolePerms(roles);
            userAuthInfo.setPerms(perms);
        }
        return new SysUserDetails(userAuthInfo);
    }


    public static boolean isMobileNO(String input) {
        // 中国大陆手机号正则表达式
        String regex = "^1[3-9]\\d{9}$";
        return input.matches(regex);
    }

    /**
     * 手机注册账号
     */
    private UserAuthInfo register(String username) {
        log.info("手机号用户注册--{}--开始", username);
       try {
           User user = this.userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getPhone, username));
//        判断该用户是否存在 不存在添加用户
           if (Objects.isNull(user)) {
               user = new User().setPhone(username).setUsername(UUID.randomUUID().toString()).setType(0).setVxAvatar("https://oss.youlai.tech/youlai-boot/2023/05/16/811270ef31f548af9cffc026dfc3777b.gif");
//            //添加用户
               userMapper.insert(user);
               //  默认分配权限用户
               List<Long> roleIdList = new ArrayList<>();
               roleIdList.add(17L);//用户
               AssginRoleVo assginRoleVo = new AssginRoleVo().setUserId(Long.parseLong(user.getId())).setRoleIdList(roleIdList);
               roleService.doAssignRole(assginRoleVo);
           }
       }catch (Exception e){
           log.error("手机号用户注册--{}--异常", username,e);
           throw new CustomException(500,"手机号用户注册异常,请联系管理员~");
       }
        log.info("手机号用户注册--{}--结束", username);
        return this.userMapper.getUserNameByPhone(username);
    }

}

 最后一步加上配置文件即可

SecurityConfig


/**
 * Spring Security 权限配置
 *
 * @author cws
 */
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    // 自定义未认证处理类
    private final MyAuthenticationEntryPoint authenticationEntryPoint;
    // 自定义无权限访问处理类
    @Resource
    private final MyAccessDeniedHandler accessDeniedHandler;

    // Redis操作模板
    @Autowired
    private final RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private SmsCodeAuthenticationProvider smsCodeAuthenticationProvider;

    @Autowired
    private SysUserDetailsService userDetailsService;

    private final Filter globalSpringSecurityExceptionHandler = new CustomSecurityExceptionHandler();

    @Autowired
    LoginFailHandler loginFailHandler;

    @Autowired
    LoginSuccessHandler loginSuccessHandler;


   @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 配置Spring Security过滤器链。
     *
     * @param http HttpSecurity对象,用于构建安全配置
     * @return 构建好的SecurityFilterChain对象
     * @throws Exception 配置过程中可能抛出的异常
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http,AuthenticationManager authenticationManager) throws Exception {
        http
                .authorizeHttpRequests(requestMatcherRegistry ->// 配置请求授权规则
                        //登录路径公开访问
                        requestMatcherRegistry.requestMatchers(SecurityConstants.LOGIN_PATH,
                                        SecurityConstants.LOGOUT_PATH,
                                        SecurityConstants.VERIFY_TREE_PATH,
                                        SecurityConstants.GET_PHONE_CODE_PATH
                                ).permitAll()
                                // 其他所有请求都需要认证
                                .anyRequest().authenticated()
                )
                // 禁用Session创建
                .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                // 配置异常处理
                .exceptionHandling(httpSecurityExceptionHandlingConfigurer ->
                        httpSecurityExceptionHandlingConfigurer
                                // 设置未认证处理入口
                                .authenticationEntryPoint(authenticationEntryPoint)
                                // 设置无权限访问处理
                                .accessDeniedHandler(accessDeniedHandler)
                )
                // 禁用CSRF保护
                .csrf(AbstractHttpConfigurer::disable)
                .formLogin(AbstractHttpConfigurer::disable)
                .httpBasic(AbstractHttpConfigurer::disable)
                .logout(AbstractHttpConfigurer::disable)
                .sessionManagement(AbstractHttpConfigurer::disable)
                .csrf(AbstractHttpConfigurer::disable)
                // requestCache用于重定向,前后端分析项目无需重定向,requestCache也用不上
                .requestCache(cache -> cache
                        .requestCache(new NullRequestCache())
                )
        ;
        //添加手机号登陆过滤器
        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
        smsCodeAuthenticationProvider.setStringRedisTemplate(stringRedisTemplate);
        SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter(
                                new ProviderManager(
                        List.of(smsCodeAuthenticationProvider)),
                loginSuccessHandler,
                loginFailHandler);
        http.addFilterBefore(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        // JWT 校验过滤器
        http.addFilterBefore(new JwtValidationFilter(redisTemplate), UsernamePasswordAuthenticationFilter.class);
        // 其他未知异常. 尽量提前加载。
        http.addFilterBefore(globalSpringSecurityExceptionHandler, SecurityContextHolderFilter.class);
        // 构建并返回过滤器链
        return http.build();
    }

    /**
     * 不走过滤器链的放行配置
     */
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        // 忽略指定路径的安全检查
        return (web) -> web.ignoring()
                .requestMatchers(
                        "/api/v1/auth/captcha",
                        "/webjars/**",
                        "/doc.html",
                        "/swagger-resources/**",
                        "/v3/api-docs/**",
                        "/swagger-ui/**",
                        "/swagger-ui.html",
                        "/ws/**",
                        "/ws-app/**"
                );
    }

    /**
     * 密码编码器
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 手动注入AuthenticationManager,用于处理认证和授权请求。
     *
     * @param authenticationConfiguration 认证配置对象
     * @return AuthenticationManager对象
     * @throws Exception 配置过程中可能抛出的异常
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        // 获取认证管理器实例
        return authenticationConfiguration.getAuthenticationManager();
    }


}

认证成功或者失败的处理器

@Component
public class LoginSuccessHandler extends
    AbstractAuthenticationTargetUrlRequestHandler implements AuthenticationSuccessHandler {

  @Autowired
  private ApplicationEventPublisher applicationEventPublisher;

;

  public LoginSuccessHandler() {
    this.setRedirectStrategy(new RedirectStrategy() {
      @Override
      public void sendRedirect(HttpServletRequest request, HttpServletResponse response, String url)
          throws IOException {
        // 更改重定向策略,前后端分离项目,后端使用RestFul风格,无需做重定向
        // Do nothing, no redirects in REST
      }
    });
  }

  @Override
  public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
      Authentication authentication) throws IOException {
    Object principal = authentication.getPrincipal();
    if (principal == null || !(principal instanceof UserDetails)) {
      throw new CustomException(500, "登陆认证成功后,authentication.getPrincipal()返回的Object对象必须是:UserLoginInfo!");
    }
    String token= JwtUtils.generateToken(authentication);
    LoginVo loginVo = new LoginVo();
    loginVo.setTokenType("Bearer ");
    loginVo.setAccessToken(token);
    // 虽然APPLICATION_JSON_UTF8_VALUE过时了,但也要用。因为Postman工具不声明utf-8编码就会出现乱码
    response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
    PrintWriter writer = response.getWriter();
    writer.print(JSONUtil.toJsonStr(Result.success(loginVo)));
    writer.flush();
    writer.close();
  }
}



@Component
public class LoginFailHandler implements AuthenticationFailureHandler {

  @Override
  public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                      AuthenticationException exception) throws IOException, ServletException {
    String errorMessage = exception.getMessage();
    response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
    PrintWriter writer = response.getWriter();
    writer.print(JSONUtil.toJsonStr(Result.fail(errorMessage)));
    writer.flush();
    writer.close();
  }
}

总结:重点在于理解上面的三个类及整个Security的认证流程即可实现。

关注后面更新接入第三方做认证的案例及阿里云短信服务~~~

 

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

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

相关文章

【C++】10道经典面试题带你玩转二叉树

&#x1f984;个人主页:修修修也 &#x1f38f;所属专栏:C ⚙️操作环境:Leetcode/牛客网 目录 一.根据二叉树创建字符串 二.二叉树的层序遍历 三.二叉树的层序遍历 II 四.二叉树的最近公共祖先 五.二叉搜索树与双向链表 六.从前序与中序遍历序列构造二叉树 七.从中序与后序遍历…

基于yolov8的无人机检测系统python源码+onnx模型+评估指标曲线+精美GUI界面

【算法介绍】 基于YOLOv8的无人机检测系统是一项前沿技术&#xff0c;结合了YOLOv8深度学习模型的强大目标检测能力与无人机的灵活性。YOLOv8作为YOLO系列的最新版本&#xff0c;在检测精度和速度上均有显著提升&#xff0c;特别适用于复杂和高动态的场景。 该系统通过捕获实…

【QML 基础】QML ——描述性脚本语言,用于用户界面的编写

文章目录 1. QML 定义2. QML 1. QML 定义 &#x1f427; QML全称为Qt Meta-Object Language&#xff0c;QML是一种描述性的脚本语言&#xff0c;文件格式以.qml结尾。支持javascript形式的编程控制。QML是Qt推出的Qt Quick技术当中的一部分&#xff0c;Qt Quick是 Qt5中用户界…

C++笔记---set和map

1. 序列式容器与关联式容器 前面我们已经接触过STL中的部分容器如&#xff1a;string、vector、list、deque、array、forward_list等&#xff0c;这些容器统称为序列式容器&#xff0c;因为逻辑结构为线性序列的数据结构&#xff0c;两个位置存储的值之间一般没有紧密的关联关…

U盘格式化了怎么办?这4个工具能帮你恢复数据。

如果你思维U盘被格式化了&#xff0c;也不用太过担心&#xff0c;其实里面的数据并没有被删除&#xff0c;只是被标记为了可覆盖的状态。只要我们及时采取正确的数据恢复措施&#xff0c;就有很大的机会可以将数据找回。比如使用专业得的数据恢复软件&#xff0c;我也可以跟大家…

缓存的思考与总结

缓存的思考与总结 什么是缓存缓存命中率数据一致性旁路模式 Cache aside双写模式直写模式 write through异步写 Write Behind 旁路和双写 案例 新技术或中间的引入&#xff0c;一定是解决了亟待解决的问题或是显著提升了系统性能&#xff0c;并且这种改变所带来的增幅&#xff…

python新手的五个练习题

代码 # 1. 定义一个变量my_Number,将其设置为你的学号&#xff0c;然后输出到终端。 my_Number "20240001" # 假设你的学号是20240001 print("学号:", my_Number) # 2. 计算并输出到终端:两个数(例如3和5)的和、差、乘积和商。 num1 3 num2 5 print(&…

nodejs基于vue电子产品商城销售网站的设计与实现 _bugfu

目录 技术栈具体实现截图系统设计思路技术可行性nodejs类核心代码部分展示可行性论证研究方法解决的思路Express框架介绍源码获取/联系我 技术栈 该系统将采用B/S结构模式&#xff0c;开发软件有很多种可以用&#xff0c;本次开发用到的软件是vscode&#xff0c;用到的数据库是…

论文集搜索网站-dblp 详细使用方法

分享在dblp论文集中的两种论文搜索方式&#xff1a;关键字搜索&#xff0c;指定会议/期刊搜索。 关键字搜索 进入dblp官方网址dblp: computer science bibliography&#xff0c;直接在上方搜索栏&#xff0c;搜索关键字&#xff0c;底下会列出相关论文。 指定会议/期刊搜索 …

三菱FX5U PLC故障处理(各种出错的内容、原因及处理方法进行说明。)

对使用系统时发生的各种出错的内容、原因及处理方法进行说明。 故障排除的步骤 发生故障时&#xff0c;按以下顺序实施故障排除。 1.确认各模块是否正确安装或正确配线。 2、确认CPU模块的LED。 3.确认各智能功能模块的LED。(各模块的用户手册) 4、连接工程工具&#xff0c;启…

从数据仓库到数据中台再到数据飞轮:我了解的数据技术进化史

这里写目录标题 前言数据仓库&#xff1a;数据整合的起点数据中台&#xff1a;数据共享的桥梁数据飞轮&#xff1a;业务与数据的双向驱动结语 前言 在当今这个数据驱动的时代&#xff0c;企业发展离不开对数据的深度挖掘和高效利用。从最初的数据仓库&#xff0c;到后来的数据…

828华为云征文|华为Flexus云服务器搭建Cloudreve私人网盘

一、华为云 Flexus X 实例&#xff1a;开启高效云服务新篇&#x1f31f; 在云计算的广阔领域中&#xff0c;资源的灵活配置与卓越性能犹如璀璨星辰般闪耀。华为云 Flexus X 实例恰似一颗最为耀眼的新星&#xff0c;将云服务器技术推向了崭新的高度。 华为云 Flexus X 实例基于…

使用SpringCloud构建可伸缩的微服务架构

Spring Cloud是一个用于构建分布式系统的开源框架。它基于Spring Boot构建&#xff0c;并提供了一系列的工具和组件&#xff0c;用于简化开发分布式系统的难度。Spring Cloud可以帮助开发人员快速构建可伸缩的微服务架构。 要使用Spring Cloud构建可伸缩的微服务架构&#xff0…

对接阿里asr和Azure asr

1&#xff1a;对接阿里asr 1.1&#xff1a;pom <dependency><groupId>com.alibaba.nls</groupId><artifactId>nls-sdk-recognizer</artifactId><version>2.2.1</version> </dependency>1.2&#xff1a;生成token package c…

C++之STL—vector容器基础篇

头文件 #include <vector> //vector容器 #include <algorithm> //算法 基本用法&&概念 vector<int> v; v.push_back(10); vector<int >::iterator v.begin(); v.end(); 三种遍历方式 #include <vector> #include <algorithm>…

基于区块链的相亲交易系统源码解析

随着区块链技术的成熟与发展&#xff0c;其去中心化、不可篡改的特性逐渐被应用于各行各业。特别是在婚恋市场中&#xff0c;区块链技术的应用为相亲平台带来了新的可能性 。本文将探讨如何利用区块链技术构建一个透明、高效的相亲交易系统&#xff0c;并提供部分源码示例。 区…

大模型的实践应用30-大模型训练和推理中分布式核心技术的应用

大家好,我是微学AI,今天给大家介绍一下大模型的实践应用30-大模型训练和推理中分布式核心技术的应用。本文深入探讨了大模型训练和推理中分布式核心技术的应用。首先介绍了项目背景,阐述了大模型发展对高效技术的需求。接着详细讲解了分布式技术的原理,包括数据并行、模型并…

数据转换器——佛朗哥Chater2

【注:本文基于《数据转换器》一书进行学习、总结编撰,适合新手小白进行学习】 目录 2.1 数据转换器类别 2.2 工作条件 2.3 转换器性能参数 2.3.1 基本特性参数 2.4 静态性能参数 2.5 动态性能参数 2.6 数字和开关性能参数 2.1 数据转换器类别 转换器类型可以被分为两…

英飞凌TC3xx -- Bootstrap Loader分析

目录 1.Bootstrap Loaders作用 2.CAN BSL详解 2.1 CAN BSL的时钟系统 2.2 CAN BSL流程 3.小结 英飞凌TC3xx的Platform Firmware章节里&#xff0c;提供了多种启动模式&#xff1a; Internal start from Flash&#xff1a;b111Alternate Boot Mode&#xff1a;b110Generic …

杀软对抗 ---> Perfect Syscall??

好久没更了&#xff0c;今天想起来更新了&#x1f60b;&#x1f60b;&#x1f60b;&#x1f60b; 目录 1.AV && EDR 2.Perfect Syscall&#xff1f;&#xff1f; 3.Truly Perfect ??? 在开始之前先来展示一下这次的免杀效果 1.AV && EDR 360 天擎EDR …