Spring Security 自定义资源认证规则

自定义资源认证规则

在这里插入图片描述

自定义资源权限规则

  • /index公共资源
  • /hello受保护资源

在项目中添加如下配置就可以实现对资源权限规则设定:

创建一个配置类,继承WebSecurityConfigurerAdapter,重写其中的configure(HttpSecurity http)方法,最终在类上使用@Configuration.

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                .mvcMatchers("/index")
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin();
    }
}
# 说明
- permitAll() 代表放行该资源,该资源为公共资源,无需认证和授权可以直接访问
- anyRequest().authenticated()代表所有请求,必须认证之后才能访问
- formLogin() 代表开启表单认证
## 注意:放行资源必须放在所有认证请求之前!

1.自定义登录界面

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                .mvcMatchers("/loginHtml").permitAll()  //请求路径,即Controller路径,放行自定义登录界面
                .mvcMatchers("/index").permitAll()   //请求路径/index放行
                .anyRequest().authenticated()   //其他的任何请求都要进行认证
                .and()
                .formLogin() //表单验证
                .loginPage("/loginHtml") //默认登录页面,也是通过请求进行跳转至默认登录页面
                .loginProcessingUrl("/doLogin")    //指定发送过来的/doLogin请求被捕获,进行权限验证。
                .usernameParameter("uname") //默认接收username参数,修改为uname
                .passwordParameter("passwd") //默认接收password参数,修改为passwd
//                .successForwardUrl("/index")   //认证成功 forward跳转路径 地址栏不变  每次都默认跳转到/index
                .defaultSuccessUrl("/index",true)   //认证成功之后的跳转  重定向
                // 如果之前保存了请求但是被拦截,在拦截之后先跳转到被保存的请求,如果没有,则跳转到指定的请求。
                // 如果第二个参数设置为true,则无论如何,只要认证成功,不管有没有保存的请求直接跳转到指定的请求。
                .and()
                .csrf().disable();   //禁止 csrf 跨域请求保护
    }
}

在这里插入图片描述

在这里插入图片描述

需要注意的是

  • 登录表单method必须为post。

  • antion的请求路径与配置类中的loginProcessingUrl()一致。

  • 用户名密码的参数也需要与配置类中的usernameParameter()、passwordParameter()一致。

  • successForwardUrl、defaultSuccessUrl这个两个方法都可以实现成功之后跳转

    • successForwardUrl默认使用forward跳转 注意:不会跳转到之前请求路径
    • defaultSuccessUrl默认使用redirect跳转 注意:如果之前请求路径,会优先跳转之前请求路径,可以传入第二个参数进行修改。

2.自定义登录成功处理

有时候页面跳转并不能满足我们,特别是在前后端分离开发中就不需要成功之后跳转页面。只需要给前端返回一个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<String,Object>();
        result.put("msg","登录成功");
        result.put("status",200);
        response.setContentType("application/json;charset=UTF-8");
        String s = new ObjectMapper().writeValueAsString(result);
        response.getWriter().println(s);
    }
}
  • 配置AuthenticationSuccessHandler
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                //...
                .and()
                .formLogin() //表单验证
				//...
				//自定义登录成功处理
                .successHandler(new MyAuthenticationSuccessHandler())
                .failureForwardUrl("/loginHtml")
                .and()
                .csrf().disable();   //禁止 csrf 跨域请求保护
    }
}

在这里插入图片描述

3.显示登录失败信息

为了能够更直观在登录页面看到异常错误信息,可以在登录页面中直接获取异常信息。Spring Security在登录失败之后会将异常信息存储到request,session作用域中key为SPRING_SECURITY_LAST_EXCEPTION命名属性中。

源码可以参考:SimpleUrlAuthenticationFailureHandler

  • 显示异常信息

在这里插入图片描述

  • 配置
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                //...
                .and()
                .formLogin() //表单验证
				//...
              //.failureForwardUrl("/loginHtml") //认证失败之后的forward跳转    forward  -->异常信息存放在request
                .failureUrl("/loginHtml")  //  默认  认证失败之后的 redirect 跳转 redirect -->异常信息存放在session
                .and()
                .csrf().disable();   //禁止 csrf 跨域请求保护
    }
}

在这里插入图片描述

4.自定义登录失败处理

和自定义登录成功处理一样,Spring Security同样为前后端分离开发提供了登录失败的处理,这个类就是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","登录失败:"+exception.getMessage());
        result.put("status",500);
        response.setContentType("application/json;charset=UTF-8");
        String s = new ObjectMapper().writeValueAsString(result);
        response.getWriter().println(s);
    }
}

