Spring Security 登录认证实践

前言

Spring Security是一个功能强大且高度且可定制的身份验证和访问控制框架,包含标准的身份认证和授权。

本文主要介绍SpringBoot中如何配置使用 Spring Security 安全认证框架并简述相关原理和步骤。

核心认证流程解析

  1. 请求过滤
  • 用户提交登录表单
  • AbstractAuthenticationProcessingFilter 过滤请求,创建 AbstractAuthenticationToken

以默认提供的 UsernamePasswordAuthenticationFilter举例:

ini 复制代码
public class UsernamePasswordAuthenticationFilter extends
  AbstractAuthenticationProcessingFilter {

 public UsernamePasswordAuthenticationFilter() {
        // 设置过滤规则
  super(new AntPathRequestMatcher("/login", "POST"));
 }

 public Authentication attemptAuthentication(HttpServletRequest request,
   HttpServletResponse response) throws AuthenticationException {
        // ......
  String username = obtainUsername(request);
  String password = obtainPassword(request);

  if (username == null) {
   username = "";
  }

  if (password == null) {
   password = "";
  }

  username = username.trim();

        // 根据请求参数设置 UsernamePasswordAuthenticationToken 实例
  UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
    username, password);
  setDetails(request, authRequest);

  return this.getAuthenticationManager().authenticate(authRequest);
 }

 // ......
}
  1. 认证管理器
  • AuthenticationManager(通常是 ProviderManager)协调认证过程,根据 AbstractAuthenticationToken类型 选择对应的 AuthenticationProvider

ProviderManager中根据不同的 Token 类型匹配不同的 Provider

ini 复制代码
public Authentication authenticate(Authentication authentication)
        throws AuthenticationException {
    Class<? extends Authentication> toTest = authentication.getClass();
    AuthenticationException lastException = null;
    AuthenticationException parentException = null;
    Authentication result = null;
    Authentication parentResult = null;
    boolean debug = logger.isDebugEnabled();

    // 遍历所有已注册的 providers ,匹配能处理当前 authentication 的 provider 进行认证处理
    for (AuthenticationProvider provider : getProviders()) {
        // 判断provider是否可以处理当前的authentication
        if (!provider.supports(toTest)) {
            continue;
        }
        try {
            // 执行认证逻辑,返回认证结果
            result = provider.authenticate(authentication);

            if (result != null) {
                copyDetails(authentication, result);
                break;
            }
        } catch (AccountStatusException | InternalAuthenticationServiceException e) {
            prepareException(e, authentication);
            throw e;
        } catch (AuthenticationException e) {
            lastException = e;
        }
    }
    // .......
    throw lastException;
}
  • 匹配到对应的 provider 后,调用 provider.authenticate(authentication);执行实际的认证过程。

认证过程以 AbstractUserDetailsAuthenticationProvider(实现DaoAuthenticationProvider)为例,大致过程为:

  • getUserDetailsService().loadUserByUsername(username); 加载用户信息
  • preAuthenticationChecks.check(user); 校验用户信息,是否锁定、过期、可用等
  • additionalAuthenticationChecks(user,authentication); 验证用户密码
  1. 认证后处理
  • 认证成功:生成已认证的 Authentication 对象,存入 SecurityContext
  • 认证失败:抛出 AuthenticationExceptionAuthenticationEntryPoint 处理
组件 职责 典型实现类
AbstractAuthenticationProcessingFilter 拦截认证请求,封装认证对象 UsernamePasswordAuthenticationFilter
AuthenticationManager 认证流程协调者 ProviderManager
AuthenticationProvider 执行具体认证逻辑 DaoAuthenticationProvider
UserDetailsService 加载用户数据 自定义实现类

其中 UsernamePasswordAuthenticationFilterUsernamePasswordAuthenticationToken为框架自带的 登录请求过滤和Token实例。

如需自定义登录请求过滤和Token实例,可自行实现 AbstractAuthenticationProcessingFilterAbstractAuthenticationToken接口。

不同的 AbstractAuthenticationToken 通常有不同的 AuthenticationProvider与之对应,用于实现不同的认证逻辑。

自定义的认证逻辑中,通常都是对 AbstractAuthenticationTokenAuthenticationProvider 的不同实现。

JWT

JWT(JSON Web Token) 是一种轻量级的开放标准(RFC 7519),用于在网络应用间安全地传输信息。它通常用于 身份认证(Authentication) 和 数据交换(Information Exchange),特别适合 前后端分离 和 无状态(Stateless) 的应用场景。

