南飓风 发表于 2024-11-6 20:26:05

七、Spring Boot集成Spring Security之前后分离认证最佳实现

二、自定义用户名密码认证过滤器RestfulUsernamePasswordAuthenticationFilter

1、注册过滤器方式


[*]使用httpSecurity.addFilter/addFilterBefore/addFilterAfter向过滤器链中添加过滤器,此中addFilter只能添加内置的过滤器,序次已在过滤器序次注册器(FilterOrderRegistration)中设置;addFilterBefore/addFilterAfter可以添加自定义过滤器,添加在指定的过滤器之前/之后。该方式优点是使用简单,缺点是无法使用spring security内置的组件,与RestfulUsernamePasswordAuthenticationFilter需要使用AuthenticationManager组件辩论,故不使用该方式。
[*]使用SecurityConfigurer通过配置类的方式向过滤器链中添加过滤器,官方使用的方式。该方式优点是可以使用spring security内置的组件,缺点是实现较为笨重,而且只能注册过滤器序次注册器(FilterOrderRegistration)中设定的过滤器。该方式可以使用spring security内置的组件,所以采用本方式,需要修改过滤器序次注册器添加自定义的过滤器。
2、修改并覆盖过滤器序次注册器


[*]FilterOrderRegistration类为final类且未提供开放的注册自定义过滤器的方式,所以只能重写该类,并添加自定义过滤器的序次
package org.springframework.security.config.annotation.web.builders;

import com.yu.demo.spring.filter.RestfulUsernamePasswordAuthenticationFilter;
import org.springframework.security.web.access.ExceptionTranslationFilter;
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
import org.springframework.security.web.access.intercept.AuthorizationFilter;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter;
import org.springframework.security.web.authentication.switchuser.SwitchUserFilter;
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.authentication.www.DigestAuthenticationFilter;
import org.springframework.security.web.context.SecurityContextHolderFilter;
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.header.HeaderWriterFilter;
import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter;
import org.springframework.security.web.savedrequest.RequestCacheAwareFilter;
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter;
import org.springframework.security.web.session.ConcurrentSessionFilter;
import org.springframework.security.web.session.DisableEncodeUrlFilter;
import org.springframework.security.web.session.ForceEagerSessionCreationFilter;
import org.springframework.security.web.session.SessionManagementFilter;
import org.springframework.web.filter.CorsFilter;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.Map;

final class FilterOrderRegistration {


    private static final int INITIAL_ORDER = 100;

    private static final int ORDER_STEP = 100;

    private final Map<String, Integer> filterToOrder = new HashMap<>();

