文章目录
- 一、自定义认证
- 二、自定义登录界面
- 三、自定义成功的处理
- 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>
但是,对于post
,method
和用户名和密码的name
怎么配置,我们知道给予默认页面的是UsernamePasswordAuthenticationFilter
,如果需要自己自定义页面,如何符合规定。
一共有三项要求:
- 登录表单
method
必须为post
,action
的请求路径为/doLogin
- ⽤户名的
name
属性为uname
- 密码的
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
对于上面的内容有successForwardUrl
和defaultSuccessUrl
表认证成功之后的跳转页面,对于主流的前后端分离模式,这样子难免会出问题。如果是希望返回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
在登录失败之后会将异常信息存储到 request
、session
作⽤域中key
为SPRING_SECURITY_LAST_EXCEPTION 命名属性中
在前端显示异常信息
在配置类中进行配置
在前端错误信息的获取,通过上面源码的分析可知:
failureUr
l、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;
}
根据接⼝的描述信息,也可以得知登录失败会⾃动回调这个⽅法,进⼀步查看它的默认实现,你会发现failureUrl
、failureForwardUrl
也是由它的⼦类实现的
⾃定义 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);
}
}
}
MODE THREADLOCAL
:这种存放策略是将 SecurityContext 存放在 ThreadLocal中,⼤家知道 Threadlocal 的特点是在哪个线程中存储就要在哪个线程中读取,这其实⾮常适合 web 应⽤,因为在默认情况下,⼀个请求⽆论经过多少 Filter 到达Servlet,都是由⼀个线程来处理的。这也是 SecurityContextHolder 的默认存储策略,这种存储策略意味着如果在具体的业务处理代码中,开启了⼦线程,在⼦线程中去获取登录⽤户数据,就会获取不到。MODE INHERITABLETHREADLOCAL
:这种存储模式适⽤于多线程环境,如果希望在⼦线程中也能够获取到登录⽤户数据,那么可以使⽤这种存储模式。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
步骤分析:
- 发起认证发起认证请求,请求中携带⽤户名、密码,该请求会被
UsernamePasswordAuthenticationFilter
拦截 - 在
UsernamePasswordAuthenticationFilter
的attemptAuthentication
⽅法中将请求中⽤户名和密码,封装为Authentication()
对象,并交给AuthenticationManager
进⾏认证
在AuthenticationManager中有一个Authentication authenticate(Authentication authentication)方法,是传入一个UsernamePasswordAuthenticationToken对象,实际上他的超级父类是Authentication符合AuthenticationManager的定义。 |
- 认证成功,将认证信息存储到
SecurityContextHodler
以及调⽤记住我等,并回调AuthenticationSuccessHandler
处理 - 认证失败,清除
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
中AuthenticationManager
的authenticate
方法,但是AuthenticationManager
是一个接口,他的实现类是ProviderManager
,所以最终效果观察到就是ProviderManager
中的authenticate
走了两次为什么要这样设计?
对于AuthenticationManager
、ProviderManager
、AuthenticationProvider
三者的关系?
AuthenticationManager
是认证的核⼼类,但实际上在底层真正认证时还离不开ProviderManager
以及AuthenticationProvider
AuthenticationManager
是⼀个认证管理器,它定义了Spring Security
过滤器要执⾏认证操作。
ProviderManager AuthenticationManager
接⼝的实现类。Spring Security
认证时默认使⽤就是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
,它们共享⼀个⽗级。然后,⽗级是⼀种 全局资源,作为所有提供者的后备资源。根据上⾯的介绍,我们绘出新的 AuthenticationManager
、ProvideManager
和AuthentictionProvider
关系。
下面的图来自于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
有两种方法:
- 默认的全局
AuthenticationManager
- 完全使用自己自定义的
默认全局的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
是对自动配置的所有默认配置都进行抹除掉。
总结
- ⼀旦通过
configure
⽅法⾃定义AuthenticationManager
实现 就回将⼯⼚中⾃动配置AuthenticationManager
进⾏覆盖 - ⼀旦通过
configure
⽅法⾃定义AuthenticationManager
实现 需要在实现中指定认证数据源对象UserDetaiService
实例 - ⼀旦通过
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..
}
}
- 启动测试