09、Spring Security 实战 - SpringSecurity 自定义认证数据源实现用户认证

1. 表单登录认证流程分析

官方文档:https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html

AbstractAuthenticationProcessingFilter用作Filter验证用户凭据的基础,可以对提交给它的任何身份验证请求进行身份验证。
 

①发起认证请求,请求中携带用户名、密码,该请求会被UsernamePasswordAuthenticationFilter 拦截, 在 UsernamePasswordAuthenticationFilter 的 attemptAuthentication 方法中将请求中用户名和密码,封装为 Authentication 对象

②将Authentication 对象 交给AuthenticationManager 进行认证;

③认证失败,清除 SecurityContextHodler 以及 记住我中信息,回调AuthenticationFailureHandler 处理;

④认证成功,将认证信息存储到 SecurityContextHodler 及记住我等,回调AuthenticationSuccessHandler 处理;

2. 表单登录认证源码

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
   
     
    @Override
    protected void configure(HttpSecurity http) throws Exception {
   
     
        // 开启请求的权限管理
        http.authorizeRequests()
                // 放行访问登录页面的/login.html请求
                .mvcMatchers("/login.html").permitAll()
                // 放行/index请求
                .mvcMatchers("/index").permitAll()
                // 其他所有的请求都需要去认证
                .anyRequest().authenticated()
                .and()
                // 认证方式为表单认证
                .formLogin()
                    // 指定默认的登录页面
                    .loginPage("/login.html")
                    // 指定登录请求路径
                    .loginProcessingUrl("/doLogin")
                    // 指定表单用户名的 name 属性为 uname
                    .usernameParameter("uname")
                    // 指定表单密码的 name 属性为 passwd
                    .passwordParameter("passwd")
                    // 指定登录成功后的自定义处理逻辑
                    .defaultSuccessUrl("/index")
                .and()
                // 禁止csrf跨站请求保护
                .csrf().disable();
    }
}

①进入WebSecurityConfigurer的formLogin方法:

public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<DefaultSecurityFilterChain, HttpSecurity>
    implements SecurityBuilder<DefaultSecurityFilterChain>, HttpSecurityBuilder<HttpSecurity> {
   
     
    
   /**
     * 指定支持基于表单的身份验证。 
     * 如果未指定 FormLoginConfigurer.loginPage(String),则将生成默认登录页面。
     */
    public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception {
   
     
        return getOrApply(new FormLoginConfigurer<>());
    }
}

public final class FormLoginConfigurer<H extends HttpSecurityBuilder<H>> extends
      AbstractAuthenticationFilterConfigurer<H, FormLoginConfigurer<H>, UsernamePasswordAuthenticationFilter> {
   
     

   /**
    * Creates a new instance
    * @see HttpSecurity#formLogin()
    */
   public FormLoginConfigurer() {
   
     
      super(new UsernamePasswordAuthenticationFilter(), null);
      usernameParameter("username");
      passwordParameter("password");
   }
}

②进入UsernamePasswordAuthenticationFilter类来处理身份验证表单提交:

UsernamePasswordAuthenticationFilter 用来处理身份验证表单提交。在 Spring Security 3.0 之前称为 AuthenticationProcessingFilter。登录表单必须向此过滤器提供两个参数:用户名和密码。要使用的默认参数名称包含在静态字段 SPRING_SECURITY_FORM_USERNAME_KEYSPRING_SECURITY_FORM_PASSWORD_KEY 中。 参数名称也可以通过设置 usernameParameter 和 passwordParameter 属性来更改。 默认情况下,此过滤器响应 URL /login。

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
   
     

    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";

    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER 
        = new AntPathRequestMatcher("/login","POST");

    private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;

    private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;

    private boolean postOnly = true;

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response)
        throws AuthenticationException {
   
     
        //  从request中获取登录方法,判断是否为post方法
        if (this.postOnly && !request.getMethod().equals("POST")) {
   
     
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        // 从request中根据参数名称username获取用户名
        String username = obtainUsername(request);
        username = (username != null) ? username : "";
        username = username.trim();
        // 从request中根据参数名称password获取密码
        String password = obtainPassword(request);
        password = (password != null) ? password : "";
        // 将username和password封装成UsernamePasswordAuthenticationToken
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        setDetails(request, authRequest);
        // 调用AuthenticationManager接口的authenticate方法对UsernamePasswordAuthenticationToken进行认证
        // 实际上调用的ProviderManager对象的authenticate方法
        return this.getAuthenticationManager().authenticate(authRequest);
    }
    
    @Nullable
    protected String obtainPassword(HttpServletRequest request) {
   
     
        return request.getParameter(this.passwordParameter);
    }

    @Nullable
    protected String obtainUsername(HttpServletRequest request) {
   
     
        return request.getParameter(this.usernameParameter);
    }
}