    FilterOrderRegistration() {
      Step order = new Step(INITIAL_ORDER, ORDER_STEP);
      put(DisableEncodeUrlFilter.class, order.next());
      put(ForceEagerSessionCreationFilter.class, order.next());
      put(ChannelProcessingFilter.class, order.next());
      order.next(); // gh-8105
      put(WebAsyncManagerIntegrationFilter.class, order.next());
      put(SecurityContextHolderFilter.class, order.next());
      put(SecurityContextPersistenceFilter.class, order.next());
      put(HeaderWriterFilter.class, order.next());
      put(CorsFilter.class, order.next());
      put(CsrfFilter.class, order.next());
      put(LogoutFilter.class, order.next());
      this.filterToOrder.put(
                "org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter",
                order.next());
      this.filterToOrder.put(
                "org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter",
                order.next());
      put(X509AuthenticationFilter.class, order.next());
      put(AbstractPreAuthenticatedProcessingFilter.class, order.next());
      this.filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter", order.next());
      this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter",
                order.next());
      this.filterToOrder.put(
                "org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter",
                order.next());
      //添加自定义过滤器
      put(RestfulUsernamePasswordAuthenticationFilter.class, order.next());
      put(UsernamePasswordAuthenticationFilter.class, order.next());
      order.next(); // gh-8105
      this.filterToOrder.put("org.springframework.security.openid.OpenIDAuthenticationFilter", order.next());
      put(DefaultLoginPageGeneratingFilter.class, order.next());
      put(DefaultLogoutPageGeneratingFilter.class, order.next());
      put(ConcurrentSessionFilter.class, order.next());
      put(DigestAuthenticationFilter.class, order.next());
      this.filterToOrder.put(
                "org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter",
                order.next());
      put(BasicAuthenticationFilter.class, order.next());
      put(RequestCacheAwareFilter.class, order.next());
      put(SecurityContextHolderAwareRequestFilter.class, order.next());
      put(JaasApiIntegrationFilter.class, order.next());
      put(RememberMeAuthenticationFilter.class, order.next());
      put(AnonymousAuthenticationFilter.class, order.next());
      this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter",
                order.next());
      put(SessionManagementFilter.class, order.next());
      put(ExceptionTranslationFilter.class, order.next());
      put(FilterSecurityInterceptor.class, order.next());
      put(AuthorizationFilter.class, order.next());
      put(SwitchUserFilter.class, order.next());
    }

    /**
   * Register a {@link Filter} with its specific position. If the {@link Filter} was
   * already registered before, the position previously defined is not going to be
   * overriden
   *
   * @param filter   the {@link Filter} to register
   * @param position the position to associate with the {@link Filter}
   */
    void put(Class<? extends Filter> filter, int position) {
      String className = filter.getName();
      if (this.filterToOrder.containsKey(className)) {
            return;
      }
      this.filterToOrder.put(className, position);
    }

    /**
   * Gets the order of a particular {@link Filter} class taking into consideration
   * superclasses.
   *
   * @param clazz the {@link Filter} class to determine the sort order
   * @return the sort order or null if not defined
   */
    Integer getOrder(Class<?> clazz) {
      while (clazz != null) {
            Integer result = this.filterToOrder.get(clazz.getName());
            if (result != null) {
                return result;
            }
            clazz = clazz.getSuperclass();
      }
      return null;
    }

    private static class Step {

      private final int stepSize;
      private int value;

      Step(int initialValue, int stepSize) {
            this.value = initialValue;
            this.stepSize = stepSize;
      }

      int next() {
            int value = this.value;
            this.value += this.stepSize;
            return value;
      }

    }

}3、创建RestfulUsernamePasswordAuthenticationFilter


[*]参考UsernamePasswordAuthenticationFilter
[*]将参数获取方式从request.getParameter改为从body体中
[*]创建UsernamePasswordAuthenticationToken
[*]设置细节
[*]调用getAuthenticationManager()的authenticate方法获取认证信息
package com.yu.demo.spring.filter;

import com.yu.demo.util.SpringUtil;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;

/**
* 自定义前后端分离/restful方式的用户名密码认证过滤器
* 参考UsernamePasswordAuthenticationFilter
*/
public class RestfulUsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    //是否只支持post方法
    private final boolean postOnly;
    private final String username;
    private final String password;

    public RestfulUsernamePasswordAuthenticationFilter(String username, String password, String loginUrl, String httpMethod) {
      super(new AntPathRequestMatcher(loginUrl, httpMethod));
      postOnly = HttpMethod.POST.name().equals(httpMethod);
      this.username = username;
      this.password = password;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException {
      if (this.postOnly && !request.getMethod().equals(HttpMethod.POST.name())) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
      } else {
            Map<String, String> body = SpringUtil.rawBodyToMap(request);
            String name = body.get(username);
            String pswd = body.get(password);
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(name, pswd);
            setDetails(request, authRequest);
            return getAuthenticationManager().authenticate(authRequest);
      }
    }

    protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
      authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }

}4、创建自定义用户名密码认证过滤器配置类RestfulLoginConfigurer


[*]参考FormLoginConfigurer
[*]注册自定义用户名密码认证过滤器RestfulUsernamePasswordAuthenticationFilter
[*]设置登录地址和哀求方式
package com.yu.demo.spring.filter;

