spring security如何扩展自定义登录
作者:小星星1991
背景
spring security 默认给我们提供了一套用户名密码登录的的逻辑实现,在很多时候根本不满足我们实际开发的要求,所以我们经常需要对它认证模块进行扩展,那么如何扩展我们的认证方式呢,这篇文章将会给你一个较为完整的解答。
认证原理
Spring Security使用了一种基于过滤器链的认证原理来保护应用程序。它提供了一组过滤器,这些过滤器按照特定的顺序对每个请求进行处理。
认证过程中的主要步骤如下:
- 用户发送认证请求到应用程序。
- 应用程序将请求发送给Spring Security的过滤器链。
- 过滤器链依次处理请求,直到找到可以处理该请求的过滤器。
- 找到处理请求的过滤器后,该过滤器会验证用户的凭证(如用户名和密码)。
- 如果凭证有效,则生成一个已认证的安全上下文,并将其保存在Spring Security的上下文存储中。
- 如果凭证无效,则返回认证失败的信息给用户。
- 认证成功后,用户可以继续访问受保护的资源。
在认证过程中,Spring Security还提供了灵活的配置选项,可以根据特定的需求进行自定义配置,例如使用不同的认证提供程序、自定义身份验证逻辑等。
总结起来,Spring Security的认证原理是通过一组过滤器链来处理认证请求,验证用户凭证并生成认证的安全上下文,以保护应用程序的资源。
具体实现原理
Spring Security的认证实现原理主要涉及以下几个核心概念和组件:
- 用户提供的凭证:用户在进行认证时,需要提供其身份凭证,如用户名和密码。
- 认证提供程序(Authentication Provider):认证提供程序是Spring Security中的重要组件,负责验证用户提供的凭证。它使用用户提供的凭证与系统中存储的凭证进行比对,并决定是否通过认证。Spring
- Security提供了多种内置的认证提供程序,如基于数据库的JDBC认证提供程序、LDAP认证提供程序、InMemory认证提供程序等。同时也支持自定义认证提供程序。
- 用户详情服务(UserDetailsService):用户详情服务是Spring Security中的接口,负责获取用户的详细信息,如用户的权限、角色等。认证提供程序在验证用户凭证后,会使用用户详情服务获取用户的详细信息,并构建一个认证对象(Authentication)。
- 认证对象(Authentication):认证对象是Spring Security中表示已认证用户的对象。它包含了用户的身份信息、凭证信息、权限信息等。认证对象由认证提供程序生成,并将其保存在SecurityContextHolder的上下文存储中。
- 认证过滤器链(Authentication Filter Chain):认证过滤器链是Spring Security中的过滤器组成的链条,负责处理认证请求。每个过滤器都会对请求进行处理,并根据需要进行认证、授权和其他安全操作。在认证过程中,认证过滤器链会根据请求的URL匹配合适的过滤器进行处理。
- 安全上下文(Security Context):安全上下文是Spring Security中用于保存已认证用户信息的容器。它以ThreadLocal的方式存储,可以通过SecurityContextHolder来访问和操作。安全上下文中保存了当前的认证对象,可以在应用程序的任何地方获取已认证用户的信息。
总结起来,Spring Security的认证实现原理是通过认证提供程序验证用户提供的凭证,使用用户详情服务获取用户的详细信息,并构建一个认证对象,然后将该认证对象保存在安全上下文中。
认证过滤器链负责处理认证请求,并根据需要进行认证、授权和其他安全操作。
通过这种方式,Spring Security实现了对应用程序进行认证和授权的功能。
如何实现自定义认证的功能
要自定义Spring Security的认证模块,你可以按照以下步骤进行:
- 创建一个自定义的认证提供程序(Authentication Provider)类,该类实现了
org.springframework.security.authentication.AuthenticationProvider
接口。在该类中,你可以编写你自己的认证逻辑,比如验证用户名和密码是否匹配。 - 实现
UserDetailsService
接口来获取用户的详细信息。你可以根据自己的需求,从数据库、LDAP等数据源中获取用户信息,并返回一个UserDetails
对象。 - 在配置类(通常是继承自
WebSecurityConfigurerAdapter
的类)中,覆盖configure(AuthenticationManagerBuilder auth)
方法。在该方法中,你可以指定使用你自定义的认证提供程序和用户详情服务来进行认证。 - 在上述配置类中,覆盖
configure(HttpSecurity http)
方法来配置认证过滤器链。你可以根据需要添加和配置各种过滤器,例如表单登录过滤器、基于Token的认证过滤器等。
下面根据微信小程序登录来实现一个自定义模块的接口:
定义token信息类
public class JsCodeAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = 5615450055779559101L; private String code; /** * Creates a token with the supplied array of authorities. * * @param authorities the collection of <tt>GrantedAuthority</tt>s for the principal * represented by this authentication object. */ public JsCodeAuthenticationToken(Collection<? extends GrantedAuthority> authorities, String code) { super(authorities); this.code = code; } public JsCodeAuthenticationToken(String code) { this(Collections.emptyList(), code); } @Override public Object getCredentials() { return null; } @Override public Object getPrincipal() { return code; } 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(); } }
从代码中我们可以看出我们需要创建一个相关的token类来继承 AbstractAuthenticationToken 类,并实现相关的方法
AbstractAuthenticationToken 的定义
- Spring Security框架中的一个抽象类,用于表示身份验证请求令牌。它用于封装用户凭据或其他必要的信息来进行用户身份验证。
- AbstractAuthenticationToken的子类负责提供特定的身份验证细节,例如用户名和密码、基于令牌的身份验证令牌等。这些子类通常包含获取器和设置器,用于访问和修改身份验证细节。
- AbstractAuthenticationToken还提供了一组方法来管理身份验证状态,例如设置已认证标志、检索与已认证用户关联的权限以及管理与身份验证过程相关的其他详细信息。
AbstractAuthenticationToken 的工作原理
- 当用户请求进行身份验证时,Spring Security将创建一个实现AbstractAuthenticationToken的具体子类对象,并将其中的身份验证信息进行填充。然后,该身份验证令牌将被传递给身份验证管理器进行进一步的身份验证处理。
- 身份验证管理器将检查身份验证令牌,并根据所配置的身份验证策略和安全规则,对用户进行身份验证。在身份验证过程中,身份验证管理器可能会调用相关的身份验证提供者,比如用户名密码身份验证提供者或令牌身份验证提供者,来验证用户的凭据信息。
- 一旦用户通过身份验证,身份验证管理器将更新AbstractAuthenticationToken中的已认证标志和权限信息,并将其返回给应用程序。在后续的请求中,应用程序可以根据需要访问身份验证令牌中的身份验证信息和权限信息,以实现不同的业务逻辑和访问控制。
定义一个接收参数的filter
@Slf4j public class JsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter { private static String jsCode = "code"; private static boolean postOnly = true; protected JsCodeAuthenticationFilter(WechatProperties properties) { super(new AntPathRequestMatcher(properties.getAuthUrl(), "POST")); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { if (postOnly && !request.getMethod().equalsIgnoreCase("post")) { throw new AuthenticationServiceException("该接口不支持:" + request.getMethod()); } String code = obtainCode(request); code = code.trim(); log.info("得到的code:{}", code); JsCodeAuthenticationToken authenticationTokenRequest = new JsCodeAuthenticationToken(code); setDetails(request, authenticationTokenRequest); return this.getAuthenticationManager().authenticate(authenticationTokenRequest); } public String obtainCode(HttpServletRequest request) { if (request.getHeader("Content-Type").contains("application/json")) { return obtainCodeJson(request, jsCode); } return request.getParameter(jsCode); } public String obtainCodeJson(HttpServletRequest request, String param) { StringBuilder builder = new StringBuilder(); String line = null; String code = null; try{ BufferedReader reader = request.getReader(); while ((line = reader.readLine()) != null) { builder.append(line); } Map<String, String> map = JsonUtils.fromString(builder.toString(), Map.class, String.class, String.class); code = map.get(param); }catch (RuntimeException | IOException e) { throw new AuthenticationServiceException("获取参数失败"); } return code; } /** * Provided so that subclasses may configure what is put into the * authentication request's details property. * * @param request that an authentication request is being created for * @param authRequest the authentication request object that should have its details * set */ protected void setDetails(HttpServletRequest request, JsCodeAuthenticationToken authRequest) { authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); }
从上面的代码可以看出,我们需要定义一个filter 类继承 AbstractAuthenticationProcessingFilter 来获取前端传输的参数
AbstractAuthenticationProcessingFilter的定义
- AbstractAuthenticationProcessingFilter是Spring Security框架中的一个过滤器,用于处理用户认证的相关操作。
AbstractAuthenticationProcessingFilter的功能有哪些
- 拦截用户发起的认证请求,并交给AuthenticationManager来进行身份验证。
- 通过调用AuthenticationManager完成身份验证后,将认证结果封装成Authentication对象。
- 将认证结果传递给AbstractAuthenticationProcessingFilter的成功或失败处理器进行处理。
- 处理认证成功或失败的逻辑,例如生成并返回认证成功的Jwt token、跳转到特定页面、返回错误信息等。
定义相关的Provider 类
public class JsCodeAuthenticationProvider implements AuthenticationProvider { private WxOAuth2Service weChatService; private UserDetailsService userDetailsService; private WxMpProperties wxMpProperties; public JsCodeAuthenticationProvider(WxOAuth2Service weChatService, WxMpProperties wxMpProperties) { this.weChatService = weChatService; this.wxMpProperties = wxMpProperties; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { JsCodeAuthenticationToken token = (JsCodeAuthenticationToken) authentication; String code = (String) token.getPrincipal(); log.info("得到的code:{}", code); if (StringUtils.isBlank(code)) { throw new AuthenticationServiceException("jscode 不能为空"); } WxOAuth2AccessToken wxOAuth2AccessToken; try { wxOAuth2AccessToken = weChatService.getAccessToken(wxMpProperties.getAppId(), wxMpProperties.getSecret(), code); } catch (WxErrorException e) { throw new AuthenticationServiceException("授权登录失败"); } WeChatDetailsService weChatDetailsService = (WeChatDetailsService)userDetailsService; UserDetails details = weChatDetailsService.loadByToken(wxOAuth2AccessToken.getOpenId(), wxOAuth2AccessToken.getAccessToken()); if (details == null) { throw new AuthenticationServiceException("无法获取公众号用户"); } JsCodeAuthenticationToken authenticationResult = new JsCodeAuthenticationToken(details.getUsername()); authenticationResult.setDetails(token.getDetails()); return authenticationResult; } @Override public boolean supports(Class<?> authentication) { return JsCodeAuthenticationToken.class.isAssignableFrom(authentication); } public void setUserDetailsService(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; } }
AuthenticationProvider 的定义
AuthenticationProvider是Spring Security框架中的一个接口,用于身份验证的核心组件。
它的定义如下:
public interface AuthenticationProvider { Authentication authenticate(Authentication authentication) throws AuthenticationException; boolean supports(Class<?> authentication); }
它包含两个方法:
- authenticate(Authentication authentication):该方法用于执行身份验证过程。输入参数authentication是一个封装了用户认证信息的Authentication对象,包括用户名、密码等信息。该方法返回一个认证成功的Authentication对象,或者抛出AuthenticationException异常表示认证失败。
- supports(Class<?> authentication):该方法用于判断是否支持给定类型的认证请求。输入参数authentication是要被验证的对象类型,通常是UsernamePasswordAuthenticationToken或JwtAuthenticationToken等。该方法返回一个boolean值,表示是否支持该类型的认证请求。
AuthenticationProvider可以自定义实现,用于实现不同的身份验证逻辑。开发者可以通过实现该接口,并覆盖其中的方法来定制化身份验证过程,例如从数据库查询用户信息并进行密码校验等。
在Spring Security的配置中,可以通过AuthenticationManagerBuilder来注册和配置AuthenticationProvider的实例,以供身份验证使用。
定义相关配置类
@Component @EnableConfigurationProperties(WxMpProperties.class) public class JsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { @Autowired private UserDetailsService userDetailsService; @Autowired private AuthenticationSuccessHandler jsCodeAuthenticationSuccessHandler; @Autowired private AuthenticationFailureHandler jsCodeAuthenticationFailureHandler; @Autowired private WxOAuth2Service weChatService; @Autowired private WechatProperties weChatProperties; @Autowired private WxMpProperties wxMpProperties; @Override public void configure(HttpSecurity http) throws Exception { JsCodeAuthenticationFilter mobilePasswordAuthenticationFilter = new JsCodeAuthenticationFilter(weChatProperties); mobilePasswordAuthenticationFilter.setAuthenticationSuccessHandler(jsCodeAuthenticationSuccessHandler); mobilePasswordAuthenticationFilter.setAuthenticationFailureHandler(jsCodeAuthenticationFailureHandler); mobilePasswordAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class)); JsCodeAuthenticationProvider provider = new JsCodeAuthenticationProvider(weChatService, wxMpProperties); provider.setUserDetailsService(userDetailsService); // 将当前服务注册到 mobilePasswordAuthenticationFilter 连之后 http.authenticationProvider(provider).addFilterAfter(mobilePasswordAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } }
SecurityConfigurerAdapter 的定义
SecurityConfigurerAdapter是Spring Security提供的一个抽象类,用于简化配置和定制化Spring Security的行为。
它是一个适配器模式,继承自SecurityConfigurer接口,并提供了一些默认实现方法,用于提供给开发者灵活地配置和扩展Spring Security。
SecurityConfigurerAdapter的定义如下:
public abstract class SecurityConfigurerAdapter<O extends SecurityFilterChain, B extends SecurityBuilder<O>> implements SecurityConfigurer<O, B> { // 提供一个空实现的configure方法,供子类进行覆盖定制 public void configure(B builder) throws Exception {} // 提供一个空实现的init方法,供子类进行覆盖定制 public void init(B builder) throws Exception {} // 提供一个空实现的configure方法,供子类进行覆盖定制 public void configure(O object) throws Exception {} // 提供一个空实现的postProcess方法,供子类进行覆盖定制 protected void postProcess(O object) throws Exception {} }
开发者可以继承SecurityConfigurerAdapter,并重写其中的方法来实现自定义的安全配置。常见的用法是在WebSecurityConfigurerAdapter中继承SecurityConfigurerAdapter,并重写configure方法来配置Spring Security的行为,例如定义认证规则、权限控制等。通过继承SecurityConfigurerAdapter可以避免直接实现SecurityConfigurer接口时需要实现所有方法的繁琐操作。
整体配置
public void configure(HttpSecurity http) throws Exception { String[] auth = StringUtils.split( securityProperties.getAuthUrl(), ","); http.formLogin().failureHandler(authenticationFailureHandler) .successHandler(authenticationSuccessHandler) .loginProcessingUrl(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_FORM) .loginPage(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL) .and().apply(jsCodeAuthenticationSecurityConfig) .and().apply(usernameAuthenticationSecurityConfig()) .and().authorizeRequests().antMatchers(auth).authenticated() .and().authorizeRequests().anyRequest().permitAll() .and().csrf().disable(); }
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。