SpringSecurity的认证原理及如何自定义认证结合MyBatis替换原数据源

一、自定义认证

对于在SpringBootWebSecurityConfiguration中自定义的认证规则,也就是所有的请求都必须要通过认证才可以访问。有些时候并不满足业务需求。比如有两个资源,一个是/index 还有一个是/hello
打算把/index作为一个公共的资源,/hello作为一个认证的资源。
在这里插入图片描述
默认的认证规则就做不到这一点。所以,我们需要自定义,从上个小节中发现,如果需要默认的规则失效有两个条件。

@ConditionalOnMissingBean({   
    org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter.class,
       SecurityFilterChain.class })

如果系统中存在WebSecurityConfigurerAdapter或者是SecurityFilterChain的话,默认的就会失效。
这里,我们使用第一种方式,去继承WebSecurityConfigurerAdapter类。
在这里插入图片描述
需要和方法@Overridepublic void configure(WebSecurity web) throws Exception {}有区分,一般是对静态资源放行的配置
说明:

  • permitAll() 代表放⾏该资源,该资源为公共资源 ⽆需认证和授权可以直接访问
  • anyRequest().authenticated() 代表所有请求,必须认证之后才能访问
  • formLogin() 代表开启表单认证

!注意: 放⾏资源必须放在所有认证请求之前!

二、自定义登录界面

只要是在项目中配置了对某一个资源认证,在请求资源的时候就会出现下面的默认登录页面,如何对该登录页面进行替换呢?
在这里插入图片描述
这里,我只是举一个列子,通过Thymeleaf模板引擎来实现。

  • 引入依赖
<!--导入页面模板-->
<dependency>    
    <groupId>org.springframework.boot</groupId>    
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
  • 自定义登录界面
@Controller
public class LoginController {    
    @RequestMapping("/login.html")    
    public String login(){        
        return "login";
    }
}
  • templates 中定义登录界⾯
<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body style="margin: 0 auto;">
    <h1>用户登录</h1>
    <form method="post" th:action="@{/doLogin}">
        用户名: <input type="text" name="uname"><br>
        密码:<input type="password" name="passwd"><br>
        <input type="submit" value="登录">
    </form>
</body>
</html>

但是,对于postmethod和用户名和密码的name怎么配置,我们知道给予默认页面的是UsernamePasswordAuthenticationFilter,如果需要自己自定义页面,如何符合规定。
在这里插入图片描述
一共有三项要求:

  1. 登录表单 method 必须为 postaction 的请求路径为 /doLogin
  2. ⽤户名的 name 属性为 uname
  3. 密码的name属性为 passwd

但是,这样子还是不行的,还是需要对没有认证的页面来指定跳转的位置,具体还是在protected void configure(HttpSecurity http) throws Exception中指定。

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .mvcMatchers("/login.html").permitAll()
            .mvcMatchers("/index").permitAll() // 允许放行的一定要在其他的限制之前
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .loginPage("/login.html") // 用来指定登录页面,注意:一旦自定义登录页面之后,必须指定登录的url,否则会一直302重定向到login.html
            .loginProcessingUrl("/doLogin") // 如果发送的请求是dologinin,就应该别username,password所铺获,指定登录请求的url
            .usernameParameter("uname")
            .passwordParameter("passwd")
//                .successForwardUrl("/index") // 认证成功之后,forward跳转的路径, 始终保持最新
            .defaultSuccessUrl("/index") // 重定向跳转 redirect之后跳转, 根据上一层保存的请求跳转
            .and()
            .csrf()
            .disable(); // 禁止csrf跨站请求保护
}
  • successForwardUrl defaultSuccessUrl 这两个⽅法都可以实现成功之后跳转
    • successForwardUrl 默认使⽤ forward跳转 注意:不会跳转到之前请求路径
    • defaultSuccessUrl默认使⽤redirect跳转 注意:如果之前请求路径,会有优先跳转之前请求路径,可以传⼊第⼆个参数进⾏修改

需要注意的是:这里配置csrf主要是在原来的login页面,配置了csrf,而我们自定义的没有配置,所以需要禁止使用csrf跨站请求保护。
在这里插入图片描述