import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;

/**
* 自定义前后端分离/restful方式的用户名密码验证过滤器配置器,用于注册认证过滤器
* 参考FormLoginConfigurer
*/
public class RestfulLoginConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractAuthenticationFilterConfigurer<H, RestfulLoginConfigurer<H>, RestfulUsernamePasswordAuthenticationFilter> {
    private final String loginMethod;

    public RestfulLoginConfigurer(RestfulUsernamePasswordAuthenticationFilter authenticationFilter, String defaultLoginProcessingUrl, String loginMethod) {
      super(authenticationFilter, defaultLoginProcessingUrl);
      this.loginMethod = loginMethod;
    }

    @Override
    public RestfulLoginConfigurer<H> loginPage(String loginPage) {
      return super.loginPage(loginPage);
    }

    @Override
    public void init(H http) throws Exception {
      super.init(http);
    }

    @Override
    protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl) {
      return new AntPathRequestMatcher(loginProcessingUrl, loginMethod);
    }
}三、自定义安全上下文仓库SecurityContextRepositoryImpl


[*]基于分布式缓存实现安全上下文仓库
[*]获取上下文时从哀求头中获取token,通过token从缓存中获取上下文,不存在时返回空值安全上下文
[*]保存上下文时从哀求头或者登任命户信息中获取token,将token和上下文保存到缓存中
1、分布式缓存接口和实现

package com.yu.demo.manager;

import org.springframework.security.core.context.SecurityContext;

public interface CacheManager {

    /**
   * 通过token获取认证信息
   *
   * @param token token
   * @return 认证信息
   */
    SecurityContext getSecurityContext(String token);

    /**
   * 是否包含token
   *
   * @param token token
   * @return 是否包含token
   */
    boolean contains(String token);

    /**
   * 通过token添加认证信息
   *
   * @param token         token
   * @param securityContext 认证信息
   */
    void addSecurityContext(String token, SecurityContext securityContext);

    /**
   * 通过token删除认证信息
   *
   * @param token token
   */
    void deleteSecurityContext(String token);

}为演示方便,这里采用过期Map,实际使用将map改为redis或者其他分布式缓存即可
package com.yu.demo.manager.impl;

import com.yu.demo.manager.CacheManager;
import net.jodah.expiringmap.ExpirationPolicy;
import net.jodah.expiringmap.ExpiringMap;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;

@Component
public class CacheManagerImpl implements CacheManager {

    private static ExpiringMap<String, SecurityContext> SECURITY_CONTEXT_CACHE;

    @PostConstruct
    public void init() {
      SECURITY_CONTEXT_CACHE = ExpiringMap.builder().maxSize(200).expiration(30, TimeUnit.MINUTES).expirationPolicy(ExpirationPolicy.ACCESSED).variableExpiration().build();
    }

    @Override
    public SecurityContext getSecurityContext(String token) {
      return SECURITY_CONTEXT_CACHE.get(token);
    }

    @Override
    public boolean contains(String token) {
      return SECURITY_CONTEXT_CACHE.containsKey(token);
    }

    @Override
    public void addSecurityContext(String token, SecurityContext securityContext) {
      SECURITY_CONTEXT_CACHE.put(token, securityContext);
    }

    @Override
    public void deleteSecurityContext(String token) {
      SECURITY_CONTEXT_CACHE.remove(token);
    }
}2、创建SecurityContextRepositoryImpl

package com.yu.demo.spring.impl;