JWT在登录中的应用过程

  1. 用户提交 用户名 + 密码 登录
  2. 服务器验证后,生成 JWT 并返回给客户端
  3. 客户端存储 JWT(通常放 localStorageCookie
  4. 后续请求 在 Authorization 头携带 JWT
  5. 服务器 验证 JWT 签名,并解析数据

特点

  • 防篡改:签名(Signature)确保 Token 未被修改
  • 可设置有效期(exp 字段)避免长期有效
  • 无状态:服务器不需要存储 Session,适合分布式系统

自定义认证逻辑


接下来根据 Spring Security 的认证过程和JWT的特点进行自定义登录逻辑的编写

核心类图

登录流程

具体实现

JwtLoginFilter

实现 AbstractAuthenticationProcessingFilter接口

java 复制代码
public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter {
    public JwtLoginFilter() {
        // 设置当前 Filter ,也就是需要过滤的登录URL
        super(new AntPathRequestMatcher("/auth/login", "POST"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException, IOException, ServletException {

        // 获取前端传递登录模式
        String loginType = request.getParameter("loginType");
        Authentication authentication = null;
        // 判断前端使用的登录模式
        if (CommonConstant.LoginType.SMS.equals(loginType)) {
            // 手机短信
            String phone = request.getParameter("phone");
            String code = request.getParameter("code");
            authentication = new SmsAuthenticationToken(phone, code);
        }
        if (CommonConstant.LoginType.WX.equals(loginType)) {
            String code = request.getParameter("code");
            authentication = new WxAuthenticationToken(code);
        }

        if (authentication == null) {
            throw new UnsupportedLoginTypeException();
        }
        return getAuthenticationManager().authenticate(authentication);
    }
}

SmsAuthenticationToken

TIPS:如果需要多种认证模式,如:用户密码、短信认证、扫描登录、三方认证等,可实现不同的 Token实例,并实现与之对应的 Provider

以短信认证Token SmsAuthenticationToken举例

typescript 复制代码
public class SmsAuthenticationToken extends AbstractAuthenticationToken {

    private final String phone;
    private final String code;

    public SmsAuthenticationToken(String phone, String code) {
        super(new ArrayList<>());
        this.phone = phone;
        this.code = code;
    }

    @Override
    public String getCredentials() {
        return code;
    }

    @Override
    public String getPrincipal() {
        return phone;
    }
}

SmsAuthProvider

具体实现短信认证的逻辑,主要工作原理是将前端传递的手机号和短信验证码进行匹配校验,如果合法,则认证成功,如果不合法,返回认证失败。

java 复制代码
@Component
public class SmsAuthProvider implements AuthenticationProvider {

    @Autowired
    private AbstractLogin abstractLogin;

    @Autowired
    private JwtUtil jwtUtil;

    /**
     * 验证手机验证码登录认证
     *
     * @param authentication the authentication request object.
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsAuthenticationToken token = (SmsAuthenticationToken) authentication;
        String phone = token.getPrincipal();
        String code = token.getCredentials();
        try {
            UserDetails userDetails = abstractLogin.smsLogin(phone, code);
            token.setDetails(userDetails);
        } catch (AuthenticationException authenticationException) {
            throw authenticationException;
        } catch (Exception e) {
            throw new LoginFailException();
        }
        return token;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return SmsAuthenticationToken.class.equals(authentication);
    }
}

SpringBoot配置过程


我们已在上述的过程中将核心的认证逻辑实现,接下来就是把对应的代码配置到 Spring Security 工程之中。

JwtAuthConfig

JwtAuthConfig实现 WebSecurityConfigurerAdapter作为整体的配置入口

less 复制代码
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class JwtAuthConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtLoginConfig loginConfig;

    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf()
                .disable()
                .formLogin()
                .disable()
                // 应用登录相关配置信息
                .apply(loginConfig)
                .and()
                .authorizeRequests()
                // 放行登录 URL 
                .antMatchers("/auth/login")
                .permitAll();
    }
}

该配置类中,只做了初始化的简单配置,如设置放行登录URL、禁用 csrf、禁用 默认的formLogin等。更多的登录认证配置在 JwtLoginConfig中进行。

JwtLoginConfig

JwtLoginConfig实现了SecurityConfigurerAdapterSecurityConfigurerAdapter 是 Spring Security 的核心配置基类,用于自定义安全规则(如认证、授权、过滤器链等)。

配置信息如下:

scss 复制代码
@Configuration
public class JwtLoginConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    @Autowired
    private LoginSuccessHandler successHandler;
    @Autowired
    private LoginFailHandler failHandler;

    @Autowired
    private JwtProviderManager jwtProviderManager;


    /**
     * 将登录接口的过滤器配置到过滤器链中
     * 1. 配置登录成功、失败处理器
     * 2. 配置自定义的userDetailService(从数据库中获取用户数据)
     * 3. 将自定义的过滤器配置到spring security的过滤器链中,配置在UsernamePasswordAuthenticationFilter之前
     * @param http
     */
    @Override
    public void configure(HttpSecurity http) {
        JwtLoginFilter filter = new JwtLoginFilter();
        // authenticationManager 中已经预设系统内的 provider 集合
        filter.setAuthenticationManager(jwtProviderManager);
        //认证成功处理器
        filter.setAuthenticationSuccessHandler(successHandler);
        //认证失败处理器
        filter.setAuthenticationFailureHandler(failHandler);

        //将这个过滤器添加到UsernamePasswordAuthenticationFilter之后执行
        http.addFilterAfter(filter, UsernamePasswordAuthenticationFilter.class);
    }
}

JwtProviderManager

从上文中的认证过程(时序图)中,AuthenticationManager 是委托 ProviderManager进行认证模式的匹配和执行对应的 provider。

:::tips 为了后续的多认证模式的支持和动态匹配,所以将 ProviderManager 交给 Spring 容器管理,并且通过构造方法将平台内所有已经注册到Spring容器中的 provider进行注入,以达到自动装配的目的。

注:暂只做简单实现。

java 复制代码
@Component
public class JwtProviderManager extends ProviderManager {
    public JwtProviderManager(List<AuthenticationProvider> providers) {
        super(providers);
    }
}

LoginSuccessHandler

认证成功后,对认证结果生成JWT Token 返回前端

ini 复制代码
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private NacosHcUserConfigProperties configProperties;
    
    @Override
    public void onAuthenticationSuccess(
            HttpServletRequest request,
            HttpServletResponse response,
            Authentication authentication)
            throws IOException, ServletException {

        // 生成 token 返回前端
//        Object principal = authentication.getPrincipal();
        UserDetails details = (UserDetails) authentication.getDetails();
        // accessToken 过期时间 30分钟
        Long accessTokenExpireSeconds = configProperties.getAuth().getAccessTokenExpireSeconds();
        // refreshToken 过期时间 6小时
        Long refreshTokenExpireSeconds = configProperties.getAuth().getRefreshTokenExpireSeconds();
        String accessToken = jwtUtil.createToken(details.getUsername(), accessTokenExpireSeconds);
        String refreshToken = jwtUtil.createToken(accessToken, refreshTokenExpireSeconds);

        Map<String, String> tokenMap = new HashMap<>();
        tokenMap.put("accessToken", accessToken);
        tokenMap.put("refreshToken", refreshToken);

        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        response.getWriter().write(JSON.toJSONString(ApiResult.success(tokenMap)));

    }
}

END

至此,相关 Spring Security 配置已完成!💯💯

🧑‍💻🧑‍💻🧑‍💻

下一篇继续探究 Spring Security 在登录后的认证鉴权过程。

相关推荐
爬山算法12 小时前
Netty(15)Netty的线程模型是什么?它有哪些线程池类型?
java·后端
白宇横流学长12 小时前
基于SpringBoot实现的冬奥会科普平台设计与实现【源码+文档】
java·spring boot·后端
Python编程学习圈13 小时前
Asciinema - 终端日志记录神器,开发者的福音
后端
bing.shao13 小时前
Golang 高并发秒杀系统踩坑
开发语言·后端·golang
壹方秘境13 小时前
一款方便Java开发者在IDEA中抓包分析调试接口的插件
后端
brzhang13 小时前
A2UI:但 Google 把它写成协议后,模型和交互的最后一公里被彻底补全
前端·后端·架构
开心猴爷14 小时前
iOS App 性能测试中常被忽略的运行期问题
后端
SHERlocked9314 小时前
摄像头 RTSP 流视频多路实时监控解决方案实践
c++·后端·音视频开发
AutoMQ14 小时前
How does AutoMQ implement a sub-10ms latency Diskless Kafka?
后端·架构
Rover.x14 小时前
Netty基于SpringBoot实现WebSocket
spring boot·后端·websocket