配置AuthenticationFailureHandler

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
				//...
                .and()
                .formLogin() //表单验证
				//...
               .successHandler(new MyAuthenticationSuccessHandler()) //用来自定义认证成功之后处理 前后端分离解决方案
//             .failureForwardUrl("/loginHtml") //认证失败之后的forward跳转    forward  -->异常信息存放在request
//             .failureUrl("/loginHtml")  //  默认  认证失败之后的 redirect 跳转 redirect -->异常信息存放在session
              .failureHandler(new MyAuthenticationFailureHandler()) //用来自定义认证失败之后处理  前后端分离解决方案
                .and()
                .csrf().disable();   //禁止 csrf 跨域请求保护
    }
}

在这里插入图片描述

5.注销登录

Spring Security中提供了默认的注销登录配置,在开发时也可以按照自己需求对注销进行个性化定制。

  • 开启注销登录 默认开启

    @Configuration
    public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests()
    				//...
                    .and()
                    .formLogin() //表单验证
    				//...
                    .and()
                    .logout()   //开启注销登录  默认
                    .logoutUrl("/logout")  //指定注销登录 url  默认 默认请求方式:GET
                    .invalidateHttpSession(true) //默认 会话失效
                    .clearAuthentication(true)  //默认 清楚认证标记
                    .logoutSuccessUrl("/loginHtml") //注销登录成功之后跳转
                    .and()
                    .csrf().disable();   //禁止 csrf 跨域请求保护
        }
    }
    
    • 通过logout()方法注销配置
    • logoutUrl指定退出登录请求地址,默认是GET请求,路径为/logout
    • invalidateHttpSession退出时是否是session失效,默认值为true
    • clearAuthentication退出时是否清楚认证信息,默认值为true
    • logoutSuccessUrl退出登录时跳转地址
  • 配置多个注销登录请求

    如果项目中有需要,开发者还可以配置多个注销登录的请求,同时还可以指定请求的方法:

    @Configuration
    public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests()
    				//...
                    .and()
                    .formLogin() //表单验证
    				//...
                    .and()
                    .logout()   //开启注销登录  默认
                    .logoutRequestMatcher(new OrRequestMatcher(
                            new AntPathRequestMatcher("/aa","GET"),
                            new AntPathRequestMatcher("/bb","POST")
                    ))      //
                    .invalidateHttpSession(true) //默认 会话失效
                    .clearAuthentication(true)  //默认 清楚认证标记
                    .logoutSuccessUrl("/loginHtml") //注销登录成功之后跳转
                    .and()
                    .csrf().disable();   //禁止 csrf 跨域请求保护
        }
    }
    
  • 前后端分离注销登录配置

如果是前后端分离开发,注销成功之后就不需要页面跳转了,只需要将注销成功的信息返回前端即可,此时我们可以通过自定义LogoutSuccessHandler实现来返回内容注销之后信息:

public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        Map<String,Object> result = new HashMap<String,Object>();
        result.put("msg","注销成功");
        result.put("status",200);
        response.setContentType("application/json;charset=UTF-8");
        String s = new ObjectMapper().writeValueAsString(result);
        response.getWriter().println(s);
    }
}

配置

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
				//...
                .and()
                .formLogin() //表单验证
				//...
                .and()
                .logout()   //开启注销登录  默认
                .logoutRequestMatcher(new OrRequestMatcher(
                        new AntPathRequestMatcher("/aa","GET"),
                        new AntPathRequestMatcher("/bb","POST")
                ))      //
                .invalidateHttpSession(true) //默认 会话失效
                .clearAuthentication(true)  //默认 清楚认证标记
                .logoutSuccessHandler(new MyLogoutSuccessHandler())  //注销登录成功之后处理
                .and()
                .csrf().disable();   //禁止 csrf 跨域请求保护
    }
}

在这里插入图片描述

6.登录用户数据获取

6.1.SecurityContextHolder

​ Spring Security会将登录用户数据保存在Session中。但是,为了使用方便,Spring Security在此基础上还做了一些改进,其中最主要的一个变化就是线程绑定。当用户登录成功后,Spring Security会将登录成功的用户信息保存到SecurityContextHolder中。

​ SecurityContextHolder中的数据保存默认是通过ThreadLocal来实现的,使用ThreadLocal创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是用户数据和请求线程绑定在一起。当登录请求处理完毕后,Spring Security会将SecurityContextHolder中的数据拿出来保存到Session中,同时将SecurityContextHolder中的数据清空。以后每当有请求到来时,Spring Security就会先从Session中取出用户登录数据,保存到SecurityContextHolder中,方便在该请求的后续处理过程中使用,同时在请求结束时将SecurityContextHolder中的数据拿出来保存到Session中,然后将SecurityContextHolder中的数据清空。

