java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > spring security扩展自定义登录

spring security如何扩展自定义登录

作者:小星星1991

本文详细介绍了Spring Security的认证原理和具体实现,认证原理基于过滤器链,通过验证用户凭证和构建认证对象来保护应用程序资源,实现自定义认证功能的步骤包括创建自定义认证提供程序、实现UserDetailsService接口以及在配置类中进行相应的配置

背景

spring security 默认给我们提供了一套用户名密码登录的的逻辑实现,在很多时候根本不满足我们实际开发的要求,所以我们经常需要对它认证模块进行扩展,那么如何扩展我们的认证方式呢,这篇文章将会给你一个较为完整的解答。

认证原理

Spring Security使用了一种基于过滤器链的认证原理来保护应用程序。它提供了一组过滤器,这些过滤器按照特定的顺序对每个请求进行处理。

认证过程中的主要步骤如下:

  1. 用户发送认证请求到应用程序。
  2. 应用程序将请求发送给Spring Security的过滤器链。
  3. 过滤器链依次处理请求,直到找到可以处理该请求的过滤器。
  4. 找到处理请求的过滤器后,该过滤器会验证用户的凭证(如用户名和密码)。
  5. 如果凭证有效,则生成一个已认证的安全上下文,并将其保存在Spring Security的上下文存储中。
  6. 如果凭证无效,则返回认证失败的信息给用户。
  7. 认证成功后,用户可以继续访问受保护的资源。

在认证过程中,Spring Security还提供了灵活的配置选项,可以根据特定的需求进行自定义配置,例如使用不同的认证提供程序、自定义身份验证逻辑等。

总结起来,Spring Security的认证原理是通过一组过滤器链来处理认证请求,验证用户凭证并生成认证的安全上下文,以保护应用程序的资源。

具体实现原理

Spring Security的认证实现原理主要涉及以下几个核心概念和组件:

总结起来,Spring Security的认证实现原理是通过认证提供程序验证用户提供的凭证,使用用户详情服务获取用户的详细信息,并构建一个认证对象,然后将该认证对象保存在安全上下文中。

认证过滤器链负责处理认证请求,并根据需要进行认证、授权和其他安全操作。

通过这种方式,Spring Security实现了对应用程序进行认证和授权的功能。

如何实现自定义认证的功能

要自定义Spring Security的认证模块,你可以按照以下步骤进行:

下面根据微信小程序登录来实现一个自定义模块的接口:

定义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 的定义

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的功能有哪些

定义相关的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);
}

它包含两个方法:

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

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文