三、自定义成功的处理

1、前后端分离返回json

对于上面的内容有successForwardUrldefaultSuccessUrl表认证成功之后的跳转页面,对于主流的前后端分离模式,这样子难免会出问题。如果是希望返回json,这个时候就可以通过自定义一个AuthenticationSuccessHandler来实现

public interface AuthenticationSuccessHandler {
    /**
     * Called when a user has been successfully authenticated.
     * @param request the request which caused the successful authentication
     * @param response the response
     * @param authentication the <tt>Authentication</tt> object which was created during
     * the authentication process.
     */
    void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws IOException, ServletException;

}

根据接⼝的描述信息,也可以得知登录成功会⾃动回调这个⽅法,进⼀步查看它的默认实现,你会发现successForwardUrl、defaultSuccessUrl也是由它的⼦类实现的
在这里插入图片描述

  • 自定义AuthenticationSuccessHandler的实现
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        Map<String, Object> result = new HashMap<>();
        result.put("msg", "login success");
        result.put("status", 200);
        result.put("authentication", authentication);
        response.setContentType("application/json;charset=utf-8");
        String s = new ObjectMapper().writeValueAsString(result);
        response.getWriter().println(s);
    }
}
  • 配置AuthenticationSuccessHandler
    在这里插入图片描述

四、如何在前端显示异常

1、传统的方法

通过源码分析可以看到在这里插入图片描述
为了能更直观在登录⻚⾯看到异常错误信息,可以在登录⻚⾯中直接获取异常信息,对于错误信息的获取,会分情况处理的。Spring Security 在登录失败之后会将异常信息存储到 requestsession作⽤域中keySPRING_SECURITY_LAST_EXCEPTION 命名属性中
在这里插入图片描述
在前端显示异常信息
在这里插入图片描述
在配置类中进行配置
在这里插入图片描述
在前端错误信息的获取,通过上面源码的分析可知:
failureUrl、failureForwardUrl 关系类似于之前提到的successForwardUrl defaultSuccessUrl ⽅法
failureUrl 失败以后的重定向跳转
failureForwardUrl 失败以后的 forward 跳转

注意:因此获取request 中异常信息,这⾥只能使⽤failureForwardUrl。

2、前后端分离返回错误的json

和自定义成功返回json的操作一样,也是需要将页面跳转的方式换成是failureHandler的方式。
在这里插入图片描述
这里我们需要有一个AuthenticationFailureHandler的类,

public interface AuthenticationFailureHandler {

    /**
     * Called when an authentication attempt fails.
     * @param request the request during which the authentication attempt occurred.
     * @param response the response.
     * @param exception the exception which was thrown to reject the authentication
     * request.
     */
    void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException exception) throws IOException, ServletException;
}

根据接⼝的描述信息,也可以得知登录失败会⾃动回调这个⽅法,进⼀步查看它的默认实现,你会发现failureUrlfailureForwardUrl也是由它的⼦类实现的
在这里插入图片描述
⾃定义 AuthenticationFailureHandler 实现

public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        Map<String, Object> result = new HashMap<>();
        result.put("msg", "error");
        result.put("error_", exception);
        String s = new ObjectMapper().writeValueAsString(result);
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().println(s);
    }
}

最后在对FailureHandler进行配置,AuthenticationFailureHandler
在这里插入图片描述
最后访问失败就会进入到自己自定义的错误处理逻辑返回json的字符串。

五、注销登录配置

1、注销登录前后端不分离

Spring Security 中也提供了默认的注销登录配置,在开发时也可以按照⾃⼰需求对注销进⾏个性化定制
在这里插入图片描述

  • 通过 logout() ⽅法开启注销配置
    -logoutUrl指定退出登录请求地址,默认是GET请求,路径为 /logout
  • invalidateHttpSession 退出时是否是session失效,默认值为 true
  • clearAuthentication 退出时是否清除认证信息,默认值为 true
  • logoutSuccessUrl 退出登录时跳转地址

配置多个注销登录请求
如果项⽬中有需要,开发者还可以配置多个注销登录的请求,同时还可以指定请求的⽅法
在这里插入图片描述

