当前位置: 首页 > article >正文

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.kler.cn/news/315566.html

相关文章:

  • 2024 硬盘格式恢复软件大揭秘
  • 《论分布式存储系统架构设计》写作框架,软考高级系统架构设计师
  • 无限边界:现代整合安全如何保护云
  • 怀庄之醉是勾兑酒吗?
  • YOLOv10改进,YOLOv10替换主干网络为PP-HGNetV2(百度飞桨视觉团队自研,独家手把手教程,助力涨点)
  • re题(38)BUUCTF-[FlareOn6]Overlong
  • 在vue中嵌入vitepress,基于markdown文件生成静态网页从而嵌入社团周报系统的一些想法和思路
  • 【GMNER】Grounded Multimodal Named Entity Recognition on Social Media
  • 负载均衡服务由几部分组成?分别是什么
  • vue3 中后台系统中,复杂表单的开发优化技巧
  • Spring框架总体结构
  • 无人机之航线规划篇
  • Flutter 项目结构的区别
  • 十八,Spring Boot 整合 MyBatis-Plus 的详细配置
  • linux中vim编辑器的应用实例
  • 基于LSTM的温度时序预测
  • 量化交易系统开发源码独立搭建
  • VUE项目在Linux子系统部署
  • 2.个人电脑部署MySQL,傻瓜式教程带你拥有个人金融数据库!
  • Google 释出 Android 15 源代码
  • 数业智能心大陆:职场倦怠的新解法
  • [数据集][目标检测]无人机飞鸟检测数据集VOC+YOLO格式6647张2类别
  • 安装selenium、chrome、chromedriver.exe相对应的版本
  • 【Java】线程暂停比拼:wait() 和 sleep()的较量
  • 安卓数据存储——SharedPreferences
  • Apifox 「定时任务」操作指南,解锁自动化测试的新利器
  • HTTPS:构建安全通信的基石
  • 关于es的一个多集群、多索引切换的实现
  • [leetcode刷题]面试经典150题之2移除元素(简单)
  • pycharm 使用 translation 插件通过openai进行翻译