SpringSecurity6--认证和授权的原理

news2024/12/21 5:56:31

SpringSecurity6–认证和授权的原理,项目gitee地址在文章末尾。

文章目录

      • 一、Spring Security简介
      • 二、Spring Security框架中认证流程中几个非常重要的类
          • 1、 `FilterChainProxy`
          • 2、 `AbstractAuthenticationProcessingFilter`
          • 3、 `UsernamePasswordAuthenticationFilter`
          • 4、`AbstractAuthenticationToken`
          • 5、 `UsernamePasswordAuthenticationToken`
          • 6、 `AuthenticationManager`
          • 7、 `ProviderManager`
          • 8、`AbstractUserDetailsAuthenticationProvider`
          • 9、 `UserDetailsService`
          • 10、 `DaoAuthenticationProvider`
          • 11、`AuthenticationSuccessHandler`
          • 12、`AuthenticationFailureHandler`
      • 三、Spring Security框架认证流程
      • 四、编写新的认证方式
          • 1、编写新的认证方式的步骤
          • 2、编写新的认证方式----短信登录("/codeLogin")

一、Spring Security简介

Spring Security是一个基于Spring框架的安全解决方案,提供了认证和授权等安全方面的大服务,包括身份认证和权限处理两大服务。Spring Security的实现依赖于大量的过滤器,采用责任链模式对请求请求不同的过滤处理。在日常使用中,Spring Security已经实现了基于表单的登录认证和授权模式,只需简单的配置,即可做到拿来即用。当然Spring Security也提供了灵活的其他认证入口,通过实现其暴漏出来的接口即可自定义自己的登录认证和授权方法。

  • 认证(Authentication):是Spring Security识别当前登录用户身份的主要方式。
  • 授权(Authorization):是Spring Security对当前认证用户权限管理的主要方式。

二、Spring Security框架中认证流程中几个非常重要的类

1、 FilterChainProxy

这是一个Spring Security框架中非常重要的类,它用于维护一组过滤器连,属于Servlet级别的过滤器。所有的请求在进入WEB容器之前都会经过该过滤器,该过滤器会对请求进行不同程度的拦截和处理。FilterChainProxy根据不同的请求获取对应的过滤器链组,之后按照顺序执行该请求对应的过滤器链组中的每一个过滤器的逻辑对该请求进行处理,直到所有的过滤器都执行完成。FilterChainProxy实际上是一个Filter,它的doFilter方法最终会调用自己内部的doFilterInternal方法。

public class FilterChainProxy extends GenericFilterBean {

	private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		……省略部分代码……
		
		// 根据请求类型,获取请求下的所有的过滤器
		List<Filter> filters = getFilters(firewallRequest);

		// 循环执行匹配到该请求下的所有的过滤器
		FilterChain reset = (req, res) -> {
			// Deactivate path stripping as we exit the security filter chain
			firewallRequest.reset();
			chain.doFilter(req, res);
		};
		this.filterChainDecorator.decorate(reset, filters).doFilter(firewallRequest, firewallResponse);
	}
}

2、 AbstractAuthenticationProcessingFilter

这个是认证过程中抽出来的抽象类,自定义的过滤器可直接实现这个抽象类自定义过滤方式,UsernamePasswordAuthenticationFilter就是直接实现的这个抽象类。AbstractAuthenticationProcessingFilter实际上是一个Filter,它的doFilter方法最终会调用自己内部的attemptAuthentication方法。这是一个抽象方法,需要子类实现它,UsernamePasswordAuthenticationFilter就是直接实现该抽象类做到,实现了attemptAuthentication方法。

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean  
implements ApplicationEventPublisherAware, MessageSourceAware {
@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
	}

	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);
			return;
		}
		try {
			// 调用内部的attemptAuthentication方法,该方法是抽象方法。需要子类实现。
			Authentication authenticationResult = attemptAuthentication(request, response);
			if (authenticationResult == null) {
				// return immediately as subclass has indicated that it hasn't completed
				return;
			}
			this.sessionStrategy.onAuthentication(authenticationResult, request, response);
			// Authentication success
			if (this.continueChainBeforeSuccessfulAuthentication) {
				chain.doFilter(request, response);
			}
			// 认证成功之后会调用该方法处理认证成功之后的逻辑。
			successfulAuthentication(request, response, chain, authenticationResult);
		}
		catch (InternalAuthenticationServiceException failed) {
			this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
			unsuccessfulAuthentication(request, response, failed);
		}
		catch (AuthenticationException ex) {
			// Authentication failed
			// 认证失败会调用该方法处理认证失败之后的逻辑。
			unsuccessfulAuthentication(request, response, ex);
		}
	}
}

