• SpringSecurity系列 - 09 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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    ① 进入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<>());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    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");
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    ② 进入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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    ③ 进入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;
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    源码中对该类的说明:

    通过 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 {
        // ...
    }
    
    • 1
    • 2
    • 3
    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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    ⑤ 断点继续向下走,进入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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

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

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

    因此我们只需要自定义一个类实现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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    默认自动配置创建全局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());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    一旦通过 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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    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());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    ② 插入数据

    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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    ③ 导入数据库依赖

    <dependency>
        <groupId>org.mybatis.spring.bootgroupId>
        <artifactId>mybatis-spring-boot-starterartifactId>
        <version>2.2.0version>
    dependency>
    
    <dependency>
        <groupId>mysqlgroupId>
        <artifactId>mysql-connector-javaartifactId>
        <version>8.0.15version>
    dependency>
    
    <dependency>
        <groupId>com.alibabagroupId>
        <artifactId>druidartifactId>
        <version>1.2.8version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    ④ 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    ⑤ 创建 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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    ⑥ 创建 Role 对象

    @Data
    public class Role {
        private Integer id;
        private String name;
        private String nameZh;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    ⑦ 创建 UserDao

    @Mapper
    public interface UserDao {
        
       // "根据用户名查询用户
        User loadUserByUsername(String username);
        
        // "根据用户id查询⻆色
        List<Role> getRolesByUid(Integer uid);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    
    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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    ⑧ 创建 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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    ⑨ 配置 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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    启动测试即可。

  • 相关阅读:
    Clearview X for mac v3.5.0 电子书阅读器 兼容 M1/M2/M3
    ChatGPT背后:从0到1,OpenAI的创立之路
    计算机毕业设计Java江西婺源旅游文化推广系统(源码+系统+mysql数据库+lw文档)
    嵌入式Ubuntu根文件系统移植带桌面
    InfiniBand vs 光纤通道,存储协议的选择
    T5 model
    【LeetCode回溯算法#07】子集问题I+II,巩固解题模板并详解回溯算法中的去重问题
    ABB机器人Whlie循环指令
    C++多线程--std::thread
    内部UI自动化测试培训之python基础
  • 原文地址:https://blog.csdn.net/qq_42764468/article/details/126844979