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

news2024/11/30 2:43:46

文章目录

  • 一、自定义认证
  • 二、自定义登录界面
  • 三、自定义成功的处理
    • 1、前后端分离返回json
  • 四、如何在前端显示异常
    • 1、传统的方法
    • 2、前后端分离返回错误的json
  • 五、注销登录配置
    • 1、注销登录前后端不分离
    • 2、前后端分离的方法
  • 六、登录⽤户数据获取
    • 1、SecurityContextHolder
    • 2、SecurityContextHolderStrategy
    • 3、代码中获取认证之后⽤户数据
    • 4、页面上获取数据
  • 七、如何自定义认证数据源
    • 1、认证流程分析
    • 2、配置全局 AuthenticationManager
    • 3、完全自定义全局 AuthenticationManager
  • 八、自定义数据库数据源

一、自定义认证

对于在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..
    }
}
  • 启动测试

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/572235.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Netty】Netty 程序引导类(九)

文章目录 前言一、引导程序类二、AbstractBootStrap 抽象类三、Bootstrap 类四、ServerBootstrap 类五、引导服务器5.1、 实例化引导程序类5.2、设置 EventLoopGroup5.3、指定 Channel 类型5.4、指定 ChannelHandler5.5、设置 Channel 选项5.6、绑定端口启动服务 六、引导客户端…

STL-reverse_iterator 反向迭代器

回顾 对于STL中的容器&#xff0c;迭代器(iterator)是很重要的部分&#xff0c;同时迭代器也是STL六大组件之一&#xff0c;在之前我们实现vector和list中&#xff0c;我们已经对于迭代器有了初步的认识&#xff0c;为什么设计迭代器&#xff1f; 就是为了能像数组中的指针一样…

虹科干货|创新求变:虹科Redis企业版数据库驱动金融实时业务

BDO调查显示&#xff0c;43%的金融企业正计划全力加速数字化转型&#xff0c;互联网巨头与金融科技初创公司正在颠覆传统。”与此同时&#xff0c;客户行为、消费习惯和期望持续变化&#xff0c;以客户为中心的快速金融服务已成趋势&#xff0c;企业微服务、云原生应用亟需一组…

【算法题解】30. 全排列的递归解法

这是一道 中等难度 的题 https://leetcode.cn/problems/permutations/ 题目 给定一个不含重复数字的数组 n u m s nums nums &#xff0c;返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。 示例 1&#xff1a; 输入&#xff1a;nums [1,2,3] 输出&#xff1a;[[1…

景点解说二维码怎么做?一键教你轻松生成二维码

现在的各种景区为了节省人工都会将景点的详细讲解做成二维码。通过手机扫码就能自助导览。那么&#xff0c;大家知道这种景区讲解二维码是怎么制作的吗&#xff1f; 一、什么工具能制作二维码图片&#xff1f; 机智熊二维码生成器&#xff08;https://www.jzx.com/&#xff09;…

什么是高性能计算实习生?做高性能计算有前景吗?

随着大模型和算力时代的大火&#xff0c;高性能计算实习的岗位越来越多了&#xff0c;各个大厂都在码人&#xff0c;百度、小米、字节、华为等等&#xff0c;也有很多网友晒出了面试一众知名芯片企业的面经和笔试题。 但是依然有很多朋友不清楚什么是高性能计算实习生&#xf…

搜索推荐系统[10]项目实战系列Z1:手把手教学(商品搜索系统、学术文献检索)语义检索系统搭建、召回排序模型详解。

搜索推荐系统专栏简介:搜索推荐全流程讲解(召回粗排精排重排混排)、系统架构、常见问题、算法项目实战总结、技术细节以及项目实战(含码源) 专栏详细介绍:搜索推荐系统专栏简介:搜索推荐全流程讲解(召回粗排精排重排混排)、系统架构、常见问题、算法项目实战总结、技术…

每日一练 | 网络工程师软考真题 Day12

阅读以下说明&#xff0c;答复以下【问题1】至【问题3】 【说明】 某单位有1个总部和6个分部&#xff0c;各个部门都有自己的局域网。该单位申请了6个C类IP地址202.115.10.0/24~202.115.15.0/24&#xff0c;其中总部与分部4共用一个C类地址。现方案将这些部门用路由器互联&…

linuxOPS基础_操作系统概述

计算机发展史 第一台计算机是1946 年2 月14 日诞生日&#xff0c;第一台名称ENIAC。体积一间屋子的大小&#xff0c;重量高达28t。 第一代&#xff1a;1946 – 1958 > 12 年 &#xff08;电子管&#xff09; 第二代&#xff1a;1958 – 1964 > 6 年 &#xff08;晶体管…