3、 UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilteAbstractAuthenticationProcessingFilter的直接实现,也是Spring Security表单登录的默认实现过滤器,主要是将表单登录的所需要的usernamepassword从request中获取出来封装成还未认证过的UsernamePasswordAuthenticationToken,它实现了AbstractAuthenticationProcessingFilter的抽象方法attemptAuthentication方法,最终将登录逻辑的具体实现委托给AuthenticationManager去实现,而AuthenticationManager是一个接口,里面只有一个抽象方法authenticate,必须由子类实现,而最直接的实现该接口的类就是ProviderManager

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
		if (this.postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
		}
		// 从请求中获取username参数
		String username = obtainUsername(request);
		username = (username != null) ? username.trim() : "";
		// 从请求中获取password参数
		String password = obtainPassword(request);
		password = (password != null) ? password : "";
		// 构造一个没有完全认证的AuthenticationToken,最主要的是就是用来存放未认证之前的用户信息的。未认证之前,AuthenticationToken里面的authenticated属性为false,认证通过直接会被设置为true。
		UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
				password);
		// Allow subclasses to set the "details" property
		// 实际上就是保存了一下用户的详细信息
		setDetails(request, authRequest);
		// 调用AuthenticationManager的authenticate方法处理认证逻辑,实际上会调到具体的子类实现上。
		return this.getAuthenticationManager().authenticate(authRequest);
	}
}
4、AbstractAuthenticationToken

AbstractAuthenticationToken是一个用来存放用户信息的抽象类,比如表单登录的usernamepassword就会被存放在该类的实现中,而它的直接实现就是UsernamePasswordAuthenticationTokenAbstractAuthenticationToken里面有几个重要的属性:

public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer {
	// 当前登录用户的权限集合
	private final Collection<GrantedAuthority> authorities;  
	// 用户信息的详细信息
	private Object details;  
	// 当前登录用户是否完全认证。 未认证之前,authenticated属性为false,认证通过直接会被设置为true。
	private boolean authenticated = false;
}

5、 UsernamePasswordAuthenticationToken

UsernamePasswordAuthenticationTokenAbstractAuthenticationToken的直接实现,也是Spring Security表单登录的默认实现用来存放用户信息的类。该类的作用就是存放未认证之前的用户信息,也就是表单提交的username和password。

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
	// 在父类的基础之上,UsernamePasswordAuthenticationToken增加了该属性信息,源码实现的时候,未认证之前该属性代表的是username,认证之后存放的是UserDetails
	private final Object principal;  
	// 在父类的基础之上,UsernamePasswordAuthenticationToken增加了该属性信息,源码实现的时候,未认证之前该属性代表的是password,认证之后存放的是认证主体Authentication的一些信息
	private Object credentials;
}
6、 AuthenticationManager

AuthenticationManager是实际上用来做认证的接口,内部就一个authenticate抽象方法,子类必须实现它,ProviderManagerAuthenticationManager的最直接的实现。

public interface AuthenticationManager {
	// 实际上的认证入口
	Authentication authenticate(Authentication authentication) throws AuthenticationException;
}

7、 ProviderManager

ProviderManagerAuthenticationManager的最直接的实现,实现了authenticate抽象方法,也是Spring Security表单登录的默认实现认证相关的类,认证的处理逻辑就是在这个里面处理的。而在 ProviderManager又把认证逻辑委托给了对应的AbstractUserDetailsAuthenticationProvider,他是实际上干活的类,认证逻辑最终在这里执行。

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
@Override
	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;
		int currentPosition = 0;
		int size = this.providers.size();
		// 循环ProviderManager管理的provider,找到那个与之相匹配的provider
		for (AuthenticationProvider provider : getProviders()) {
			// 找到那个与之相匹配的provider,比如表单登录的ProviderManager仅支持DaoAuthenticationProvider
			if (!provider.supports(toTest)) {
				continue;
			}

			try {
				// 将认证信息委托给对应的p进行处理,默认是DaoAuthenticationProvider
				result = provider.authenticate(authentication);
				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException | InternalAuthenticationServiceException ex) {
				prepareException(ex, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw ex;
			}
			catch (AuthenticationException ex) {
				lastException = ex;
			}
		}
		if (result == null && this.parent != null) {
			// Allow the parent to try.
			try {
				// 假设子类provider认证失败了,会继续使用父类的provider进行认证。但是实际上对于表单登录来说,还是ProviderManager
				parentResult = this.parent.authenticate(authentication);
				result = parentResult;
			}
			catch (ProviderNotFoundException ex) {
				// ignore as we will throw below if no other exception occurred prior to
				// calling parent and the parent
				// may throw ProviderNotFound even though a provider in the child already
				// handled the request
			}
			catch (AuthenticationException ex) {
				parentException = ex;
				lastException = ex;
			}
		}
		
		throw lastException;
	}
}

8、AbstractUserDetailsAuthenticationProvider

AbstractUserDetailsAuthenticationProvider是实际上将认证所需要的信息进行收集的抽象类,它重写了authenticate方法,它主要就是根据request中的username从数据库中的获取到对应的用户信息,该操作是交给UserDetailsService接口来实现的,子类需要实现它,并且实现loadUserByUsername方法,并将用户信息封装成UserDetails对象。最终调用它的直接实现类DaoAuthenticationProvider进行处理,并将将从数据库中查询出来的用户信息UserDetails对象和从request中获取的用户信息UsernamePasswordAuthenticationToken传给该实现类的additionalAuthenticationChecks方法进行比对,主要是比对的两个用户信息的密码是否匹配,如果匹配就说明认证成功,否则,认证失败。

