springboot整合springsecurity 实现前后端分离项目中的用户认证登录及权限管理(源码分析)(3)

使用数据库中的用户信息进行登录认证

在上一篇中我们通过关闭 csrf token 验证功能,使Post 请求不被拦截,接下来我们继续来看,框架内部是如何处理登录请求的。
使用PostMan 发送请求至 “/login”
在这里插入图片描述
由于我们发送请求时并未带上用户名密码参数,因此控制台报错: 用户名密码错误。
看控制台输出可以发现 请求在经过 UsernamePasswordAuthenticationFilter 过滤器时报错,因此这个过滤器应该就是用来验证用户名密码的。

UsernamePasswordAuthenticationFilter 继承了 AbstractAuthenticationProcessingFilter,并且没有重写 doFIter 方法,看一下 AbstractAuthenticationProcessingFilter 的 dofilter() 方法的源码:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {

		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);

			return;
		}

		if (logger.isDebugEnabled()) {
			logger.debug("Request is to process authentication");
		}

		Authentication authResult;

		try {
			authResult = attemptAuthentication(request, response);
			if (authResult == null) {
				// return immediately as subclass has indicated that it hasn't completed
				// authentication
				return;
			}
			sessionStrategy.onAuthentication(authResult, request, response);
		}
		catch (InternalAuthenticationServiceException failed) {
			logger.error(
					"An internal error occurred while trying to authenticate the user.",
					failed);
			unsuccessfulAuthentication(request, response, failed);

			return;
		}
		catch (AuthenticationException failed) {
			// Authentication failed
			unsuccessfulAuthentication(request, response, failed);

			return;
		}

		// Authentication success
		if (continueChainBeforeSuccessfulAuthentication) {
			chain.doFilter(request, response);
		}

		successfulAuthentication(request, response, chain, authResult);
	}

在这个方法中,首先调用 requiresAuthentication 方法 判断请求是否需要认证,若返回 false 则表明不需要认证,直接执行过滤器链中的下一个过滤器。

protected boolean requiresAuthentication(HttpServletRequest request,
			HttpServletResponse response) {
		return requiresAuthenticationRequestMatcher.matches(request);
	}

requiresAuthenticationRequestMatcher 是 AbstractAuthenticationProcessingFilter 的属性,它是 RequestMatcher 的实现类 ,调用其 matches 方法来判断请求是否匹配。

UsernamePasswordAuthenticationFilter 继承了AbstractAuthenticationProcessingFilter,requiresAuthenticationRequestMatcher 自然也是 UsernamePasswordAuthenticationFilter的属性,
UsernamePasswordAuthenticationFilter 的构造方法调用了 父类的构造方法,并传入了一个 AntPathRequestMatcher 类的实例。

public UsernamePasswordAuthenticationFilter() {
		super(new AntPathRequestMatcher("/login", "POST"));
	}
protected AbstractAuthenticationProcessingFilter(
			RequestMatcher requiresAuthenticationRequestMatcher) {
		Assert.notNull(requiresAuthenticationRequestMatcher,
				"requiresAuthenticationRequestMatcher cannot be null");
		this.requiresAuthenticationRequestMatcher = requiresAuthenticationRequestMatcher;
	}

在本专栏第一篇文章中说到过 AntPathRequestMatcher,但并没有分析过它的实现原理,这里我们顺便来看一下它的源码,首先是构造方法:

public AntPathRequestMatcher(String pattern, String httpMethod) {
		this(pattern, httpMethod, true);
	}
