前言
Spring Security是一个功能强大且高度且可定制的身份验证和访问控制框架,包含标准的身份认证和授权。
本文主要介绍SpringBoot中如何配置使用 Spring Security 安全认证框架并简述相关原理和步骤。
核心认证流程解析
- 请求过滤
- 用户提交登录表单
AbstractAuthenticationProcessingFilter
过滤请求,创建AbstractAuthenticationToken
以默认提供的 UsernamePasswordAuthenticationFilter
举例:
public class UsernamePasswordAuthenticationFilter extendsAbstractAuthenticationProcessingFilter {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);}// ......
}
- 认证管理器
AuthenticationManager
(通常是ProviderManager
)协调认证过程,根据AbstractAuthenticationToken
类型 选择对应的AuthenticationProvider
在 ProviderManager
中根据不同的 Token 类型匹配不同的 Provider
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是否可以处理当前的authenticationif (!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);
执行实际的认证过程。
:::tips
认证过程以 AbstractUserDetailsAuthenticationProvider
(实现DaoAuthenticationProvider)为例,大致过程为:
- getUserDetailsService().loadUserByUsername(username); 加载用户信息
- preAuthenticationChecks.check(user); 校验用户信息,是否锁定、过期、可用等
- additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication); 验证用户密码
:::
- 认证后处理
- 认证成功:生成已认证的
Authentication
对象,存入SecurityContext
- 认证失败:抛出
AuthenticationException
由AuthenticationEntryPoint
处理
组件 | 职责 | 典型实现类 |
---|---|---|
AbstractAuthenticationProcessingFilter | 拦截认证请求,封装认证对象 | UsernamePasswordAuthenticationFilter |
AuthenticationManager | 认证流程协调者 | ProviderManager |
AuthenticationProvider | 执行具体认证逻辑 | DaoAuthenticationProvider |
UserDetailsService | 加载用户数据 | 自定义实现类 |
其中
UsernamePasswordAuthenticationFilter
和UsernamePasswordAuthenticationToken
为框架自带的 登录请求过滤和Token实例。如需自定义登录请求过滤和Token实例,可自行实现
AbstractAuthenticationProcessingFilter
和AbstractAuthenticationToken
接口。不同的
AbstractAuthenticationToken
通常有不同的AuthenticationProvider
与之对应,用于实现不同的认证逻辑。自定义的认证逻辑中,通常都是对
AbstractAuthenticationToken
和AuthenticationProvider
的不同实现。
JWT
JWT(JSON Web Token) 是一种轻量级的开放标准(RFC 7519),用于在网络应用间安全地传输信息。它通常用于 身份认证(Authentication) 和 数据交换(Information Exchange),特别适合 前后端分离 和 无状态(Stateless) 的应用场景。
JWT在登录中的应用过程
- 用户提交 用户名 + 密码 登录
- 服务器验证后,生成 JWT 并返回给客户端
- 客户端存储 JWT(通常放
localStorage
或Cookie
) - 后续请求 在
Authorization
头携带 JWT - 服务器 验证 JWT 签名,并解析数据
特点
- 防篡改:签名(Signature)确保 Token 未被修改
- 可设置有效期(
exp
字段)避免长期有效 - 无状态:服务器不需要存储 Session,适合分布式系统
自定义认证逻辑
接下来根据 Spring Security 的认证过程和JWT的特点进行自定义登录逻辑的编写
核心类图
登录流程
具体实现
JwtLoginFilter
实现 AbstractAuthenticationProcessingFilter
接口
public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter {public JwtLoginFilter() {// 设置当前 Filter ,也就是需要过滤的登录URLsuper(new AntPathRequestMatcher("/auth/login", "POST"));}@Overridepublic 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
举例
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;}@Overridepublic String getCredentials() {return code;}@Overridepublic String getPrincipal() {return phone;}
}
SmsAuthProvider
具体实现短信认证的逻辑,主要工作原理是将前端传递的手机号和短信验证码进行匹配校验,如果合法,则认证成功,如果不合法,返回认证失败。
@Component
public class SmsAuthProvider implements AuthenticationProvider {@Autowiredprivate AbstractLogin abstractLogin;@Autowiredprivate JwtUtil jwtUtil;/*** 验证手机验证码登录认证** @param authentication the authentication request object.* @return* @throws AuthenticationException*/@Overridepublic 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;}@Overridepublic boolean supports(Class<?> authentication) {return SmsAuthenticationToken.class.equals(authentication);}
}
SpringBoot配置过程
我们已在上述的过程中将核心的认证逻辑实现,接下来就是把对应的代码配置到 Spring Security 工程之中。
JwtAuthConfig
JwtAuthConfig
实现 WebSecurityConfigurerAdapter
作为整体的配置入口
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class JwtAuthConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate JwtLoginConfig loginConfig;@Overridepublic void configure(WebSecurity web) throws Exception {super.configure(web);}@Overrideprotected 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
实现了SecurityConfigurerAdapter
,SecurityConfigurerAdapter
是 Spring Security 的核心配置基类,用于自定义安全规则(如认证、授权、过滤器链等)。
配置信息如下:
@Configuration
public class JwtLoginConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {@Autowiredprivate LoginSuccessHandler successHandler;@Autowiredprivate LoginFailHandler failHandler;@Autowiredprivate JwtProviderManager jwtProviderManager;/*** 将登录接口的过滤器配置到过滤器链中* 1. 配置登录成功、失败处理器* 2. 配置自定义的userDetailService(从数据库中获取用户数据)* 3. 将自定义的过滤器配置到spring security的过滤器链中,配置在UsernamePasswordAuthenticationFilter之前* @param http*/@Overridepublic 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
进行注入,以达到自动装配的目的。
注:暂只做简单实现。
:::
@Component
public class JwtProviderManager extends ProviderManager {public JwtProviderManager(List<AuthenticationProvider> providers) {super(providers);}
}
LoginSuccessHandler
认证成功后,对认证结果生成JWT Token 返回前端
public class LoginSuccessHandler implements AuthenticationSuccessHandler {@Autowiredprivate JwtUtil jwtUtil;@Autowiredprivate NacosHcUserConfigProperties configProperties;@Overridepublic 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 在登录后的认证鉴权过程。