2、前后端分离的方法

前后端分离的配置方法和前面两种登录成功和返回错误信息的json的方式是一样的,都是自定义handler类。如果是前后端分离开发,注销成功之后就不需要⻚⾯跳转了,只需要将注销成功的信息返回前端即可,此时我们可以通过⾃定义 LogoutSuccessHandler 实现来返回注销之后信息:
在这里插入图片描述
在这里插入图片描述

六、登录⽤户数据获取

1、SecurityContextHolder

在用户登录成功之后,如何获取用户登录成功的数据。在Spring Security中用户数据,主要是通过SecurityContextHolder这个类来获得的。
Spring Security会将登陆用户数据保存在session中,但是,为了使用方便Spring Security在此基础上还做了⼀些改进,其中最主要的⼀个变化就是线程绑定。
当⽤户登录成功后,Spring Security 会将登录成功的⽤户信息保存到SecurityContextHolder 中。

SecurityContextHolder 中的数据保存默认是通过ThreadLocal 来实现的,使⽤ThreadLocal 创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是⽤户数据和请求线程绑定在⼀起。当登录请求处理完毕后,Spring Security 会将SecurityContextHolder 中的数据拿出来保存到 Session 中,同时将SecurityContexHolder 中的数据清空。以后每当有请求到来时,Spring Security就会先从Session中取出⽤户登录数据,保存到SecurityContextHolder 中,⽅便在该请求的后续处理过程中使⽤,同时在请求结束时将 SecurityContextHolder 中的数据拿出来保存到 Session 中,然后将SecurityContextHolder 中的数据清空。
实际上 SecurityContextHolder 中存储是 SecurityContext,在SecurityContext 中存储是 Authentication
在这里插入图片描述
Spring Security中所有的用户信息都封装在Authentication
Spring Security中这种设计是典型的策略模式

public class SecurityContextHolder {

    public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";

    public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";

    public static final String MODE_GLOBAL = "MODE_GLOBAL";
    private static final String MODE_PRE_INITIALIZED = "MODE_PRE_INITIALIZED";

    public static final String SYSTEM_PROPERTY = "spring.security.strategy";

    private static String strategyName = System.getProperty(SYSTEM_PROPERTY);

    private static SecurityContextHolderStrategy strategy;

    private static void initializeStrategy() {
        if (MODE_PRE_INITIALIZED.equals(strategyName)) {
            Assert.state(strategy != null, "When using " + MODE_PRE_INITIALIZED
                    + ", setContextHolderStrategy must be called with the fully constructed strategy");
            return;
        }
        if (!StringUtils.hasText(strategyName)) {
            // Set default
            strategyName = MODE_THREADLOCAL;
        }
        if (strategyName.equals(MODE_THREADLOCAL)) {
            strategy = new ThreadLocalSecurityContextHolderStrategy();
            return;
        }
        if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
            strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
            return;
        }
        if (strategyName.equals(MODE_GLOBAL)) {
            strategy = new GlobalSecurityContextHolderStrategy();
            return;
        }
        // Try to load a custom strategy
        try {
            Class<?> clazz = Class.forName(strategyName);
            Constructor<?> customStrategy = clazz.getConstructor();
            strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
        }
        catch (Exception ex) {
            ReflectionUtils.handleReflectionException(ex);
        }
    }
}
  1. MODE THREADLOCAL:这种存放策略是将 SecurityContext 存放在 ThreadLocal中,⼤家知道 Threadlocal 的特点是在哪个线程中存储就要在哪个线程中读取,这其实⾮常适合 web 应⽤,因为在默认情况下,⼀个请求⽆论经过多少 Filter 到达Servlet,都是由⼀个线程来处理的。这也是 SecurityContextHolder 的默认存储策略,这种存储策略意味着如果在具体的业务处理代码中,开启了⼦线程,在⼦线程中去获取登录⽤户数据,就会获取不到。
  2. MODE INHERITABLETHREADLOCAL:这种存储模式适⽤于多线程环境,如果希望在⼦线程中也能够获取到登录⽤户数据,那么可以使⽤这种存储模式。
  3. MODE GLOBAL:这种存储模式实际上是将数据保存在⼀个静态变量中,在 JavaWeb开发中,这种模式很少使⽤到。

