使用数据库中的用户信息进行登录认证
在上一篇中我们通过关闭 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的用户信息登录认证。