public AntPathRequestMatcher(String pattern, String httpMethod,
			boolean caseSensitive, UrlPathHelper urlPathHelper) {
		Assert.hasText(pattern, "Pattern cannot be null or empty");
		this.caseSensitive = caseSensitive;

		if (pattern.equals(MATCH_ALL) || pattern.equals("**")) {
			pattern = MATCH_ALL;
			this.matcher = null;
		}
		else {
			// If the pattern ends with {@code /**} and has no other wildcards or path
			// variables, then optimize to a sub-path match
			if (pattern.endsWith(MATCH_ALL)
					&& (pattern.indexOf('?') == -1 && pattern.indexOf('{') == -1
							&& pattern.indexOf('}') == -1)
					&& pattern.indexOf("*") == pattern.length() - 2) {
				this.matcher = new SubpathMatcher(
						pattern.substring(0, pattern.length() - 3), caseSensitive);
			}
			else {
				this.matcher = new SpringAntMatcher(pattern, caseSensitive);
			}
		}

		this.pattern = pattern;
		this.httpMethod = StringUtils.hasText(httpMethod) ? HttpMethod.valueOf(httpMethod)
				: null;
		this.urlPathHelper = urlPathHelper;
	}

最终调用了这个带有4个参数的构造方法,此时 pattern 为 ”/login“ httpMethod 为”Post“ caseSensitive 为true urlPathHelper 为null MATCH_ALL 是一个String 类型的常量值为 “/**”。

这个方法的逻辑并不复杂,

首先根据 pattern 的值设置其 matcher 属性的值 matcher 是 Matcher 类型的对象,Matcher 是 AntPathRequestMatcher 的内部接口 其中有一个抽象方法 matches; 如果 pattern 的值为 /** 或者
** 那么matcher 赋值为null 如果 pattern 的值以 ”/**“ 结尾并且不包含 路径变量(即通配符{})matcher 赋值为 SubpathMatcher 类的对象,否则 matcher 赋值为 SpringAntMatcher 类的对象。

然后就是给它的 pattern,httpMethod urlPathHelper三个属性根据传入的参数进行赋值。

再看一下 AntPathRequestMatcher 的 matches方法:

@Override
	public boolean matches(HttpServletRequest request) {
		if (this.httpMethod != null && StringUtils.hasText(request.getMethod())
				&& this.httpMethod != valueOf(request.getMethod())) {
			if (logger.isDebugEnabled()) {
				logger.debug("Request '" + request.getMethod() + " "
						+ getRequestPath(request) + "'" + " doesn't match '"
						+ this.httpMethod + " " + this.pattern + "'");
			}

			return false;
		}

		if (this.pattern.equals(MATCH_ALL)) {
			if (logger.isDebugEnabled()) {
				logger.debug("Request '" + getRequestPath(request)
						+ "' matched by universal pattern '/**'");
			}

			return true;
		}

		String url = getRequestPath(request);

		if (logger.isDebugEnabled()) {
			logger.debug("Checking match of request : '" + url + "'; against '"
					+ this.pattern + "'");
		}

		return this.matcher.matches(url);
	}

首先判断 当前请求的请求方法类型 和 AntPathRequestMatcher 对象的 httpMethod 属性值是否一致,不一致则直接返回 false 表示匹配失败。
其次,如果当前 pattern 属性值为 ”/**“,则无论 request的请求url 是什么,都返回true 表示匹配成功。
最后 获取 request 的请求路径 url 交给 matcher,再调用 matcher 的matchs 方法:
由于此时的 pattern 为 ”/login“,根据上文的分析,matcher 应为 SpringAntMatcher 类的对象

@Override
		public boolean matches(String path) {
			return this.antMatcher.match(this.pattern, path);
		}

在 SpringAntMatcher 的matches 方法中 调用了,antMatcher 属性的match 方法。antMatcher属性是在 SpringAntMatcher 的构造方法中通过调用 createMatcher 方法获取的

private SpringAntMatcher(String pattern, boolean caseSensitive) {
			this.pattern = pattern;
			this.antMatcher = createMatcher(caseSensitive);
		}
private static AntPathMatcher createMatcher(boolean caseSensitive) {
			AntPathMatcher matcher = new AntPathMatcher();
			matcher.setTrimTokens(false);
			matcher.setCaseSensitive(caseSensitive);
			return matcher;
		}

createMatcher 方法比较简单,就是 new了一个AntPathMatcher对象,并设置其 trimTokens 为false,设置 caseSensitive 为 true(默认情况下caseSensitive 为true)。
AntPathMatcher 类是spring 框架中 用来实现 ant 风格的路径解析的类。具体匹配规则主要有:
? 匹配1个字符,* 匹配0个或多个字符,** 匹配路径中的0个或多个目录。caseSensitive表示是否区分大小写;
trimTokens是否去除前后空格。spring 中的类的源码不是本文重点,因此就不再深入分析了。我们只需要知道上述的三种匹配规则即可。

综上,AntPathRequestMatcher 类 主要由String 类型的 pattern 和httpMethod 两个参数进行初始化,根据 pattern 格式的不同 会给 AntPathRequestMatcher 的 matcher 属性 初始化不同的 Matcher 实现类(SubpathMatcher,SpringAntMatcher),在调用 matches 方法时首先判断 请求方法是否一致,其次,若pattern 为“/**”,则直接返回匹配成功,否则调用 matcher 实现类的matches 方法判断请求路径是否匹配。

接着分析 AbstractAuthenticationProcessingFilter 的 dofilter() 方法,此时 AntPathRequestMatcher 对象的 pattern属性 为“/login” httpMethod 属性为 Post,因此只有发送 Post请求到 “/login”,才会被 UsernamePasswordAuthenticationFilter 拦截,这个filter 就是专门用于 校验用户登录信息的。
确定请求需要被校验之后,调用了 attemptAuthentication() 方法,这是个 抽象方法,实现在 UsernamePasswordAuthenticationFilter 中:

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

		String username = obtainUsername(request);
		String password = obtainPassword(request);

		if (username == null) {
			username = "";
		}

		if (password == null) {
			password = "";
		}

		username = username.trim();

		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);

		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);

		return this.getAuthenticationManager().authenticate(authRequest);
	}

在这个方法中,首先判断请求类型是否为Post 不为Post 则抛出异常。然后调用 obtainUsername,obtainPassword 方法从request 中取出 username 和password,这两个方法就是调用了request 的getParameter 方法获取请求参数:

@Nullable
	protected String obtainUsername(HttpServletRequest request) {
		return request.getParameter(usernameParameter);
	}
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
	public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

	private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
	private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;

可以发现,usernameParameter ,passwordParameter 的值就是 “username”,和“password”,因此我们发送登录请求时,用户名密码的参数名应为“username”,和“password”。
然后使用 username,password 初始化一个 UsernamePasswordAuthenticationToken 类的对象。
调用 setDetail() 方法,传入参数为 request 对象 及 UsernamePasswordAuthenticationToken 对象。
这个方法主要是初始化了 一个 WebAuthenticationDetails 类对象,remoteAddress属性和 sessionId属性从request 对象中获取,然后将其赋给 UsernamePasswordAuthenticationToken 的 detail 属性。

protected void setDetails(HttpServletRequest request,
			UsernamePasswordAuthenticationToken authRequest) {
		authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
	}
public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
		return new WebAuthenticationDetails(context);
	}
public WebAuthenticationDetails(HttpServletRequest request) {
		this.remoteAddress = request.getRemoteAddr();

		HttpSession session = request.getSession(false);
		this.sessionId = (session != null) ? session.getId() : null;
	}

接着 调用 getAuthenticationManager() 方法获取 AuthenticationManager 接口 对象,此时的实现类为 ProviderManager,最后传入UsernamePasswordAuthenticationToken 对象,调用 ProviderManager 的 authenticate() 方法,这个方法实现了主要的验证逻辑:

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;
		boolean debug = logger.isDebugEnabled();

		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}

			if (debug) {
				logger.debug("Authentication attempt using "
						+ provider.getClass().getName());
			}

			try {
				result = provider.authenticate(authentication);

				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException | InternalAuthenticationServiceException e) {
				prepareException(e, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw e;
			} catch (AuthenticationException e) {
				lastException = e;
			}
		}

		if (result == null && parent != null) {
			// Allow the parent to try.
			try {
				result = parentResult = parent.authenticate(authentication);
			}
			catch (ProviderNotFoundException e) {
				// 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 e) {
				lastException = parentException = e;
			}
		}

		if (result != null) {
			if (eraseCredentialsAfterAuthentication
					&& (result instanceof CredentialsContainer)) {
				// Authentication is complete. Remove credentials and other secret data
				// from authentication
				((CredentialsContainer) result).eraseCredentials();
			}

			// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
			// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
			if (parentResult == null) {
				eventPublisher.publishAuthenticationSuccess(result);
			}
			return result;
		}

		// Parent was null, or didn't authenticate (or throw an exception).

		if (lastException == null) {
			lastException = new ProviderNotFoundException(messages.getMessage(
					"ProviderManager.providerNotFound",
					new Object[] { toTest.getName() },
					"No AuthenticationProvider found for {0}"));
		}

		// If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
		// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
		if (parentException == null) {
			prepareException(lastException, authentication);
		}

		throw lastException;
	}

在这个方法中,首先调用 getProviders() 方法获取 ProviderManager 的 providers 属性,该属性是一个 AuthenticationProvider 对象的列表,遍历 providers 调用其中 AuthenticationProvider 对象的 support() 方法,若support() 方法返回false,则进行下一次遍历,否则调用当前遍历的 AuthenticationProvider 对象的 authenticate() 方法进行验证。,通过debug 可以发现当前的 providers 中只有一个 AnonymousAuthenticationProvider 对象,其support() 方法的源码如下:

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

方法很简单,直接判断当前传入的 UsernamePasswordAuthenticationToken 对象是否是 AnonymousAuthenticationToken 的子类,显然 这个此时这个方法会返回false。
由此我们可以发现,AuthenticationToken 和 AuthenticationProvider 其实是一一对应的,使用support() 方法来判断是否对应。

回到 ProviderManager 的 authenticate 方法中,由于support 方法返回false,会尝试调用 ProviderManager 的parent 属性的 authenticate 方法,此时 parent 任然是 ProviderManager ,因此再次进入这个 authenticate 方法中,此时 getProviders 方法获取的 provider 也变成了 DaoAuthenticationProvider,它的 support 方法如下:

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

很明显是支持 UsernamePasswordAuthenticationToken 的,因此会调用它的 authenticate 方法,DaoAuthenticationProvider 继承 AbstractUserDetailsAuthenticationProvider 且没有重写 authenticate,因此调用的是父类的 authenticate 方法:

public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				() -> messages.getMessage(
						"AbstractUserDetailsAuthenticationProvider.onlySupports",
						"Only UsernamePasswordAuthenticationToken is supported"));

		// Determine username
		String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
				: authentication.getName();

		boolean cacheWasUsed = true;
		UserDetails user = this.userCache.getUserFromCache(username);

		if (user == null) {
			cacheWasUsed = false;

			try {
				user = retrieveUser(username,
						(UsernamePasswordAuthenticationToken) authentication);
			}
			catch (UsernameNotFoundException notFound) {
				logger.debug("User '" + username + "' not found");

				if (hideUserNotFoundExceptions) {
					throw new BadCredentialsException(messages.getMessage(
							"AbstractUserDetailsAuthenticationProvider.badCredentials",
							"Bad credentials"));
				}
				else {
					throw notFound;
				}
			}

			Assert.notNull(user,
					"retrieveUser returned null - a violation of the interface contract");
		}

		try {
			preAuthenticationChecks.check(user);
			additionalAuthenticationChecks(user,
					(UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException exception) {
			if (cacheWasUsed) {
				// 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);
				preAuthenticationChecks.check(user);
				additionalAuthenticationChecks(user,
						(UsernamePasswordAuthenticationToken) authentication);
			}
			else {
				throw exception;
			}
		}

		postAuthenticationChecks.check(user);

		if (!cacheWasUsed) {
			this.userCache.putUserInCache(user);
		}

		Object principalToReturn = user;

		if (forcePrincipalAsString) {
			principalToReturn = user.getUsername();
		}

		return createSuccessAuthentication(principalToReturn, authentication, user);
	}

这个方法中,首先获取 UsernamePasswordAuthenticationToken 中的 用户名及密码,然后去 userCache 中按照用户名取 UserDetails 对象,若获取不到则调用 retrieveUser 方法获取:

protected final UserDetails retrieveUser(String username,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		prepareTimingAttackProtection();
		try {
			UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
			if (loadedUser == null) {
				throw new InternalAuthenticationServiceException(
						"UserDetailsService returned null, which is an interface contract violation");
			}
			return loadedUser;
		}
		catch (UsernameNotFoundException ex) {
			mitigateAgainstTimingAttack(authentication);
			throw ex;
		}
		catch (InternalAuthenticationServiceException ex) {
			throw ex;
		}
		catch (Exception ex) {
			throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
		}
	}

这个方法中,首先调用 prepareTimingAttackProtection 方法,从方法名称可以看出这个方法是为防御计时攻击做准备的,这块内容不是我们当前关注的重点,暂时不作深入分析。接着 获取 userDetailsService 属性,调用 loadUserByUsername 方法,获取 UserDetails 对象,此时 userDetailsService 属性值是 InMemoryUserDetailsManager 对象,它的 loadUserByUsername 方法如下:

public UserDetails loadUserByUsername(String username)
			throws UsernameNotFoundException {
		UserDetails user = users.get(username.toLowerCase());

		if (user == null) {
			throw new UsernameNotFoundException(username);
		}

		return new User(user.getUsername(), user.getPassword(), user.isEnabled(),
				user.isAccountNonExpired(), user.isCredentialsNonExpired(),
				user.isAccountNonLocked(), user.getAuthorities());
	}

这个方法是从 users 属性中根据用户名获取 UserDetails 对象,获取不到则抛出异常, 此时user 属性中存储的是框架自动生成的用户信息,用户名及密码会在项目启动时在控制台打印出来。

我们来总结一下获取用户信息的流程。登录请求首先被 UsernamePasswordAuthenticationFilter 拦截,从request中获取请求参数 “username” 和 “password” 封装为 UsernamePasswordAuthenticationToken 对象。判断请求路径是否为“/login” 调用 authenticationManager 对象(默认为 ProviderManager)的 authenticate 方法,遍历 authenticationManager 中的 AuthenticationProvider,找到 UsernamePasswordAuthenticationToken 对应的 AuthenticationProvider对象(DaoAuthenticationProvider),调用 authenticate 方法,在这个方法中,调用其userDetailsService 属性的 loadUserByUsername 方法或取用户信息。

因此,DaoAuthenticationProvider 对象中的 userDetailsService 属性是问题的关键。在 DaoAuthenticationProvider 的构造方法打断点调试,看它的初始化过程,发现是在 InitializeUserDetailsManagerConfigurer 的config 方法中 初始化了 DaoAuthenticationProvider:

class InitializeUserDetailsManagerConfigurer
			extends GlobalAuthenticationConfigurerAdapter {
		@Override
		public void configure(AuthenticationManagerBuilder auth) throws Exception {
			if (auth.isConfigured()) {
				return;
			}
			UserDetailsService userDetailsService = getBeanOrNull(
					UserDetailsService.class);
			if (userDetailsService == null) {
				return;
			}

			PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
			UserDetailsPasswordService passwordManager = getBeanOrNull(UserDetailsPasswordService.class);

			DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
			provider.setUserDetailsService(userDetailsService);
			if (passwordEncoder != null) {
				provider.setPasswordEncoder(passwordEncoder);
			}
			if (passwordManager != null) {
				provider.setUserDetailsPasswordService(passwordManager);
			}
			provider.afterPropertiesSet();

			auth.authenticationProvider(provider);
		}

		/**
		 * @return a bean of the requested class if there's just a single registered component, null otherwise.
		 */
		private <T> T getBeanOrNull(Class<T> type) {
			String[] userDetailsBeanNames = InitializeUserDetailsBeanManagerConfigurer.this.context
					.getBeanNamesForType(type);
			if (userDetailsBeanNames.length != 1) {
				return null;
			}

			return InitializeUserDetailsBeanManagerConfigurer.this.context
					.getBean(userDetailsBeanNames[0], type);
		}
	}