③进入authenticate方法内部,发现this.getAuthenticationManager().authenticate(authRequest)实际调用的是 ProviderManager 类中的 authenticate 方法:

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
   
     
    
    // AuthenticationProviders 列表
    private List<AuthenticationProvider> providers = Collections.emptyList();
    
    // AuthenticationManager 接口对象
    private AuthenticationManager parent;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
   
     
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        AuthenticationException parentException = null;
        Authentication result = null;
        int currentPosition = 0;
        int size = this.providers.size();
        // 遍历AuthenticationProviders列表
        for (AuthenticationProvider provider : getProviders()) {
   
     
            // 判断AuthenticationProvider能否验证当前传递的 Authentication 对象的类型
            if (!provider.supports(toTest)) {
   
     
                continue;
            }
            try {
   
     
                // 如果AuthenticationProvider指示它能够验证传递的 Authentication 对象的类型。
                // 将尝试使用该 AuthenticationProvider 进行身份验证
                result = provider.authenticate(authentication);
            }catch (AuthenticationException ex) {
   
     
                // 如果任何支持 AuthenticationProvider 的身份验证未成功,
                // 则最后抛出的 AuthenticationException 将被重新抛出。
                lastException = ex;
            }
        }
        if (result == null && this.parent != null) {
   
     
            // Allow the parent to try.
            try {
   
     
                // 也可以设置父 AuthenticationManager,
                // 如果配置的AuthenticationProvider都不能执行身份验证,也会尝试这样做。
                parentResult = this.parent.authenticate(authentication);
                result = parentResult;
            }catch (AuthenticationException ex) {
   
     
                parentException = ex;
                lastException = ex;
            }
        }
    }
}

源码中对该类的说明:

通过 AuthenticationProviders 列表迭代 Authentication 请求。
AuthenticationProvider 通常会按顺序尝试,直到提供非空响应。非空响应表示提供者有权决定身份验证请求,并且不再尝试其他提供者。如果后续提供者成功验证了请求,则忽略之前的验证异常并使用成功的验证。如果没有后续提供者提供非空响应或新的 AuthenticationException,则将使用最后收到的 AuthenticationException。如果没有提供者返回非空响应,或者表明它甚至可以处理身份验证,则 ProviderManager 将抛出 ProviderNotFoundException。也可以设置父 AuthenticationManager,如果配置的AuthenticationProvider都不能执行身份验证,也会尝试这样做。

源码中对 authenticate(Authentication authentication)方法的说明:

尝试对传递的 Authentication 对象进行身份验证。将连续尝试 AuthenticationProvider 列表,直到 AuthenticationProvider 指示它能够验证传递的 Authentication 对象的类型。 然后将尝试使用该 AuthenticationProvider 进行身份验证。如果多个 AuthenticationProvider 支持传递的 Authentication 对象,则第一个能够成功验证 Authentication 对象的人会确定结果,并覆盖早期支持的 AuthenticationProvider 引发的任何可能的 AuthenticationException。 成功认证后,不会尝试后续的 AuthenticationProviders。 如果任何支持 AuthenticationProvider 的身份验证未成功,则最后抛出的 AuthenticationException 将被重新抛出。

断点调用的过程:

遍历AuthenticationProviders列表获得AuthenticationProvider接口的实现类,AuthenticationProvider接口的实现类只有一个AnonymousAuthenticationProvider,它并不能验证Authentication 对象的类型即UsernamePasswordAuthenticationToken 类型,因此跳出循环,循环结束。
 

跳出for循环后,继续向下走,因为result == null && this.parent != null条件满足,将继续尝试使用AuthenticationManager中的authenticate方法认证,AuthenticationManager接口的默认实现类是ProviderManager,因此最终调用的仍然是ProviderManager中的authenticate方法,从断点可以看出,父类的ProviderManager中有一个AuthenticationProvider接口的实现类DaoAuthenticationProvider:
 

断点继续进入authenticate方法内,继续回调ProviderManager中的authenticate方法,遍历AuthenticationProviders列表获得AuthenticationProvider接口的实现类,AuthenticationProvider接口的实现类只有一个DaoAuthenticationProvider,它能够验证Authentication 对象的类型即UsernamePasswordAuthenticationToken 类型,因此将使用DaoAuthenticationProvider的authenticate方法进行身份认证:
 