public abstract class AbstractUserDetailsAuthenticationProvider  
implements AuthenticationProvider, InitializingBean, MessageSourceAware {
@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
						"Only UsernamePasswordAuthenticationToken is supported"));
		String username = determineUsername(authentication);
		boolean cacheWasUsed = true;
		UserDetails user = this.userCache.getUserFromCache(username);
		if (user == null) {
			cacheWasUsed = false;
			try {
				// 这个方法最终会调用UserDetailsService的loadUserByUsername方法,最终根据username获取用户信息。具体获取用户信息的方式依据子类的实现
				user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
			}

		catch (AuthenticationException ex) {
			if (!cacheWasUsed) {
				throw ex;
			}
			// There was a problem, so try again after checking
			// we're using latest data (i.e. not from the cache)
			cacheWasUsed = false;
			user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
			this.preAuthenticationChecks.check(user);
			// 这个方法最终会调用DaoAuthenticationProvider的additionalAuthenticationChecks方法进行密码比对,如果匹配就说明认证成功,否则,认证失败。
			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
		}

		// 认证如果能走到这一步,那就说明认证成功了。该方法最终返回一个认证过的UsernamePasswordAuthenticationToken对象,其authenticated的属性的值为true
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}
}

9、 UserDetailsService

UserDetailsService是一个接口,主要是提供一个入口,让子类实现根据username从获取到对应的用户信息的方式,一般情况都是根据username从数据库中获取用户信息。

public interface UserDetailsService {  
  
/**  
* Locates the user based on the username. In the actual implementation, the search  
* may possibly be case sensitive, or case insensitive depending on how the  
* implementation instance is configured. In this case, the <code>UserDetails</code>  
* object that comes back may have a username that is of a different case than what  
* was actually requested..  
* @param username the username identifying the user whose data is required.  
* @return a fully populated user record (never <code>null</code>)  
* @throws UsernameNotFoundException if the user could not be found or the user has no  
* GrantedAuthority  
*/  
// 子类必须实现该方法,实现根据username获取用户信息的逻辑
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;  
  
}

10、 DaoAuthenticationProvider

DaoAuthenticationProviderAbstractUserDetailsAuthenticationProvider的直接实现,也是spring security表单登录种密码匹配的默认实现。它匹配密码的方式实际就是调用了PasswordEncodermatches方法进行匹配,如果匹配,登录认证成功,否则失败。

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
	@Override
	@SuppressWarnings("deprecation")
	protected void additionalAuthenticationChecks(UserDetails userDetails,
			UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
		if (authentication.getCredentials() == null) {
			this.logger.debug("Failed to authenticate since no credentials provided");
			throw new BadCredentialsException(this.messages
					.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
		}
		String presentedPassword = authentication.getCredentials().toString();
		// 直接调用PasswordEncoder的matches方法进行密码匹配
		if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
			this.logger.debug("Failed to authenticate since password does not match stored value");
			throw new BadCredentialsException(this.messages
					.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
		}
	}
}

11、AuthenticationSuccessHandler

AuthenticationSuccessHandler是认证成功处理类,是一个接口,需要子类去显示它,并实现onAuthenticationSuccess方法,通过实现该方法,可以自定义返回结果。比如前后端分离项目,后端认证成功之后,需要返回一个JSON格式的数据给前台,那么就可以实现AuthenticationSuccessHandleronAuthenticationSuccess方法处理。

public interface AuthenticationSuccessHandler {
	void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException;
}
12、AuthenticationFailureHandler

AuthenticationFailureHandler是认证失败处理类,是一个接口,需要子类去显示它,并实现onAuthenticationFailure方法,通过实现该方法,可以自定义返回结果。比如前后端分离项目,后端认证失败之后,需要返回一个JSON格式的数据给前台,那么就可以实现AuthenticationFailureHandleronAuthenticationFailure方法处理。

public interface AuthenticationFailureHandler {
	void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException;
}

三、Spring Security框架认证流程

请添加图片描述

四、编写新的认证方式

1、编写新的认证方式的步骤

① 构建一个新的AuthenticationFilter业务类,继承AbstractAuthenticationProcessingFilter抽象类,可参照UsernamePasswordAuthenticationFilter
② 构建一个新的AuthenticationToken业务类,实现AbstractAuthenticationToken抽象类,可参照UsernamePasswordAuthenticationToken
③ 构建一个AuthenticationManager业务类,实现AuthenticationManager接口,实现authenticate方法,可参照ProviderManager,也可以直接使用ProviderManager,只是需要将自定义的AuthenticationProvider指定到该ProviderManager对象当中。
③ 构建一个AuthenticationProvider业务类,实现AuthenticationProvider接口,实现authenticate方法,可参照DaoAuthenticationProvider
④ 构建两个Handle,一个是认证成功之后的处理器,实现AuthenticatonSuccessHandler接口,重写onAuthenticationSuccess方法,处理认证成功之后的逻辑处理,一个是认证失败之后的处理,实现AuthenticationFailureHandler接口,重写onAuthenticationFailure方法,处理认证失败之后的逻辑。
⑤ 构建一个UserDetailsService,实现UserDetailsService接口,实现loadUserByUsername方法,自定义加载用户信息的逻辑,可参照CachingUserDetailsService
⑥ 构建一个UserDetails,实现UserDetails接口,自定义用户信息属性。
⑦ 构建JWT存储库,用来存储token。
⑧ 构建一个AuthenticationFilterConfigurer配置类,实现AuthenticationFilterConfigurer接口,自定义配置新的认证方式。
⑨ 构建配置类,注入SecurityFilterChain类。

2、编写新的认证方式----短信登录(“/codeLogin”)