import com.yu.demo.entity.UserDetailsImpl;
import com.yu.demo.manager.CacheManager;
import com.yu.demo.util.SecurityUtil;
import org.apache.poi.util.StringUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpRequestResponseHolder;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class SecurityContextRepositoryImpl implements SecurityContextRepository {

    private static final String AUTHENTICATION = "Authentication";
    @Autowired
    private CacheManager cacheManager;

    @Override
    public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
      //获取请求头中的token,未登录访问系统时Token为空
      String token = requestResponseHolder.getRequest().getHeader(AUTHENTICATION);
      if (StringUtil.isNotBlank(token)) {
            SecurityContext securityContext = cacheManager.getSecurityContext(token);
            //securityContext已过期时为空
            if (SecurityUtil.isNotAuthenticated(securityContext)) {
                return SecurityContextHolder.createEmptyContext();
            }
            UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) securityContext.getAuthentication();
            UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
            if (token.equals(userDetails.getToken())) {
                //测试过程中伪造的Token(不修改header和body,只修改signature部分字符)有概率出现可以解析成功的情况,可能是secret太短的原因,未深究,所以这里在验证下输入的Token和缓存中的token
                return securityContext;
            }
      }
      return SecurityContextHolder.createEmptyContext();
    }

    @Override
    public void saveContext(SecurityContext securityContext, HttpServletRequest request, HttpServletResponse response) {
      //获取请求头中的token(登出时有,登录时没有)
      String token = request.getHeader(AUTHENTICATION);
      UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) securityContext.getAuthentication();
      if (StringUtil.isBlank(token) && SecurityUtil.isNotAuthenticated(securityContext)) {
            //未登录、验证码、用户名密码校验失败
            return;
      }
      //第一次登录时Token为空
      if (StringUtil.isBlank(token)) {
            UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
            //登录成功
            cacheManager.addSecurityContext(userDetails.getToken(), securityContext);
            return;
      }
      //退出或token过期(缓存中设置token过期时间)
      if (SecurityUtil.isNotAuthenticated(securityContext)) {
            cacheManager.deleteSecurityContext(token);
            return;
      }
      //更新Token
      cacheManager.addSecurityContext(token, securityContext);
    }

    @Override
    public boolean containsContext(HttpServletRequest request) {
      //本版本的Spring Security只有SessionManagementFilter中调用该方法
      //已禁用SessionManagementFilter,该方法不会被调用
      String token = request.getHeader(AUTHENTICATION);
      if (StringUtil.isBlank(token)) {
            return false;
      }
      if (StringUtil.isBlank(token)) {
            return false;
      }
      return cacheManager.contains(token);
    }

}四、自定义用户详情UserDetailsImpl

package com.yu.demo.entity;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.Set;

@Setter
@Getter
@ToString
public class UserDetailsImpl implements UserDetails {
    private String password;
    private final String username;
    private final Set<GrantedAuthority> authorities;
    private final boolean accountNonExpired;
    private final boolean accountNonLocked;
    private final boolean credentialsNonExpired;
    private final boolean enabled;
    /**
   * token
   */
    private String token;

    public UserDetailsImpl(String username, String password, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, boolean enabled, Set<GrantedAuthority> grantedAuthorities) {
      this.username = username;
      this.password = password;
      this.enabled = enabled;
      this.accountNonExpired = accountNonExpired;
      this.credentialsNonExpired = credentialsNonExpired;
      this.accountNonLocked = accountNonLocked;
      this.authorities = grantedAuthorities;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
      return authorities;
    }

    @Override
    public String getPassword() {
      return password;
    }

    @Override
    public String getUsername() {
      return username;
    }

    /**
   * 账号是否未过期
   *
   * @return true:是,false:否
   */
    @Override
    public boolean isAccountNonExpired() {
      return accountNonExpired;
    }

    /**
   * 账号是否未锁定
   *
   * @return true:是,false:否
   */
    @Override
    public boolean isAccountNonLocked() {
      return accountNonLocked;
    }

    /**
   * 密码是否未过期
   *
   * @return true:是,false:否
   */
    @Override
    public boolean isCredentialsNonExpired() {
      return credentialsNonExpired;
    }

    /**
   * 账号是否启用
   *
   * @return true:是,false:否
   */
    @Override
    public boolean isEnabled() {
      return enabled;
    }
}九、案例源码获取


[*]下载地址
[*]私聊、评论区、+V均可

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 七、Spring Boot集成Spring Security之前后分离认证最佳实现