2、SecurityContextHolderStrategy

通过 SecurityContextHolder 可以得知,SecurityContextHolderStrategy 接⼝⽤来定义存储策略⽅法
在这里插入图片描述
接⼝中⼀共定义了四个⽅法:

  • clearContext:该⽅法⽤来清除存储的 SecurityContext对象。
  • getContext:该⽅法⽤来获取存储的 SecurityContext 对象。
  • setContext:该⽅法⽤来设置存储的 SecurityContext 对象。
  • create Empty Context:该⽅法则⽤来创建⼀个空的 SecurityContext 对象

在这里插入图片描述
从上⾯可以看出每⼀个实现类对应⼀种策略的实现。

3、代码中获取认证之后⽤户数据

@GetMapping("/hello")
    public String Hello(){
        System.out.println("hello");
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        User user = (User) authentication.getPrincipal();
        System.out.println(user.getUsername());
        System.out.println(authentication.getAuthorities());
        System.out.println(authentication.getCredentials());
        new Thread(()->{
            System.out.println(SecurityContextHolder.getContext().getAuthentication().getPrincipal());
        }).start();
        return "hello, world";
    }

发现在多线程情况下是失效的,默认的策略是 MODE THREADLOCAL 是⽆法在⼦线程中获取⽤户信息,如果需要在⼦线程中获取必须使⽤第⼆种策略,默认策略是通过 System.getProperty 加载的,因此我们可以通过增加 VMOptions 参数进⾏修改。

-Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL

可以看到最终结果获取到了
在这里插入图片描述

4、页面上获取数据

对于在页面上获取数据,在官方中没有定义,只有自己导入第三方的包

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    <version>3.0.4.RELEASE</version>
</dependency>

在controller中写上测试的路由

@Controller
public class TestController {

    @RequestMapping("/test")
    public String test(){
        System.out.println("test is access");
        return "test";
    }
}

在页面中使用的时候,在页面上加上命名空间

<html lang="en" xmlns:th="https://www.thymeleaf.org"xmlns:sec="http://www.thymeleaf.org/extras/spring-security">

最后写上测试的代码:

<ul>
  <li sec:authentication="principal.username"></li>
  <li sec:authentication="principal.authorities"></li>
  <li sec:authentication="principal.accountNonExpired"></li>
  <li sec:authentication="principal.accountNonLocked"></li>
  <li sec:authentication="principal.credentialsNonExpired"></li>
</ul>

最后访问http://localhost:8080/test效果是
在这里插入图片描述

七、如何自定义认证数据源

1、认证流程分析

认证的流程在官方网站中有介绍https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html
在这里插入图片描述
步骤分析:

  1. 发起认证发起认证请求,请求中携带⽤户名、密码,该请求会被UsernamePasswordAuthenticationFilter拦截
  2. UsernamePasswordAuthenticationFilterattemptAuthentication⽅法中将请求中⽤户名和密码,封装为Authentication()对象,并交给AuthenticationManager 进⾏认证
在AuthenticationManager中有一个Authentication authenticate(Authentication authentication)方法,是传入一个UsernamePasswordAuthenticationToken对象,实际上他的超级父类是Authentication符合AuthenticationManager的定义。
  1. 认证成功,将认证信息存储到 SecurityContextHodler 以及调⽤记住我等,并回调AuthenticationSuccessHandler 处理
  2. 认证失败,清除SecurityContextHodler以及 记住我中信息,回调AuthenticationFailureHandler 处理

通过断点对整个认证流程分析之后,我们发现对于一个ProviderManager中的public Authentication authenticate(Authentication authentication)我们会调用两次,第一次不会有啥具体的动作,走的逻辑是public Authentication authenticate(Authentication authentication)方法中的

if (result == null && this.parent != null) {
    // Allow the parent to try.
    try {
        parentResult = this.parent.authenticate(authentication);
        result = parentResult;
    }
    catch (ProviderNotFoundException ex) {
        // ignore as we will throw below if no other exception occurred prior to
        // calling parent and the parent
        // may throw ProviderNotFound even though a provider in the child already
        // handled the request
    }
    catch (AuthenticationException ex) {
        parentException = ex;
        lastException = ex;
    }
}