初始化完成后 随即又用 userDetailsService 变量 设置其 userDetailsService 属性。该变量通过调用 getBeanOrNull 方法获取。这个方法其实就是从 spring 容器中获取 UserDetailService 接口实现类的bean,而 InMemoryUserDetailsManager 类实现了 UserDetailService 接口,在其构造方法打断点调试可以发现 这个类的bean 是在 UserDetailsServiceAutoConfiguration 配置类中定义的:

@Lazy
    public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties, ObjectProvider<PasswordEncoder> passwordEncoder) {
        User user = properties.getUser();
        List<String> roles = user.getRoles();
        return new InMemoryUserDetailsManager(new UserDetails[]{org.springframework.security.core.userdetails.User.withUsername(user.getName()).password(this.getOrDeducePassword(user, (PasswordEncoder)passwordEncoder.getIfAvailable())).roles(StringUtils.toStringArray(roles)).build()});
    }

@Lazy注解表示 InMemoryUserDetailsManager 是在被调用时才进行初始化的。
看一下 UserDetailsServiceAutoConfiguration 的源码:

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnClass({AuthenticationManager.class})
@ConditionalOnBean({ObjectPostProcessor.class})
@ConditionalOnMissingBean(
    value = {AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class},
    type = {"org.springframework.security.oauth2.jwt.JwtDecoder", "org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector"}
)
public class UserDetailsServiceAutoConfiguration {

其中 @ConditionalOnMissingBean 注解表示这个配置类仅在 spring 容器中不存在AuthenticationManager,AuthenticationProvider,UserDetailsService,这三个接口的 bean 时才会生效,这三个接口都是整个用户认证逻辑中的关键组件,我们可以通过自定义这些接口的实现类来实现个性化的认证逻辑。
因此,我们自定义 UserDetailsService 的实现类,重写其中的 loadUserByUsername 方法,从数据库中获取用户信息进行认证。并把该实现类注册到spring容器中,这样 InMemoryUserDetailsManager 类的bean就不会被初始化。userDetailsService 变量调用 getBeanOrNull 方法获取到的就是我们自己定义的 UserDetailsService 实现类。这样就实现了自定义的用户认证逻辑。

为了从数据库中获取用户信息,一般会在项目中自定义一个 UserService 接口,和用户信息相关的增删改查操作方法都定义在这个接口中,在接口方法中利用jdcb 或者mybatis 等持久化框架访问数据库。因此可以让其继承SpringSecurity 中的 UserDetailsService 接口,并重写 loadUserByUsername 方法。

本项目使用mybatis 连接mysql数据库,相关的配置步骤就不再详述。配置完成后,UserService 接口代码如下:

public interface UserService extends UserDetailsService
{

}

其实现类:

@Service("userService")
@Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
public class UserServiceImpl implements UserService
{
    protected static final Logger LOGGER = LoggerFactory.getLogger(UserServiceImpl.class);


    @Autowired
    private UserMapper userMapper;


//    重写认证方法,实现自定义springsecurity用户认证(用户名密码登录)
    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException
    {
        LOGGER.debug("进入通过用户名获取用户信息权限信息方法");
        User user = userMapper.selectByUsername(userName);
        if (user == null)
        {
            throw new UsernameNotFoundException("用户名不存在!");
        }
        LOGGER.debug("结束通过用户名获取用户信息权限信息方法");
        return user;
    }


}

其中的 User 类 实现了 SpringSecurity 的UserDetails 接口 并重写了其中关于获取用户名,密码,用户状态的相关方法(用户是否启用,是否锁定,是否过期等)。
配置完成后,启动项目,用PostMan 发送Post请求至“/login“,这次调用的就是我们自定义的 loadUserByUsername 方法了:
在这里插入图片描述
至此我们就实现了基于jdbc的用户信息登录认证。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值