1.1.认证授权流程
SpringSecurity是基于Filter实现认证和授权,底层通过FilterChainProxy代理去调用各种Filter(Filter链),Filter通过调用AuthenticationManager完成认证 ,通过调用AccessDecisionManager完成授权。流程如下图:
1.2.Security过滤器链
我们知道,SpringSecurity是通过很多的过滤器链共同协作完成认证,授权的流程,SpringSecurity中核心的过滤器链如下:
1.SecurityContextPersistenceFilter 这个filter是整个filter链的入口和出口,请求开始会从SecurityContextRepository中 获取SecurityContext对象并设置给SecurityContextHolder。在请求完成后将 SecurityContextHolder持有的SecurityContext再保存到配置好的 SecurityContextRepository中,同时清除SecurityContextHolder中的SecurityContext
2.UsernamePasswordAuthenticationFilter 默认拦截“/login”登录请求,处理表单提交的登录认证,将请求中的认证信息包括 username,password等封装成UsernamePasswordAuthenticationToken,然后调用 AuthenticationManager的认证方法进行认证。 BasicAuthenticationFilter 基本认证,httpBasic登录,弹出登录框登录
3.RememberAuthenticationFilter 记住我
4.AnonymousAuthenticationFilter 匿名Filter,用来处理匿名访问的资源,如果SecurityContext中没有Authentication, 就会创建匿名的Token(AnonymousAuthenticationToken),然后通过 SecurityContextHodler设置到SecurityContext中。
5.ExceptionTranslationFilter 用来捕获FilterChain所有的异常,进行处理,但是只会处理 AuthenticationException和AccessDeniedException,异常,其他的异常 会继续抛出。
6.FilterSecurityInterceptor 用来做授权的Filter,通过父类(AbstractSecurityInterceptor.beforeInvocation)调用 AccessDecisionManager.decide方法对用户进行授权。 1.3.Security相关概念
7.AuthenticationToken 所有提交给AuthenticationManager的认证请求都会被封装成一个Token的实现,比如 最容易理解的UsernamePasswordAuthenticationToken,其中包含了用户名和密码。
8.AuthenticationManager 用户认证的管理类,所有的认证请求(比如login)都会通过提交一个token给 AuthenticationManager的authenticate()方法来实现认证。AuthenticationManager会 调 用AuthenticationProvider.authenticate进行认证。认证成功后,返回一个包含了认 证 信息的Authentication对象。
9.AuthenticationProvider.authenticate 认证的具体实现类,一个provider是一种认证方式的实现,比如提交的用户名密码我 是通过和DB中查出的user记录做比对实现的,那就有一个DaoProvider;如果我是通 过CAS请求单点登录系统实现,那就有一个CASProvider。按照Spring一贯的作风, 主流的认证方式它都已经提供了默认实现,比如DAO、LDAP、CAS、OAuth2等。 前 面讲了AuthenticationManager只是一个代理接口,真正的认证就是由 AuthenticationProvider来做的。一个AuthenticationManager可以包含多个Provider, 每个provider通过实现一个support方法来表示自己支持那种Token的认证。 AuthenticationManager默认的实现类是ProviderManager。
10.UserDetailService 用户的认证通过Provider来完成,而Provider会通过UserDetailService拿到数据库(或 内存)中的认证信息然后和客户端提交的认证信息做校验。虽然叫Service,但是我更愿 意把它认为是我们系统里经常有的UserDao。
11.SecurityContext 当用户通过认证之后,就会为这个用户生成一个唯一的SecurityContext,里面包含用 户的认证信息Authentication。通过SecurityContext我们可以获取到用户的标识 Principle和授权信息GrantedAuthrity。在系统的任何地方只要通过 SecurityHolder.getSecruityContext()就可以获取到SecurityContext。在Shiro中通过 SecurityUtils.getSubject()到达同样的目的
1.3.SpringSecurity认证流程原理
1.请求过来会被过滤器链中的UsernamePasswordAuthenticationFilter拦截到,请求中的用户名和密码被封装成UsernamePasswordAuthenticationToken(Authentication的实现类)
2.过滤器将UsernamePasswordAuthenticationToken提交给认证管理器(AuthenticationManager)进行认证.
3.AuthenticationManager委托AuthenticationProvider(DaoAuthenticationProvider)进行认证,AuthenticationProvider通过调用UserDetailsService获取到数据库中存储的用户信息(UserDetails),然后调用passwordEncoder密码编码器对UsernamePasswordAuthenticationToken中的密码和UserDetails中的密码进行比较
4.AuthenticationProvider认证成功后封装Authentication并设置好用户的信息(用户名,密码,权限等)返回
5.Authentication被返回到UsernamePasswordAuthenticationFilter,通过调用SecurityContextHolder工具把Authentication封装成SecurityContext中存储起来。然后UsernamePasswordAuthenticationFilter调用AuthenticationSuccessHandler.onAuthenticationSuccess做认证成功后续处理操作
6.最后SecurityContextPersistenceFilter通过SecurityContextHolder.getContext()获取到SecurityContext对象然后调用SecurityContextRepository将SecurityContext存储起来,然后调用SecurityContextHolder.clearContext方法清理SecurityContext。 注意:SecurityContext是一个和当前线程绑定的工具,在代码的任何地方都可以通过SecurityContextHolder.getContext()获取到登陆信息。
在SpringSecurity的整个认证流程中,除了UserDetailsService需要我们自己定义外,其他的的组件都可以使用默认的,因为UserDetailsService是SpringSecurity获取数据库中的认证信息的媒介,而如何才能从数据库中获取认证信息只有我们才知道。在入门案例中我们使用的是InMemoryUserDetailsManager 基于内存的UserDetailsService方案,接下来我们需要把基于内存的方案修改为基于数据库的方案。 2.1.定义UserDetailsService
相关概念 1.UserDetailsService 是SpringSecurity提供用来获取认证用户信息(用户名,密码,用户的权限列表)的 接 口,我们可以实现该接口,复写loadUserByUsername(username) 方法加载我们数 据 库中的用户信息
2.UserDetails UserDetails是SpringSecurity用来封装用户认证信息,权限信息的对象,我们使用它 的实现类User封装用户信息 并返回,我们这里从数据库查询用户名 基于入门案例进行修改
3.准备好认证表t_login(密码密文) ,集成MyBatis等,做好准备工作
4.创建类UserDetailServiceImpl实现UserDetailsService接口
/** * 用来提供给security的用户信息的service, * 我们需要复写 loadUserByUsername 方法返回数据库中的用户信息 */ @Service public class UserDetailServiceImpl implements UserDetailsService { /** * 加载数据库中的认证的用户的信息:用户名,密码,用户的权限列表 * @param username: 该方法把username传入进来,我们通过username查询用户的信息 (密码,权限列表等)然后封装成 UserDetails进行返回 ,交给security 。 */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { com.qx680.domain.User userFromMysql = userMapper.selectByUsername(username); if(loginFromMysql == null){ throw new UsernameNotFoundException("无效的用户名"); } List<GrantedAuthority> permissions = new ArrayList<>(); //密码是基于BCryptPasswordEncoder加密的密文 //User是security内部的对象,UserDetails的实现类 , //用来封装用户的基本信息(用户名,密码,权限列表) return new User(username,loginFromMysql.getPassword(),permissions); } }Provider会调用UserDetailsService 获取认证信息,这里自定义的UserDetailsService实现类,复写了loadUserByUsername方法,根据用户名查询数据库中的认证信息和当前用户的权限信息,封装成User返回。 注意:这里定义了UserDetailSerice后,WebSecurityConfig中不在需要定义UserDetailService的Bean需要移除
2.2.MyBatis集成
1.导入依赖
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.20</version> </dependency> <!-- mysql 数据库驱动. --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.1.1</version> </dependency>2.配置MyBatis
spring: datasource: url: jdbc:mysql:///auth-rbac username: root password: admin driver-class-name: com.mysql.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource mybatis: mapper-locations: classpath:com/qx680/mapper/*Mapper.xml @SpringBootApplication @MapperScan("com.qx680.mapper") public class ApplicationConfig { public static void main(String[] args) { SpringApplication.run(ApplicationConfig.class); } }2.编写Mapper.xml
3.编写Mapper映射器接口
4.创建数据库 :sql见资料:auth-rbac.sql
/* Navicat MySQL Data Transfer Source Server : localhost_3306 Source Server Version : 50720 Source Host : localhost:3306 Source Database : auth-security Target Server Type : MYSQL Target Server Version : 50720 File Encoding : 65001 Date: 2020-09-20 10:50:51 */ SET FOREIGN_KEY_CHECKS=0; -- ---------------------------- -- Table structure for t_my_user -- ---------------------------- DROP TABLE IF EXISTS `t_my_user`; CREATE TABLE `t_my_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(255) DEFAULT NULL COMMENT '员工用户名', `password` varchar(255) DEFAULT NULL COMMENT '密码', `enabled` bit(1) DEFAULT b'1', `account_non_expired` bit(1) DEFAULT b'1', `credentials_non_expired` bit(1) DEFAULT b'1', `account_non_locked` bit(1) DEFAULT b'1', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of t_my_user -- ---------------------------- INSERT INTO `t_my_user` VALUES ('1', 'zs', '$2a$10$qtAMxPqJE54nb808csYmzO6pyUDUufCbgjjza34md5AJ5.fVz7N4y', '', '', '', ''); INSERT INTO `t_my_user` VALUES ('2', 'ls', '$2a$10$qtAMxPqJE54nb808csYmzO6pyUDUufCbgjjza34md5AJ5.fVz7N4y', '', '', '', ''); -- ---------------------------- -- Table structure for t_permission -- ---------------------------- DROP TABLE IF EXISTS `t_permission`; CREATE TABLE `t_permission` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `resource` varchar(255) NOT NULL, `expression` varchar(255) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of t_permission -- ---------------------------- INSERT INTO `t_permission` VALUES ('1', '员工查询', '/employee/list', 'employee:list'); INSERT INTO `t_permission` VALUES ('2', '员工添加', '/employee/add', 'employee:add'); INSERT INTO `t_permission` VALUES ('3', '员工修改', '/employee/update', 'employee:update'); INSERT INTO `t_permission` VALUES ('4', '员工删除', '/employee/delete', 'employee:delete'); INSERT INTO `t_permission` VALUES ('5', '部门查询', '/dept/list', 'dept:list'); INSERT INTO `t_permission` VALUES ('6', '部门添加', '/dept/add', 'dept:add'); INSERT INTO `t_permission` VALUES ('7', '部门修改', '/dept/update', 'dept:update'); INSERT INTO `t_permission` VALUES ('8', '部门删除', '/dept/delete', 'dept:delete'); -- ---------------------------- -- Table structure for t_role -- ---------------------------- DROP TABLE IF EXISTS `t_role`; CREATE TABLE `t_role` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `sn` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of t_role -- ---------------------------- INSERT INTO `t_role` VALUES ('1', '人事', 'HR'); INSERT INTO `t_role` VALUES ('2', '部门经理', 'DM'); -- ---------------------------- -- Table structure for t_role_permission -- ---------------------------- DROP TABLE IF EXISTS `t_role_permission`; CREATE TABLE `t_role_permission` ( `role_id` bigint(20) NOT NULL, `permission_id` bigint(20) NOT NULL, PRIMARY KEY (`role_id`,`permission_id`), KEY `permission_id` (`permission_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of t_role_permission -- ---------------------------- INSERT INTO `t_role_permission` VALUES ('1', '1'); INSERT INTO `t_role_permission` VALUES ('1', '2'); INSERT INTO `t_role_permission` VALUES ('1', '3'); INSERT INTO `t_role_permission` VALUES ('1', '4'); INSERT INTO `t_role_permission` VALUES ('2', '5'); INSERT INTO `t_role_permission` VALUES ('2', '6'); INSERT INTO `t_role_permission` VALUES ('2', '7'); INSERT INTO `t_role_permission` VALUES ('2', '8'); -- ---------------------------- -- Table structure for t_user_role -- ---------------------------- DROP TABLE IF EXISTS `t_user_role`; CREATE TABLE `t_user_role` ( `user_id` bigint(20) NOT NULL, `role_id` bigint(20) NOT NULL, PRIMARY KEY (`user_id`,`role_id`), KEY `role_id` (`role_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of t_user_role -- ---------------------------- INSERT INTO `t_user_role` VALUES ('1', '1'); INSERT INTO `t_user_role` VALUES ('2', '2');2.3.定义密码编码器 在我们的案例中,密码一值是明文的,我们指定的密码编码器是 NoOpPasswordEncoder ,这个是不加密的,但是在生产环境中我们数据库中的密码肯定是密文,所以我们需要指定密码的编码器,那么SpringSecurity在认证时会调用我们指定的密码编码器进行认证 BCryptPasswordEncoder BCryptPasswordEncoder是SpringSecurity内部提供的编码器,他的好处在于多次对相 同的明文加密出来的密文是不一致的,但是多次加密出来的不同密文确有能检查通过, 这种方式增加了密码的安全性,测试代码如下:
public class PasswordTest { @Test public void testPassword(){ BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); String enPass = bCryptPasswordEncoder.encode("123"); System.out.println(enPass); System.out.println(bCryptPasswordEncoder.matches("123", enPass)); } }2.4 在配置类中定义编码器如下:
@Bean public PasswordEncoder passwordEncoder(){ //return NoOpPasswordEncoder.getInstance(); return new BCryptPasswordEncoder(); }重启测试
2.4.自定义登录页面
2.5准备登录页面static/login.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>登陆</title> </head> <body> <h1>登陆</h1> <form method="post" action="/login"> <div> 用户名:<input type="text" name="username"> </div> <div> 密码:<input type="password" name="password"> </div> <div> <button type="submit">立即登陆</button> </div> </form> </body> </html>2.6配置登录页面
在WebSecurityConfig配置类中配置登陆页面和登陆请求地址
protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/login").permitAll() //登录路径放行 .antMatchers("/login.html").permitAll() //对登录页面跳转路径放行 .anyRequest().authenticated() //其他路径都要拦截 .and().formLogin() //允许表单登录, 设置登陆页 .successForwardUrl("/loginSuccess") // 设置登陆成功页 .loginPage("/login.html") //登录页面跳转地址 .loginProcessingUrl("/login") //登录处理地址(必须) .and().logout().permitAll(); //登出 }//方法解释
1.http.csrf().disable() :屏蔽跨域伪造检查 2.antMatchers("/login.html").permitAll() : 对登录页面跳转路径放行 3.loginPage("/login.html") :登录页面跳转地址(必须) 4.loginProcessingUrl("/login") :登录处理地址(必须)
2.5.自定义登出
SpringSecurity提供了默认的退出处理,可以在Security配置类中通过.and().logout().permitAll(); 使用默认的退出路径“/logout” ,如果我们需要自定义退出路径,可以通过如下方式指定:
.and().logout().logoutUrl("/mylogout").permitAll() //制定义登出路径 .logoutSuccessHandler(new MyLogoutHandler()) //登出后处理器-可以做一些额外的事情 .invalidateHttpSession(true); //登出后session无效