调用的是父类的public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean AuthenticationManagerauthenticate方法,但是AuthenticationManager是一个接口,他的实现类是ProviderManager,所以最终效果观察到就是ProviderManager中的authenticate走了两次为什么要这样设计?

对于AuthenticationManagerProviderManagerAuthenticationProvider三者的关系?

  • AuthenticationManager 是认证的核⼼类,但实际上在底层真正认证时还离不开ProviderManager以及 AuthenticationProvider
  • AuthenticationManager 是⼀个认证管理器,它定义了 Spring Security 过滤器要执⾏认证操作。
    ProviderManager AuthenticationManager接⼝的实现类。Spring Security认证时默认使⽤就是 ProviderManager
  • AuthenticationProvider 就是针对不同的身份类型执⾏的具体的身份认证。

AuthenticationManager 与` ProviderManager的关系?
在这里插入图片描述

ProviderManagerAuthenticationManager 的唯⼀实现,也是 SpringSecurity 默认使⽤实现。从这⾥不难看出默认情况下AuthenticationManager 就是⼀个ProviderManager

ProviderManager 与 AuthenticationProvider的关系?
在这里插入图片描述
Spring Seourity中,允许系统同时⽀持多种不同的认证⽅式,例如同时⽀持⽤户名/密码认证、ReremberMe 认证、⼿机号码动态认证等,⽽不同的认证⽅式对应了不同的 AuthenticationProvider,所以⼀个完整的认证流程可能由多个AuthenticationProvider 来提供。

在这里插入图片描述
多个AuthenticationProvider将组成⼀个列表,这个列表将由ProviderManager 代理。换句话说,在ProviderManager 中存在⼀个AuthenticationProvider 列表,在Provider Manager 中遍历列表中的每⼀个AuthenticationProvider 去执⾏身份认证,最终得到认证结果。

ProviderManager 本身也可以再配置⼀个 AuthenticationManager 作为parent,这样当ProviderManager 认证失败之后,就可以进⼊到parent中再次进⾏认
证。理论上来说,ProviderManagerparent 可以是任意类型的AuthenticationManager,但是通常都是由ProviderManager(是一个默认的实现类) 来扮演parent的⻆⾊,也就是 ProviderManager ProviderManager parent

ProviderManager 本身也可以有多个,多个ProviderManager 共⽤同⼀个parent。有时,⼀个应⽤程序有受保护资源的逻辑组(例如,所有符合路径模式的⽹络资源,如/api/**),每个组可以有⾃⼰的专⽤ AuthenticationManager。通常,每个组都是⼀个ProviderManager,它们共享⼀个⽗级。然后,⽗级是⼀种 全局资源,作为所有提供者的后备资源。根据上⾯的介绍,我们绘出新的 AuthenticationManagerProvideManagerAuthentictionProvider 关系。

下面的图来自于https://spring.io/guides/topicals/spring-security-architecture/
在这里插入图片描述
明白了上述的调用关系之后,通过断点可以看到:
刚开始进来的时候,进入的是局部的ProviderManager
在这里插入图片描述
在局部不能处理之后,直接进入到全局,就会发现全局是有一个默认的Provider就是DaoAuthenticationProvider
在这里插入图片描述
之后进入DaoAuthenticationProvider中的 provider.authenticate(authentication),后面发现DaoAuthenticationProvider并没有实现authenticate方法,找到父类AbstractUserDetailsAuthenticationProvider中的authenticate做逻辑操作。
其中有个关键的方法调用就是user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);去获取用户。

获取到用户之后,调用方法additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);来进行加密之后密码的匹配逻辑。

抛开其他的先不表,先看看retrieveUser方法,retrieveUser方法是在DaoAuthenticationProvider中的,其中最主要的就是UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);方法,从UserDetailsService中的loadUserByUsername通过用户名获取用户

弄清楚认证原理之后我们来看下具体认证时数据源的获取。默认情况下AuthenticationProvider 是由 DaoAuthenticationProvider 类来实现认证的,在DaoAuthenticationProvider 认证时⼜通过 UserDetailsService 完成数据源的校验。
在这里插入图片描述
总结:
AuthenticationManager 是认证管理器,在 Spring Security 中有全局AuthenticationManager,也可以有局部AuthenticationManager。全局的
AuthenticationManager⽤来对全局认证进⾏处理,局部的AuthenticationManager⽤来对某些特殊资源认证处理。当然⽆论是全局认证管理器还是局部认证管理器都是由ProviderManger 进⾏实现。 每⼀个ProviderManger中都代理⼀个AuthenticationProvider的列表,列表中每⼀个实现代表⼀种身份认证⽅式。认证时底层数据源需要调⽤ UserDetailService 来实现。

通过上述的分析,在不是很复杂的系统中,我们没有必对不同的路径使用不同的认证方法的拆分,也就不需要局部的ProviderManager,所以直接配置全局的ProviderManager

2、配置全局 AuthenticationManager

https://spring.io/guides/topicals/spring-security-architecture/
看一个作用的效果,在UserDetailsServiceAutoConfiguration中发现默认的数据源使用的条件是

@ConditionalOnMissingBean(
        value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class,
                AuthenticationManagerResolver.class },

如果有自己自定义的UserDetailsService,就会使用自己自定义的UserDetailsService
对于自己定义全局的AuthenticationManager有两种方法:

  1. 默认的全局 AuthenticationManager
  2. 完全使用自己自定义的

默认全局的AuthenticationManager,就是在原来的AuthenticationManager上加上一些功能。

@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {

   ... // web stuff here

  @Autowired
  public void initialize(AuthenticationManagerBuilder builder, DataSource dataSource) {
    builder.jdbcAuthentication().dataSource(dataSource).withUser("dave")
      .password("secret").roles("USER");
  }
}

springboot security进⾏⾃动配置时⾃动在⼯⼚中创建⼀个全局AuthenticationManager.
总结:

  • 默认⾃动配置创建全局AuthenticationManager 默认找当前项⽬中是否存在⾃定义 UserDetailService 实例 ⾃动将当前项⽬ UserDetailService 实例设置为数据源
  • 默认⾃动配置创建全局AuthenticationManager 在⼯⼚中使⽤时直接在代码中注⼊即可

对于以下的代码会出现错误:org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name ‘webSecurityConfigurer’: Requested bean is currently in creation: Is there an unresolvable circular reference?
在这里插入图片描述

3、完全自定义全局 AuthenticationManager

@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {

  @Autowired
  DataSource dataSource;

   ... // web stuff here

  @Override
  public void configure(AuthenticationManagerBuilder builder) {
    builder.jdbcAuthentication().dataSource(dataSource).withUser("dave")
      .password("secret").roles("USER");
  }
}

⾃定义全局 AuthenticationManager,完全自定义AuthenticationManager是对自动配置的所有默认配置都进行抹除掉。
总结

  1. ⼀旦通过 configure ⽅法⾃定义 AuthenticationManager实现 就回将⼯⼚中⾃动配置AuthenticationManager 进⾏覆盖
  2. ⼀旦通过configure⽅法⾃定义 AuthenticationManager实现 需要在实现中指定认证数据源对象 UserDetaiService 实例
  3. ⼀旦通过configure⽅法⾃定义 AuthenticationManager实现 这种⽅式创建AuthenticationManager对象⼯⼚内部本地⼀个 AuthenticationManager对象 不允许在其他⾃定义组件中进⾏注⼊

在这里插入图片描述
这样就能实现完全替换数据源。
⽤来在⼯⼚中暴露⾃定义AuthenticationManager 实例
在这里插入图片描述

八、自定义数据库数据源

  • 设计表结构
-- 用户表
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;
  • 项目中引入依赖
<!--自定义mybatis-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.0</version>
</dependency>
<!--mysql的自定义连接-->
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <version>8.0.32</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.7</version>
</dependency>
  • 配置文件
# 配置数据源
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/springsecurity
spring.datasource.username=root
spring.datasource.password=admin123

# mybatis的配置,注意目录必须使用/
mybatis.mapper-locations=classpath:com/fckey/mapper/*.xml
mybatis.type-aliases-package=com.fckey.entity

# 日志处理,为了展示mybaits查询时候能打印出sql
logging.level.com.fckey=debug
  • 创建 entity
    • 创建 user 对象
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;
    }
        //get/set....
}
  • 创建 role 对象
public class Role {
    private Integer id;
    private String name;
    private String nameZh;
      //get set..
}
  • 创建UserMapper接口和xml
public interface UserMapper {
    /**
     * @author Jeff Fong
     * @description 通过用户名获取用户信息,但是密码比对的工作是直接交给框架来做的
     * @date 2023/5/25 17:16
     * @param: username
     * @return org.springframework.security.core.userdetails.UserDetails
     **/
    User loadUserByUsername(String username);

    /**
     * @author Jeff Fong
     * @description 根据用户的id查询角色
     * @date 2023/5/25 17:28
     * @param: uid
     * @return java.util.List<com.fckey.entity.Role>
     **/
    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.fckey.mapper.UserMapper">
    <!--根据用户名查询到用户-->
    <select id="loadUserByUsername" resultType="user">
        select id,
               username,
               password,
               enabled,
               accountNonExpired,
               accountNonLocked,
               credentialsNonExpired
        from user
        where username = #{username}
    </select>

    <!--查询指定行数据-->
    <select id="getRolesByUid" resultType="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>

创建 UserDetailService 实例

@Component
public class MyUserDetailService implements UserDetailsService {private  final UserDao userDao;@Autowired
    public MyUserDetailService(UserDao userDao) {
        this.userDao = userDao;
    }@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userDao.loadUserByUsername(username);
        if(ObjectUtils.isEmpty(user))throw new RuntimeException("用户不存在");
        user.setRoles(userDao.getRolesByUid(user.getId()));
        return user;
    }
}
  • 配置 authenticationManager 使用自定义UserDetailService
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
  
    private final UserDetailsService userDetailsService;@Autowired
    public WebSecurityConfigurer(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }@Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
        builder.userDetailsService(userDetailsService);
    }
  
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
      //web security..
    }
}
  • 启动测试
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要从MyBatis获取Spring Security的数据,需要进行以下步骤: 1. 在Spring Security中配置一个自定义的UserDetailsService。这个UserDetailsService需要从数据库中获取用户信息,并返回一个UserDetails对象。 2. 在MyBatis中配置一个Mapper接口,用于从数据库中获取用户信息。 3. 在UserDetailsService中调用MyBatis的Mapper接口,从数据库中获取用户信息。 4. 将获取到的用户信息封装为一个UserDetails对象,并返回给Spring Security使用。 以下是示例代码: 1. 在Spring Security中配置一个自定义的UserDetailsService: ``` @Service public class CustomUserDetailsService implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 从MyBatis中获取用户信息 User user = userMapper.getUserByUsername(username); if (user == null) { throw new UsernameNotFoundException("User not found with username: " + username); } // 将获取到的用户信息封装为一个UserDetails对象 UserDetails userDetails = User.builder() .username(user.getUsername()) .password(user.getPassword()) .roles(user.getRoles()) .build(); return userDetails; } } ``` 2. 在MyBatis中配置一个Mapper接口: ``` @Mapper public interface UserMapper { @Select("SELECT * FROM users WHERE username = #{username}") User getUserByUsername(String username); } ``` 3. 在UserDetailsService中调用MyBatis的Mapper接口: ``` @Service public class CustomUserDetailsService implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 从MyBatis中获取用户信息 User user = userMapper.getUserByUsername(username); if (user == null) { throw new UsernameNotFoundException("User not found with username: " + username); } // 将获取到的用户信息封装为一个UserDetails对象 UserDetails userDetails = User.builder() .username(user.getUsername()) .password(user.getPassword()) .roles(user.getRoles()) .build(); return userDetails; } } ``` 4. 将获取到的用户信息封装为一个UserDetails对象,并返回给Spring Security使用。 以上步骤完成后,就可以从MyBatis中获取Spring Security的用户信息了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值