① 构建一个新的OrkasgbSMSAuthencationFilter业务类,继承AbstractAuthenticationProcessingFilter抽象类,可参照UsernamePasswordAuthenticationFilter

/**
 * OrkasgbSMSAuthencationFilter类在启用了Spring Security的应用程序中实现了一个定制的基于SMS的身份验证过滤器。
 * 它检查带有所需参数的POST请求,创建身份验证令牌,如果成功,则返回经过身份验证的用户对象。
 *
 * @date Sep 21, 2010 4:21:21 PM
 * @since 0.0.5
 */
@Component
public class OrkasgbSMSAuthencationFilter extends AbstractAuthenticationProcessingFilter {

    /**
     * 自定义请求路经为"/codeLogin",请求方式为POST
     */
    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/codeLogin",
            "POST");

    /**
     * 构造函数,构造是时需要做以下操作:
     * <ol>
     *    1. 设置需要身份验证的请求路径为DEFAULT_ANT_PATH_REQUEST_MATCHER;
     *    2. 设置身份验证过滤器的匹配器,因为使用的还是默认的ProviderManager实现,所有需要手动将Provider替换为自定义的
     *       OrkasgbSMSAuthenticationProvider;
     * </ol>
     */
    public OrkasgbSMSAuthencationFilter() {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
        OrkasgbSMSAuthenticationProvider orkasgbSMSAuthenticationProvider =
                new OrkasgbSMSAuthenticationProvider();
        ProviderManager providerManager = new ProviderManager(orkasgbSMSAuthenticationProvider);
        // 这里必须调用手动设置下providerManager,否则会使用默认的,那么最终几不会走到自定义的SMS身份验证过滤器的代码块中。
        this.setAuthenticationManager(providerManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }

        String phoneNumber = request.getParameter("phoneNumber");
        String code = request.getParameter("code");

        // 此处构造的是一个还未认证的OrkasgbSMSAuthenticationToken,即里面的authenticated属性为false。
        OrkasgbSMSAuthenticationToken orkasgbSMSAuthenticationToken = new OrkasgbSMSAuthenticationToken(phoneNumber, code);
        setDetails(request, orkasgbSMSAuthenticationToken);
        // 认证方式委托为providerManager,providerManager又会委托给orkasgbSMSAuthenticationProvider来执行身份验证。
        return this.getAuthenticationManager().authenticate(orkasgbSMSAuthenticationToken);
    }

    @Override
    public void setAuthenticationManager(AuthenticationManager authenticationManager) {
        super.setAuthenticationManager(authenticationManager);
    }

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

    @Override
    public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) {
        super.setSecurityContextRepository(securityContextRepository);
    }
}

② 构建一个新的OrkasgbSMSAuthenticationToken业务类,实现AbstractAuthenticationToken抽象类,可参照UsernamePasswordAuthenticationToken

public class OrkasgbSMSAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = -8447558989998984221L;

    private final Object principal;

    private final Object credentials;

    public OrkasgbSMSAuthenticationToken(Object phoneNumber, Object code) {
        super(null);
        this.credentials = phoneNumber;
        this.principal = code;
        // 
        setAuthenticated(false);
    }

    public OrkasgbSMSAuthenticationToken(Object phoneNumber, Object userDetails, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.credentials = phoneNumber;
        this.principal = userDetails;
        super.setAuthenticated(true); // must use super, as authorities must be set
    }

    @Override
    public Object getCredentials() {
        return this.credentials;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException("Cannot set this token to trusted");
        }
        super.setAuthenticated(false);
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + "[principal=" + principal + ", authenticated=" + isAuthenticated() + "]";
    }
}

③ 构建一个AuthenticationManager业务类,实现AuthenticationManager接口,实现authenticate方法,可参照ProviderManager,也可以直接使用ProviderManager,只是需要将自定义的AuthenticationProvider指定到该ProviderManager对象当中。

OrkasgbSMSAuthenticationProvider orkasgbSMSAuthenticationProvider =  
new OrkasgbSMSAuthenticationProvider();  
ProviderManager providerManager = new ProviderManager(orkasgbSMSAuthenticationProvider);  
// 这里必须调用手动设置下providerManager,否则会使用默认的,那么最终几不会走到自定义的SMS身份验证过滤器的代码块中。  
this.setAuthenticationManager(providerManager);

③ 构建一个OrkasgbSMSAuthenticationProvider业务类,实现AuthenticationProvider接口,实现authenticate方法,可参照DaoAuthenticationProvider

/**
 * 真正认证的处理类,具体实现了根据短信验证码进行身份验证的功能。主要是通过手机号码获取到验证码,进行匹配。
 *
 * @date
 * @since 1.0.0
 */
@Component
public class OrkasgbSMSAuthenticationProvider implements AuthenticationProvider {

    private UserCache userCache = new NullUserCache();

    private final OrkasgbSMSUserDetailsService orkasgbSMSUserDetailsService;