实际那个SecurityContextHolder中存储的是SecurityContext,在SecurityContext中存储是Authentication。

在这里插入图片描述

这种设计是典型的策略设计模式:

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:这种存储模式适用于多线程环境,如果希望在子线程中ue能够获取到登录用户数据,那么可以使用这种存储模式。
  3. MODE GLOBAL:这种存储模式实际上是将数据保存在一个静态变量中,在JavaWeb开发中,这种模式很少使用到。
6.2.SecurityContextHolderStrategy

通过SecurityContextHolder可以得知,SecurityContextHolderStrategy接口用来定义存储策略方法

public interface SecurityContextHolderStrategy {

	void clearContext();

	SecurityContext getContext();

	void setContext(SecurityContext context);

	SecurityContext createEmptyContext();

}

接口中一共定义了四个方法:

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

在这里插入图片描述

从上面可以看出每一个实现类对应一种策略的实现。

在这里插入图片描述

6.3.代码中获取认证之后用户数据
@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello(){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        User user = (User)authentication.getPrincipal();
        System.out.println("身份信息:"+user.getUsername());
        System.out.println("权限信息:"+authentication.getAuthorities());

        new Thread(()->{
            Authentication authentication1 = SecurityContextHolder.getContext().getAuthentication();
            System.out.println("子线程:"+authentication1);

        }).start();
        System.out.println("hello security");
        return "hello spring security";
    }
}
6.4.多线程情况下获取用户数据

从源码中可以知道,该策略可以从系统参数中获取,因此覆盖系统中的SYSTEM_PROPERTY参数即可。

在这里插入图片描述

-Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL

在这里插入图片描述

因此在启动项目是设置系统参数即可。

在这里插入图片描述

6.5.页面上获取用户信息
  • 引入依赖
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
            <version>3.0.4.RELEASE</version>
        </dependency>
  • 页面加入命名空间
<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>

在这里插入图片描述

7.自定义认证数据源

7.1.认证流程分析

在这里插入图片描述

  • 发起认证请求,请求中携带用户名、密码,该请求会被UsernamePasswordAuthenticationFilter拦截
  • UsernamePasswordAuthenticationFilterattemptAuthentication方法中将请求中用户名和密码,封装为Authentication对象,并交给AuthenticationManager进行认证
  • 认证成功,将认证信息存储到SecurityContextHolder以及调用RememberMe记住我的操作,并回调AuthenticationSuccessHandler处理
  • 认证失败,清除SecurityContextHolder以及记住我中的信息,回调AuthenticationFailureHandler处理
7.2.三者关系

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

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

AuthenticationManager与ProviderManager

在这里插入图片描述

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

AuthenticationManager就是一个ProviderManager。

ProviderManager与AuthenticationProvider

https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html

在这里插入图片描述

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

​ 多个AuthenticationProvider将组成一个列表,这个列表将由ProviderManager代理。换句话说,在ProviderManager中存在一个AuthenticationProvider列表,在ProviderManager中遍历列表中的每一个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用来对某些特殊资源认证处理。当然无论是全局认证管理器还是局部认证管理器都是由ProviderManager进行实现。每一个ProviderManager中都代理一个AuthenticationProvier的列表,列表中每一个实现代表一种身份的认证方式。认证时底层数据源需要调用UserDetailService来实现。

7.3.配置全局AuthenticationManager

https://spring.io/guides/topical/spring-security-architecture

  • 默认的全局AuthenticationManager
    //方法名字自定义
	@Autowired
    public void initialize(AuthenticationManagerBuilder builder) throws Exception {
       System.out.println("springboot 默认配置:" + builder);
      InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
        userDetailsService.createUser(User.withUsername("zkt").password("{noop}123").roles("admin").build());
        builder.userDetailsService(userDetailsService);
    }
    //方法名字自定义(上述为一个示例)
	@Autowired
    public void initialize(AuthenticationManagerBuilder builder) throws Exception {
		//builder...
    }

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

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

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

  • 自定义全局AuthenticationManager
//    自定义AuthenticationManager
    @Override
    public void configure(AuthenticationManagerBuilder builder) throws Exception {
        System.out.println("自定义AuthenticationManager:" + builder);
        InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
        userDetailsService.createUser(User.withUsername("zkt").password("{noop}root").roles("admin").build());
        builder.userDetailsService(userDetailsService);
    }