④上一步断点可以看出最终调用了DaoAuthenticationProvider的authenticate方法,而DaoAuthenticationProvider继承自AbstractUserDetailsAuthenticationProvider,因此实际调用的是AbstractUserDetailsAuthenticationProvider的authenticate方法。进入AbstractUserDetailsAuthenticationProvider的authenticate方法:

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
   
     
    // ...
}

public abstract class AbstractUserDetailsAuthenticationProvider
    implements AuthenticationProvider, InitializingBean, MessageSourceAware {
   
     

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
   
     
        // 从authentication对象中获取登录用户的用户名
        String username = determineUsername(authentication);
        boolean cacheWasUsed = true;
        // 先从缓存中根据username获取用户详情信息UserDetails
        UserDetails user = this.userCache.getUserFromCache(username);
        // 如果缓存中获取不到
        if (user == null) {
   
     
            cacheWasUsed = false;
            try {
   
     
                // 到数据源中获取用户详情数据
                user = retrieveUser(username, 
                                   (UsernamePasswordAuthenticationToken) authentication);
            }catch (UsernameNotFoundException ex) {
   
     
            }
            // 将获取到的用户数据存入缓存
            if (!cacheWasUsed) {
   
     
                this.userCache.putUserInCache(user);
            }
            return createSuccessAuthentication(principalToReturn, authentication, user);
        }
    }
    
    // 返回认证成功的Authentication对象
    // 子类通常会将用户提供的原始凭据(不是加盐或编码密码)存储在返回的 Authentication 对象中。
    protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
   
     
        UsernamePasswordAuthenticationToken result
            = new UsernamePasswordAuthenticationToken(principal,
                                                      authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
        result.setDetails(authentication.getDetails());
        return result;
    }
}

⑤断点继续向下走,进入retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication) 方法内部:

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
   
     
    @Override
    protected final UserDetails retrieveUser(String username, 		UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
   
     
        try {
   
     
            // 根绝UserDetailsService接口提供的loadUserByUsername方法获取用户信息
            UserDetails loadedUser 
                = this.getUserDetailsService().loadUserByUsername(username);
            // ...
            return loadedUser;
        }
        // ...
    }
    
    // 获取UserDetailsService 
    protected UserDetailsService getUserDetailsService() {
   
     
        return this.userDetailsService;
    }
}

UserDetailsService 在整个框架中用作用户 DAO,并且是 DaoAuthenticationProvider 使用的策略:

public interface UserDetailsService {
   
     
   // 根据用户名获取用户信息
   UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

因此我们只需要自定义一个类实现UserDetailsService接口,并重写loadUserByUsername方法,就可以实现自定义数据源认证。

3. 三个认证类之间的关系

从上面分析中得知,AuthenticationManager 是认证的核心类,但实际上在底层真正认证时还离不开 ProviderManager 以及 AuthenticationProvider 。他们三者关系是样的呢?