    /**
     * 构造的时候将实际上获取用户信息的实现类注入
     */
    public OrkasgbSMSAuthenticationProvider() {
        this.orkasgbSMSUserDetailsService = new OrkasgbSMSUserDetailsService();
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        OrkasgbSMSAuthenticationToken orkasgbSMSAuthenticationToken = (OrkasgbSMSAuthenticationToken) authentication;
        String phoneNumber = (String) orkasgbSMSAuthenticationToken.getCredentials();
        UserDetails orkasgbSMSUserDetails = this.orkasgbSMSUserDetailsService.loadUserByUsername(phoneNumber);
        if (Objects.isNull(orkasgbSMSUserDetails)) {
            throw new IllegalStateException("User not found");
        }

        // 这里的验证码实际上是从前端传过来的,而实际上保存在后台的验证码是通过orkasgbSMSUserDetailsService从其他存储介质中获取的,
        // 实际上就是前后端的验证做一个匹配验证,匹配不通过将会抛出异常。
        String code = (String) orkasgbSMSAuthenticationToken.getPrincipal();
        if (!StringUtils.equals(code, orkasgbSMSUserDetails.getPassword())) {
            throw new BadCredentialsException("Bad credentials");
        }

        // 验证通过之后,就会构造一个新的Authentication对象,这个是完全认证过的对象,里面有用户的授权信息,并且里面的authenticated属性为true。
        OrkasgbSMSAuthenticationToken orkasgbSMSAuthenticationedToken =
                new OrkasgbSMSAuthenticationToken(phoneNumber, orkasgbSMSUserDetails, orkasgbSMSUserDetails.getAuthorities());
        orkasgbSMSAuthenticationedToken.setDetails(orkasgbSMSUserDetails);

        SecurityContextHolder.getContext().setAuthentication(orkasgbSMSAuthenticationedToken);
        return orkasgbSMSAuthenticationedToken;
    }

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

④ 构建两个Handle,一个是认证成功之后的处理器,实现AuthenticatonSuccessHandler接口,重写onAuthenticationSuccess方法,处理认证成功之后的逻辑处理,一个是认证失败之后的处理,实现AuthenticationFailureHandler接口,重写onAuthenticationFailure方法,处理认证失败之后的逻辑。

/**
 * 自定义成功处理器,用于处理登录成功的情况。
 * <p>
 * 仅将成功消息提供给前端,不受其他现有处理器的影响。
 */
@Component("orkasgbSuccessHandle")
public class OrkasgbSuccessHandle implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding(StandardCharsets.UTF_8.displayName());
        response.setHeader(HttpHeaders.AUTHORIZATION, request.getAttribute("accessToken").toString());
        response.getWriter().write(CommonResult.ofSuccess(authentication, "登录成功!").toString());
    }
}

/**
 * 自定义失败处理器,用于处理登录失败的情况。
 * <p>
 * 仅将错误消息提供给前段,不受其他现有处理器的影响。
 */
@Component("orkasgbFailureHandler")
public class OrkasgbFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
        response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding(StandardCharsets.UTF_8.displayName());
        System.out.println("认证失败" + exception.getMessage());
        response.getWriter().write(CommonResult.ofFailure("认证失败," +  exception.getMessage()).toString());
    }
}

⑤ 构建一个UserDetailsService,实现UserDetailsService接口,实现loadUserByUsername方法,自定义加载用户信息的逻辑,可参照CachingUserDetailsService

/**
 *  用于检查用户名和密码是否正确的服务器。实现自己的UserDetailService接口,并实现这个接口的方法。主要是自定义方法来返回用户信息。
 *
 * @date
 * @since
 */
@Component("orkasgbSMSUserDetailsService")
public class OrkasgbSMSUserDetailsService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String phoneNumber) throws UsernameNotFoundException {

        // 模拟从其他介质中查询验证码
        String code = this.getCodeByPhoneNumber(phoneNumber);// throw exception if not found
        List<SimpleGrantedAuthority> grantedAuthorities = List.of(new SimpleGrantedAuthority("ADMIN2"));
        // 构造userDetails
        return new OrkasgbSMSUserDetails(phoneNumber, code, grantedAuthorities);
    }

    private String getCodeByPhoneNumber(String phoneNumber) {
        return "239802";
    }
}

⑥ 构建一个UserDetails,实现UserDetails接口,自定义用户信息属性。

/**
 * 短信登录用户详细信息主体,用于存储用户信息的类。
 *
 * @date Sep 6, 2017 7:21:40 PM
 * @since 0.0.6
 */
@Data
public class OrkasgbSMSUserDetails implements UserDetails {


    private String phoneNumber;

    private String code;

    private final Set<GrantedAuthority> authorities;

    public OrkasgbSMSUserDetails(String phoneNumber, String code, Collection<? extends GrantedAuthority> authorities) {
        this.phoneNumber = phoneNumber;
        this.code = code;
        this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
    }

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

    @Override
    public String getPassword() {
        return this.code;
    }

    @Override
    public String getUsername() {
        return this.phoneNumber;
    }

    @Override
    public boolean isAccountNonExpired() {
        return false;
    }

    @Override
    public boolean isAccountNonLocked() {
        return false;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return false;
    }

    @Override
    public boolean isEnabled() {
        return false;
    }

    private static SortedSet<GrantedAuthority> sortAuthorities(Collection<? extends GrantedAuthority> authorities) {
        Assert.notNull(authorities, "Cannot pass a null GrantedAuthority collection");
        // Ensure array iteration order is predictable (as per
        // UserDetails.getAuthorities() contract and SEC-717)
        SortedSet<GrantedAuthority> sortedAuthorities = new TreeSet<>(
                Comparator.comparing(GrantedAuthority::getAuthority));
        for (GrantedAuthority grantedAuthority : authorities) {
            Assert.notNull(grantedAuthority, "GrantedAuthority list cannot contain any null elements");
            sortedAuthorities.add(grantedAuthority);
        }
        return sortedAuthorities;
    }
}

