SpringSecurity6--认证和授权的原理

一、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框架认证流程

![[Spring Security认证流程.png]]

四、编写新的认证方式

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

}

全部评论

相关推荐

10-18 13:01
已编辑
西安理工大学 C++
小米内推大使:建议技能还是放上面吧,hr和技术面试官第一眼想看的应该是技能点和他们岗位是否匹配
点赞 评论 收藏
分享
10-17 12:16
同济大学 Java
7182oat:快快放弃了然后发给我,然后让我也泡他七天最后再拒掉,狠狠羞辱他一把😋
点赞 评论 收藏
分享
点赞 收藏 评论
分享
牛客网
牛客企业服务