//    自定义AuthenticationManager(上述为一个实例)
    @Override
    public void configure(AuthenticationManagerBuilder builder) throws Exception {
		//...
    }

总结:1.一旦通过configure方法自定义AuthenticationManager实现 就会将工厂中自动配置Authentication进行覆盖

2.一旦通过configure方法自定义AuthenticationManager实现 需要在实现中指定认证数据源对象UserDetailService实例

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

  • 用来在工厂中暴露自定义AuthenticationManager实例
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

    @Bean
    public UserDetailsService userDetailsService(){
        InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
        userDetailsService.createUser(User.withUsername("zkt").password("{noop}123").roles("admin").build());
        return userDetailsService;
    }



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

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

设计表结构

-- 用户表
CREATE TABLE `user`
(
	`id` int(11) NOT NULL AUTO_INCREMENT,
	`username` varchar(32) DEFAULT NULL,
	`password` varchar(255) DEFAULT NULL,
	`enable` 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;
	

引入依赖

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

        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.38</version>
        </dependency>

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

配置springboot配置文件

#设置thymeleaf 缓存
spring.thymeleaf.cache=false
#配置数据源
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/security?characterEncoding=UTF-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root

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

#日志处理  为了展示mysql运行 sql语句
logging.level.com.zkt=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() {

        Set<SimpleGrantedAuthority> authorities = new HashSet<>();
        roles.forEach(role->{
            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getName());
            authorities.add(simpleGrantedAuthority);

        });

        return authorities;
    }

    @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;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public void setEnabled(Boolean enabled) {
        this.enabled = enabled;
    }

    public void setAccountNonExpired(Boolean accountNonExpired) {
        this.accountNonExpired = accountNonExpired;
    }

    public void setAccountNonLocked(Boolean accountNonLocked) {
        this.accountNonLocked = accountNonLocked;
    }

    public void setCredentialsNonExpired(Boolean credentialsNonExpired) {
        this.credentialsNonExpired = credentialsNonExpired;
    }

    public void setRoles(List<Role> roles) {
        this.roles = roles;
    }

    public Integer getId() {
        return id;
    }

    public List<Role> getRoles() {
        return roles;
    }
}

role对象

public class Role {
    private Integer id;
    private String name;
    private String nameZh;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getNameZh() {
        return nameZh;
    }

    public void setNameZh(String nameZh) {
        this.nameZh = nameZh;
    }
}

编写Dao类

@Mapper
public interface UserDao {

    //提供根据用户名返回用户方法
    User loadUserByUsername(String username);

    //提供根据用户id查询用户角色信息方法
    List<Role> getRolesByUid(Integer uid);

}

mapper

<?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.zkt.dao.UserDao">
    <!--根据用户名查询用户方法-->
    <!--查询单个-->
    <select id="loadUserByUsername" resultType="User">
        select id,username,password,enabled,accountNonExpired,accountNonLocked,credentialsNonExpired
        from user
        where username = #{username}
    </select>

<!--    根据用户id 查询角色信息-->
    <select id="getRolesByUid" resultType="Role">
        select r.id,r.name,r.name_zh from role r, user_role ur
        where r.id = ur.rid
        and ur.rid = #{uid}
    </select>
</mapper>

自定义UserDetailsService

@Component
public class MyUserDetailService implements UserDetailsService {


    private final UserDao userDao;
    //dao ==>springboot+mybatis
    @Autowired
    public MyUserDetailService(UserDao userDao){
        this.userDao = userDao;
    }


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//        1.查询用户
        User user = userDao.loadUserByUsername(username);
        if(ObjectUtils.isEmpty(user)) throw new UsernameNotFoundException("用户名不正确~");
//        2.查询权限信息
        List<Role> roles = userDao.getRolesByUid(user.getId());
        user.setRoles(roles);
        return user;
    }
}

配置authenticationManager使用自定义UserDetailService

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

    private final MyUserDetailService myUserDetailService;

    @Autowired
    public WebSecurityConfigurer(MyUserDetailService myUserDetailService) {
        this.myUserDetailService = myUserDetailService;
    }


//    自定义AuthenticationManager  并没有在工厂中暴露出来
    @Override
    public void configure(AuthenticationManagerBuilder builder) throws Exception {
        System.out.println("自定义AuthenticationManager:" + builder);
//        InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
//        userDetailsService.createUser(User.withUsername("zkt").password("{noop}root").roles("admin").build());
        builder.userDetailsService(myUserDetailService);
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
		//...
    }
}

测试

使用账号 root 密码123进行登录 登录成功。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值