⑦ 构建JWT存储库,用来存储token。

/**
 * 自定义JWT生成器,用于生成JWT和验证JWT。
 *
 * @date
 * @since
 */
@Component
public class OrkasgbJWTSecurityContextRepository implements SecurityContextRepository {

    private static final SecretKey ORKASGB_AUTHENTICATINO_JWT_KEY =
            new SecretKeySpec("ORKASGB_AUTHENTICATINO_JWT".getBytes(StandardCharsets.UTF_8),  "AES");;
    
    private static final String ORKASGB_AUTHENTICATINO_TOKEN_KEY = "ORKASGB:AUTHENTICATION:";

    private static final JwtParser JWT_PARSER = Jwts.parser().setSigningKey(ORKASGB_AUTHENTICATINO_JWT_KEY);

    private static final long ORKASGB_AUTHENTICATION_EXPIRE_IN_MS = Duration.ofMinutes(2).toMillis();

    private static final Map<String, Object> JWT_GRANTED_AUTHORITY_MAP = new HashMap<>(256);

    @Override
    public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
        HttpServletRequest request = requestResponseHolder.getRequest();
        String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);

        SecurityContext context = SecurityContextHolder.createEmptyContext();
        if(StringUtils.isBlank(authorization)){
            return context;
        }

        Jws<Claims> claimsJws;
        try {
            // 解析JWT
            claimsJws = JWT_PARSER.parseClaimsJws(authorization);
        }catch (Exception e) {
            return context;
        }
        Claims claims = claimsJws.getBody();
        String subject = claims.getSubject();
        UserDetails userDetails = (UserDetails) JWT_GRANTED_AUTHORITY_MAP.get("authorities" + subject);
        Collection<GrantedAuthority> grantedAuthorityList = new ArrayList<>(userDetails.getAuthorities());
        OrkasgbJWTSecurityContextToken orkasgbJWTSecurityContextToken =
                new OrkasgbJWTSecurityContextToken(grantedAuthorityList, grantedAuthorityList, subject);
        context.setAuthentication(orkasgbJWTSecurityContextToken);

        return context;
    }

    @Override
    public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
        UserDetails userDetails = (UserDetails) context.getAuthentication().getPrincipal();
        JWT_GRANTED_AUTHORITY_MAP.put("authorities" + userDetails.getUsername(), userDetails);
        String token = Jwts.builder()
                .setId(userDetails.getUsername().concat("_").concat(UUID.randomUUID().toString()))
                .setSubject(userDetails.getUsername()) // 发行者
                .setIssuer(userDetails.getUsername()) // 发行者
                .setIssuedAt(Calendar.getInstance().getTime()) // 发行时间
                .signWith(SignatureAlgorithm.HS256, ORKASGB_AUTHENTICATINO_JWT_KEY) // 签名类型 与 密钥
                .compressWith(CompressionCodecs.DEFLATE) // 对载荷进行压缩
                .setExpiration(new Date(System.currentTimeMillis() + ORKASGB_AUTHENTICATION_EXPIRE_IN_MS))
                .compact();

        response.setHeader(ORKASGB_AUTHENTICATINO_TOKEN_KEY, token);
        request.setAttribute("accessToken", token);
    }

    @Override
    public boolean containsContext(HttpServletRequest request) {
        String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
        if(StringUtils.isBlank(authorization)) {
            return false;
        }

        try {
            JWT_PARSER.parseClaimsJws(authorization);
        }catch (Exception e) {
            return false;
        }

        return true;
    }
}

⑦ 构建一个AuthenticationFilterConfigurer配置类,实现AuthenticationFilterConfigurer接口,自定义配置新的认证方式。

/**
 * 自定义配置,主要是配置新的认证方法
 *
 * @date
 * @since 
 */
public class OrkasgbSMSSecurityConfig<H extends HttpSecurityBuilder<H>> extends
        AbstractAuthenticationFilterConfigurer<H, OrkasgbSMSSecurityConfig<H>, OrkasgbSMSAuthencationFilter> {

    private final OrkasgbSMSAuthenticationProvider orkasgbSMSAuthenicationProvider;

    private final OrkasgbSuccessHandle orkasgbSuccessHandle;

    private final OrkasgbFailureHandler orkasgbFailureHandler;

    private final OrkasgbJWTSecurityContextRepository orkasgbJWTSecurityContextRepository;

    public OrkasgbSMSSecurityConfig() {
        super(new OrkasgbSMSAuthencationFilter(), "/codeLogin");
        this.orkasgbSMSAuthenicationProvider = new OrkasgbSMSAuthenticationProvider();
        this.orkasgbFailureHandler = new OrkasgbFailureHandler();
        this.orkasgbSuccessHandle = new OrkasgbSuccessHandle();
        this.orkasgbJWTSecurityContextRepository = new OrkasgbJWTSecurityContextRepository();
    }

    @Override
    protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl) {
        return new AntPathRequestMatcher("/codeLogin", "POST");
    }

    @Override
    public void configure(H http) throws Exception {
        OrkasgbSMSAuthencationFilter orkasgbSMSAuthenticationFilter = new OrkasgbSMSAuthencationFilter();
        // 设置 AuthenticationManager
        orkasgbSMSAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        // 分别设置成功和失败处理器
        orkasgbSMSAuthenticationFilter.setAuthenticationSuccessHandler(orkasgbSuccessHandle);
        orkasgbSMSAuthenticationFilter.setAuthenticationFailureHandler(orkasgbFailureHandler);
        orkasgbSMSAuthenticationFilter.setSecurityContextRepository(orkasgbJWTSecurityContextRepository);

        // 创建 SmsCodeAuthenticationProvider 并设置 userDetailsService
        // 将Provider添加到其中
        http
        .authenticationProvider(this.orkasgbSMSAuthenicationProvider)
        // 将过滤器添加到UsernamePasswordAuthenticationFilter后面
        .addFilterBefore(orkasgbSMSAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        super.configure(http);
    }
}