VR数字展厅——助力商企实现数字化营销展示

近年来&#xff0c;随着元宇宙、虚拟现实等概念逐渐进入大众视野&#xff0c;VR虚拟展厅也慢慢的发展成为了一种新的展示形式。VR数字展厅可以将展示场景复刻在线上&#xff0c;不再受限于线下环境&#xff0c;随着VR全景技术的高速发展&#xff0c;虚拟展厅帮助商企实现更具创…

一起CPU很闲,load却很大的案例分析

1、软硬件环境 硬件&#xff1a; 飞腾E2000Q 平台 软件&#xff1a; linux 4.19.246 2、问题现象 系统在上电后&#xff0c;无意中发现系统的平均负载很大&#xff0c;数值显示远超过cpu的承载能力。心想也没有跑什么业务程序呀&#xff0c;吓得赶紧运行top命令&#xff0c;瞅…

无人车端到端驾驶模型概述

摘要&#xff1a; 通常&#xff0c;端到端驾驶模型使用一个深度神经网络来完成这种映射&#xff0c;网络的所有参数为联合训练而得。这种方法因它的简洁高效而引人关注。 引言 在搭建无人车时&#xff0c;我和小伙伴们的主要工作是建立一个驾驶模型。所谓的驾驶模型是控制无人…

idea模板配置

idea版本&#xff1a;2023.1 未设置模板的idea&#xff0c;新建类会自动生成类注释 格式如下&#xff1a; /*** author user* date 2023/5/20 0020 14:25*/ public class User {} 其中&#xff0c;user为当前用户名 这里&#xff0c;如果希望将类注释改写成如下&#xff0…

“卷”还是“躺平”?职场人如何在工作中找到价值感?

今天不谈技术&#xff0c;只谈进步。 曾经看过一个回答说“职场人最好的姿势是仰卧起坐”。 卷累的就躺&#xff0c;休息好了再继续卷&#xff0c;卷是常态&#xff0c;“仰卧起坐”也好&#xff0c;“卷的姿势”也好&#xff0c;都是在反复“卷起”的过程中寻找一些舒适和平衡…

Z-Library2023现状

网上基本上年年都会传出来Z-Library要被干掉的消息&#xff0c;我一直觉得&#xff0c;如果那真的发生了&#xff0c;会是人类的悲哀。 由于之前我存储的地址又挂了&#xff0c;所以紧急又寻找了一下。 1.朋友帮忙 朋友帮我搜了一下&#xff0c;发现有三个地址。 他说这第一个…

智能CAN/串口协议转换器LCNET Pro RS-232/485

智能CAN/串口协议转换器LCNET Pro RS-232/485提供一路RS-485、一路RS-232和一路CAN通道&#xff0c;实现CAN与串口RS-485或RS-232之间的双向数据智能转换。每个通道独立隔离&#xff0c;每路通道采用金升阳电源模块和信号隔离芯片实现2500VDC电气隔离&#xff0c;电源输入防反设…

基于Redis的Java分布式锁,接口并发处理,并发方案

Redis的分布式锁很多人都知道&#xff0c;比如使用Jedis的setNx、incr等方法都可以实现分布式锁的功能&#xff0c;但是Jedis需要自己管理连接池&#xff0c;就稍微麻烦一点。 今天介绍的是使用RedisTemplate切面编程自定义注解SPEL来实现分布式锁的功能&#xff0c;封装完成后…

Spring Cloud 容错机试 Hystrix 服务降级 RestTemplate:

Ribon的服务降级操作 雪崩效应&#xff1a; 如果短信服务炸了后面的所有服务就会起连锁反应造成全部服务挂掉&#xff0c;这就是雪崩效应&#xff0c;那么其实短信服务又不是我们主要业务&#xff0c;这个时候我们可以采用服务降级&#xff0c;服务降级就是暂时的把短信服务停…

java学习——ArrayList和泛型(学习记录)

学习资料来自菜鸟教程 ArrayList 类是一个可以动态修改的数组&#xff0c;与普通数组的区别就是它是没有固定大小的限制&#xff0c;我们可以添加或删除元素。 ArrayList 继承了 AbstractList &#xff0c;并实现了 List 接口。 ArrayList 类位于 java.util 包中&#xff0c;使…

事件驱动模型IO模型

什么是事件驱动模型&#xff1f; 事件驱动模型是一种计算机编程模型&#xff0c;它通过等待事件的触发&#xff0c;在事件被触发时执行对应的处理函数。这种模型下&#xff0c;程序不再按照严格的顺序执行命令&#xff0c;而是以事件为驱动进行执行。事件驱动模型更适合处理大…