  • AuthenticationManager 是一个认证管理器,它定义了 Spring Security 过滤器要执行认证操作。
  • ProviderManager AuthenticationManager接口的实现类。Spring Security认证时默认使用就是 ProviderManager。
  • AuthenticationProvider 就是针对不同的身份类型执行的具体的身份认证。

AuthenticationManager 与 ProviderManager :
 

ProviderManager 是 AuthenticationManager 的唯一实现,也是 SpringSecurity 默认使用实现。从这里不难看出默认情况下AuthenticationManager 就是一个ProviderManager。

ProviderManager 与 AuthenticationProvider

 

AuthenticationManager 与 ProviderManager

ProviderManager 是 AuthenticationManager 的唯一实现,也是 SpringSecurity 默认使用实现。从这里不难看出默认情况下AuthenticationManager 就是一个ProviderManager。ProviderManager 与 AuthenticationProvider

在Spring Seourity 中,允许系统同时支持多种不同的认证方式,例如同时支持用户名/密码认证、ReremberMe 认证、手机号码动态认证等,而不同的认证方式对应了不同的 AuthenticationProvider,所以一个完整的认证流程可能由多个AuthenticationProvider 来提供。

多个AuthenticationProvider 将组成一个列表,这个列表将由ProviderManager 代理。换句话说,在ProviderManager 中存在一个AuthenticationProvider 列表,在Provider Manager 中遍历列表中的每一个AuthenticationProvider 去执行身份认证,最终得到认证结果。

ProviderManager 本身也可以再配置一个 AuthenticationManager 作为parent,这样当ProviderManager 认证失败之后,就可以进入到 parent 中再次进行认证。理论上来说,ProviderManager 的 parent 可以是任意类型的
AuthenticationManager,但是通常都是由ProviderManager 来扮演 parent 的⻆色,也就是 ProviderManager 是ProviderManager 的 parent。

ProviderManager 本身也可以有多个,多个ProviderManager 共用同一个parent。有时,一个应用程序有受保护资源的逻辑组(例如,所有符合路径模式的网络资源,如/api/**),每个组可以有自己的专用 AuthenticationManager。通常,每个组都是一个ProviderManager,它们共享一个父级。然后,父级是一种 全局 资源,作为所有提供者的后备资源。

 

默认情况下AuthenticationProvider 是由 DaoAuthenticationProvider 类来实现认证的,在DaoAuthenticationProvider 认证时又通过 UserDetailsService 完成数据源的校验。

AuthenticationManager 是认证管理器,在 Spring Security 中有全局AuthenticationManager,也可以有局部AuthenticationManager。全局的AuthenticationManager用来对全局认证进行处理,局部的AuthenticationManager用来对某些特殊资源认证处理。当然无论是全局认证管理器还是局部认证管理器都是由
ProviderManger 进行实现。 每一个ProviderManger中都代理一个AuthenticationProvider的列表,列表中每一个实现代表一种身份认证方式。认证时底层数据源需要调用 UserDetailService 来实现。

4. 配置全局 AuthenticationManager

1、默认的全局 AuthenticationManager

springboot 对 security 进行自动配置时自动在工厂中创建一个全局AuthenticationManager

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
   
     
    @Autowired
    public void initialize(AuthenticationManagerBuilder builder) {
   
     
        System.out.println(builder);
    }
}

默认自动配置创建全局AuthenticationManager,默认找当前项目中是否存在自定义 UserDetailService 实例 自动将当前项目 UserDetailService 实例设置为数据源

默认自动配置创建全局AuthenticationManager,在工厂中使用时直接在代码中注入即可

2、自定义全局 AuthenticationManager

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
   
     
    @Bean
    public UserDetailsService userDetailsService(){
   
     
        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
        userDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("admin").build());
        return userDetailsManager;
    }

    // 自定义AuthenticationManager:并没有在工厂中暴露出来
    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
   
     
        builder.userDetailsService(userDetailsService());
    }
}

一旦通过 configure 方法自定义 AuthenticationManager实现,就会将工厂中自动配置AuthenticationManager 进行覆盖;

一旦通过 configure 方法自定义 AuthenticationManager实现,就需要在实现中指定认证数据源对象 UserDetaiService 实例;

一旦通过 configure 方法自定义 AuthenticationManager实现,这种方式创建AuthenticationManager对象是在工厂内部本地一个 AuthenticationManager对象,不允许在其他自定义组件中进行注入;

3、用来在工厂中暴露自定义AuthenticationManager 实例

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
   
     
    
    @Bean
    public UserDetailsService userDetailsService(){
   
     
        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
        userDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("admin").build());
        return userDetailsManager;
    }

    // 自定义AuthenticationManager:并没有在工厂中暴露出来
    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
   
     
        builder.userDetailsService(userDetailsService());
    }

    // 作用: 用来将自定义AuthenticationManager在工厂中进行暴露,可以在任何位置注入
    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
   
     
        return super.authenticationManager();
    }
}

5. 自定义内存数据源

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
   
     
    @Bean
    public UserDetailsService userDetailsService(){
   
     
        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
        userDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("admin").build());
        return userDetailsManager;
    }

    // 自定义AuthenticationManager:并没有在工厂中暴露出来
    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
   
     
        builder.userDetailsService(userDetailsService());
    }
}

6. 自定义数据库数据源

①创建数据库表

CREATE TABLE user
(
id int(11) NOT NULL AUTO_INCREMENT,
username varchar(32) DEFAULT NULL,
password varchar(255) DEFAULT NULL,
enabled tinyint(1) DEFAULT NULL,
accountNonExpired tinyint(1) DEFAULT NULL,
accountNonLocked tinyint(1) DEFAULT NULL,
credentialsNonExpired tinyint(1) DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
 
CREATE TABLE role
(
id int(11) NOT NULL AUTO_INCREMENT,
name varchar(32) DEFAULT NULL,
name_zh varchar(32) DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
 
CREATE TABLE user_role
(
id int(11) NOT NULL AUTO_INCREMENT,
uid int(11) DEFAULT NULL,
rid int(11) DEFAULT NULL,
PRIMARY KEY (id),
KEY uid (uid),
KEY rid (rid)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

②插入数据

BEGIN;
INSERT INTO user
VALUES (1, 'root', '{noop}123', 1, 1, 1, 1);
INSERT INTO user
VALUES (2, 'admin', '{noop}123', 1, 1, 1, 1);
INSERT INTO user
VALUES (3, 'blr', '{noop}123', 1, 1, 1, 1);
COMMIT;
 
BEGIN;
INSERT INTO role
VALUES (1, 'ROLE_product', '商品管理员');
INSERT INTO role
VALUES (2, 'ROLE_admin', '系统管理员');
INSERT INTO role
VALUES (3, 'ROLE_user', '用户管理员');
COMMIT;
 
BEGIN;
INSERT INTO user_role
VALUES (1, 1, 1);
INSERT INTO user_role
VALUES (2, 1, 2);
INSERT INTO user_role
VALUES (3, 2, 2);
INSERT INTO user_role
VALUES (4, 3, 3);
COMMIT;

③导入数据库依赖

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.0</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.15</version>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.8</version>
</dependency>

④SpringBoot配置文件

spring.security.user.roles=admin,user
spring.security.user.password=123
spring.security.user.name=root

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test?characterEncoding=UTF-8&useSSL=false&serverTimezone=Hongkong
spring.datasource.username=root
spring.datasource.password=root

mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.hh.entity

logging.level.com.baizhi=debug

⑤创建 user 对象

@Data
public class User implements UserDetails{
   
     
    private Integer id;
    private String username;
    private String password;
    private Boolean enabled;
    private Boolean accountNonExpired;
    private Boolean accountNonLocked;
    private Boolean credentialsNonExpired;
    private List<Role> roles = new ArrayList<>();

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
   
     
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        roles.forEach(role -> grantedAuthorities.add(new SimpleGrantedAuthority(role.getName())));
        return grantedAuthorities;
    }

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

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

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

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

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

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

⑥创建 Role 对象

@Data
public class Role {
   
     
    private Integer id;
    private String name;
    private String nameZh;
}

⑦创建 UserDao

@Mapper
public interface UserDao {
   
     
    
   // "根据用户名查询用户
    User loadUserByUsername(String username);
    
    // "根据用户id查询⻆色
    List<Role> getRolesByUid(Integer uid);
}

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hh.dao.UserDao">

    <select id="loadUserByUsername" resultType="com.hh.entity.User">
        select * from user where username ={username}
    </select>
    <select id="getRolesByUid" resultType="com.hh.entity.Role">
        select
            r.id,
            r.name,
            r.name_zh nameZh
        from role r, user_role ur
        where r.id = ur.rid
        and ur.uid ={uid}
    </select>
</mapper>

⑧创建 MyUserDetailsService

@Service
public class MyUserDetailsService implements UserDetailsService {
   
     

    @Autowired
    private UserDao userDao;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
   
     
        User user = userDao.loadUserByUsername(username);
        if(Objects.isNull(user)){
   
     
            throw new RuntimeException("用户不存在");
        }
        user.setRoles(userDao.getRolesByUid(user.getId()));
        return user;
    }
}

⑨配置 authenticationManager 使用自定义UserDetailService

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
   
     

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
   
     
        builder.userDetailsService(userDetailsService);
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
   
     
        // 开启请求的权限管理
        http.authorizeRequests()
                // 放行访问登录页面的/login.html请求
                .mvcMatchers("/login.html").permitAll()
                // 放行/index请求
                .mvcMatchers("/index").permitAll()
                // 其他所有的请求都需要去认证
                .anyRequest().authenticated()
                .and()
                // 认证方式为表单认证
                .formLogin()
                    // 指定默认的登录页面
                    .loginPage("/login.html")
                    // 指定登录请求路径
                    .loginProcessingUrl("/doLogin")
                    // 指定表单用户名的 name 属性为 uname
                    .usernameParameter("uname")
                    // 指定表单密码的 name 属性为 passwd
                    .passwordParameter("passwd")
                    // 指定登录成功后的自定义处理逻辑
                    .defaultSuccessUrl("/index")
                .and()
                // 禁止csrf跨站请求保护
                .csrf().disable();
    }
}

启动测试即可。