⑧ 构建配置类,注入SecurityFilterChain类。

/**
 * spring security自动化配置。
 */
@Configuration
@EnableAutoConfiguration
@EnableMethodSecurity
@EnableWebSecurity
public class OrkasgbSecurityConfig {

    @Autowired
    private OrkasgbUserDetailsService orkasgbUserDetailsService;

    @Autowired
    private OrkasgbSuccessHandle orkasgbSuccessHandle;

    @Autowired
    private OrkasgbFailureHandler orkasgbFailureHandler;

    @Autowired
    private OrkasgbJWTSecurityContextRepository orkasgbJWTSecurityContextRepository;

    @Autowired
    private OrkasgbSMSUserDetailsService orkasgbSMSUserDetailsService;

    /**
     * 配置 SecurityFilterChain
     *
     * @param httpSecurity HttpSecurity实例
     * @return HttpSecurity实例
     * @throws Exception 异常
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                // 配置表单登录
                .formLogin()
                // 配置处理登录请求的URL
                .loginProcessingUrl( "/login")
                // 配置请求参数,获取用户名username
                .usernameParameter("username")
                // 配置请求参数,获取密码password
                .passwordParameter("password")
                // 配置处理成功的回调
                .successHandler(orkasgbSuccessHandle)
                // 配置处理失败的回调
                .failureHandler(orkasgbFailureHandler)
                .securityContextRepository(orkasgbJWTSecurityContextRepository)
                .and()
                .securityContext()
                .securityContextRepository(orkasgbJWTSecurityContextRepository)
                .and()
                .cors()
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // 配置自定义UserDetailsService
                .userDetailsService(orkasgbUserDetailsService)
                // 禁止csrf跨站请求伪造
                .csrf()
                .disable()
                // 所有的请求都需求要认证
                .authorizeHttpRequests()
                .requestMatchers("/login", "/loginOut")
                .permitAll()
                // 所有的请求都需求要认证
                .anyRequest()
                // 所有的请求都需求要认证
                .authenticated()
                .and()
                // 异常处理器
                .exceptionHandling();
                //.authenticationEntryPoint(authenticationEntryPoint);

		// 新的认证方式
        OrkasgbSMSSecurityConfig<HttpSecurity> orkasgbSMSSecurityConfig =
                new OrkasgbSMSSecurityConfig<>();
        orkasgbSMSSecurityConfig.securityContextRepository(orkasgbJWTSecurityContextRepository);
        orkasgbSMSSecurityConfig.loginProcessingUrl("/codeLogin").permitAll();
        // 调用此方法将新的认证方式配置进去。
        httpSecurity.apply(orkasgbSMSSecurityConfig);
        return httpSecurity.build();
    }

}

项目地址:
https://gitee.com/huannzi/orkasgbframework/tree/master/src/main/java/com/orkasgb/framework/orkasgbframework/spring/springsecurity

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/788706.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

打开Android device monitor

X:\assdk\tools monitor.bat 双击 更新到最新

如何优雅地下载huggingface上模型,以llama2模型下载为例

背景 由于llama2模型的下载需要经过官方的授权&#xff0c;这就需要登陆hugging face的&#xff0c;对模型页面进行申请。等待审核通过后&#xff0c;才能够下载。如果在单纯用 git lfs 的方式进行下载&#xff0c;需要输入账号和密码。为了更快速地进行下载&#xff0c;既能够…

Java刷题记录(小白边刷边学)7.25

Java刷题记录&#xff08;小白边刷边学&#xff09;7.25 1 最长公共前缀 题目分析: 首先一定需要创建一个数组存储最长公共前缀的值 为了方便比较&#xff0c;先把strs的第一个字符串放进新的数组中即strs[0] 因此比较时从strs【1】开始 数字j需要小于两个被比较的字符串的长度…

视频超分新方法--助力实现高清wav2lip数字人

文章目录 前言一、解决方案详解总结前言 ` 随着人工智能的不断发展,数字人技术也越来越重要,很多人都开启了学习模型 但是使用神级模型wav2lip生成的数字人嘴部不清晰怎么办。 很影响使用效果,接下来教大家如何优化这个问题,如下图所示: 一、解决方案详解 因为wav2lip是…

Centos8+Jenkins+微信小程序前端自动发布体验版

文章目录 **一、 实现&#xff1a;****二、项目&#xff1a;****三、环境配置&#xff1a;****四、步骤&#xff1a;****五、遇到的问题** 一、 实现&#xff1a; jenkins打通微信开发平台&#xff0c;自动上传代码 二、项目&#xff1a; 微信小程序原生开发 三、环境配置&…

PowerPoint如何修改“默认保存路径”?

很多时候&#xff0c;我们做好PPT后都要保存&#xff0c;一般会保存在创建PPT的文件夹里&#xff0c;或者另外设置保存的路径。 如果经常需要制作PPT&#xff0c;又不想每次都要重新选择保存位置&#xff0c;我们可以创建或修改“默认保存路径”&#xff0c;这样每次关闭PPT后…

集群及LVS简介、LVSNAT模式原理、LVSNAT模式配置、LVSDR模式原理、LVSDR模式配置、LVS错误排查

day01 day01集群LVS配置LVS NAT模式配置LVS NAT模式步骤LVS DR模式配置LVS DR模式 集群 将很多机器组织到一起&#xff0c;作为一个整体对外提供服务 集群在扩展性、性能方面都可以做到很灵活 集群分类&#xff1a; 负载均衡集群&#xff1a;Load Balance高可用集群&#x…

wangeditor编辑器配置

vue项目中使用编辑器&#xff0c;轻量&#xff0c;操作栏选取自己需要的 官网地址&#xff1a;用于 Vue React | wangEditor 使用在vue项目中引入 npm install wangeditor/editor --savenpm install wangeditor/editor-for-vue --save 封装成组件使用 <template>&…

[LitCTF 2023]作业管理系统

打开环境后是一个登录框&#xff0c;还以为是sql注入&#xff0c;但是尝试之后没有回显&#xff0c;尝试一下弱密码爆破咯 爆出都是admin 进入后可以看到很多选项&#xff0c;都是可以访问的 &#xff0c;说明这道题还有很多解决方法 我们可选择上传文件&#xff0c;没有任何过…

STM32CubeIDE(外部中断)

目录 一、配置时钟树 二、配置IO 三、配置中断优先级 四、当发生外部中断时会进入中断服务函数 一、配置时钟树 二、配置IO 三、配置中断优先级 四、当发生外部中断时会进入中断服务函数

Stable Diffusion本地化部署,实现免费的AI绘制图片

利用Claude进行问答&#xff0c;首先我们询问如何进行AI画图&#xff1a; 我们得到了答案&#xff0c;这里我们问问第三个的网址&#xff1a; 由于在线的都需要收费和限制次数&#xff0c;所以问问有没有本地化的项目&#xff1a; 这里我们得到了答案&#xff0c;前面两个链…

flutter开发实战-jsontodart及 生成Dart Model类

flutter开发实战-jsontodart及 生成Dart Model类。 在开发中&#xff0c;经常遇到请求的数据Json需要转换成model类。这里记录一下Jsontodart生成Dart Model类的方案。 一、JSON生成Dart Model类 在开发中经常用到将json转成map或者list。通过json.decode() 可以方便 JSON 字…

mysql主从复制 读写分离

目录 1.主从复制类型 2.主从复制工作流程(原理) 3.mysql三种同步方式 4.案例 1.主从复制类型 基于语句的复制 基于行的复制 混合类型的复制 2.主从复制工作流程(原理) 两日志 三线程 收到数据主放入二进制日志中 从服务器通过io线程发送请求进入主的dump线程…

element实现角色管理页面

用的element&#xff0c;Tree 树形控件来实现的 <template><div class"p-t-20"><div class"table-bg"><Title title"角色名称"><span>{{role.roleName}}</span></Title><div class"menu-lis…

数学建模-主成分分析

有很多变量并且变量之间有很强的关联关系时使用&#xff0c;解决多重共线性问题没必要进行降维信息损失后然后进行评分&#xff0c;TOPSIS可以直接利用所有数据。指标有各种类型&#xff0c;没有进行正向化

【GoLang】MAC安装Go语言环境

小试牛刀 首先安装VScode软件 或者pycharmmac安装brew软件 brew install go 报了一个错误 不提供这个支持 重新brew install go 之后又重新brew reinstall go 使用go version 可以看到go 的版本 使用go env 可以看到go安装后的配置 配置一个环境变量 vim ~/.zshrc, # bre…

简单的SQL注入

SQL注入攻击是通过操作输入来修改SQL语句&#xff0c;用以达到执行代码对WEB服务器进行攻击的方法。简单的说就是在post/getweb表单、输入域名或页面请求的查询字符串中插入SQL命令&#xff0c;最终使web服务器执行恶意命令的过程。可以通过一个例子简单说明SQL注入攻击。假设某…

mysql学习系列(2)--忘记mysql登录密码怎么办?

系列文章目录 文章目录 系列文章目录前言一、登录mysql二、操作步骤1.找到mysql.exe所在的文件夹2.WinR打开cmd&#xff0c;进入bin文件夹3.跳过mysql用户验证3.net start mysql启动服务 总结 前言 一、登录mysql mysql -uroot -p忘记密码无法进入&#xff1a; 二、操作步骤…

【面试系列】JDK动态代理和CGLIB静态代理

文章目录 前言JDK动态代理代码实例Cglib 代理代码实例两者优缺点 前言 是否在面试过程中经常被问到Spring的代理的问题&#xff1a;比如说几种代理方式&#xff1f;两种代理方式的区别&#xff1f;或者问为什么JDK动态代理只能代理接口&#xff1f; 如果你能回答出来JDK动态代…