# Spring-SpringSecurity **Repository Path**: zhang-purui/spring-spring-security ## Basic Information - **Project Name**: Spring-SpringSecurity - **Description**: Spring 整合SpringSecurity - **Primary Language**: Java - **License**: MulanPSL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2021-08-25 - **Last Updated**: 2022-12-05 ## Categories & Tags **Categories**: Uncategorized **Tags**: Spring, SpringSecurity ## README # Spring 整合SpringSecurity ## 0.背景 ​ 很多东西使用都会,但是知识点都比较零散,通过查阅资料,学习 总结下知识点。 此 案例 总结了常用到的 SpringSecurity 的知识点。 ​ ## 1.搭建 Demo ​ Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。相关文档 🚪=》https://docs.spring.io/spring-security/site/docs/current/reference/html5/ ### 1.1 maven 依赖 pom.xml ```xml 4.0.0 com.itheima spring.spring.security 1.0-SNAPSHOT org.apache.maven.plugins maven-compiler-plugin 6 6 war org.springframework.security spring-security-taglibs 5.5.2 org.springframework.security spring-security-config 5.5.2 org.springframework spring-web org.springframework spring-jdbc 5.1.6.RELEASE org.slf4j slf4j-log4j12 1.7.26 org.springframework spring-webmvc 5.1.6.RELEASE mysql mysql-connector-java 5.1.47 org.mybatis mybatis 3.5.1 org.mybatis mybatis-spring 2.0.1 javax.servlet javax.servlet-api 4.0.1 provided javax.servlet jsp-api 2.0 provided jstl jstl 1.2 org.projectlombok lombok 1.18.8 javax.annotation jsr250-api 1.0 ``` ### 1.2 learing_security.sql ```mysql /* Navicat Premium Data Transfer Source Server : local_maria Source Server Type : MariaDB Source Server Version : 100604 Source Host : localhost:3306 Source Schema : learing_security Target Server Type : MariaDB Target Server Version : 100604 File Encoding : 65001 Date: 30/08/2021 22:10:44 */ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for persistent_logins -- ---------------------------- DROP TABLE IF EXISTS `persistent_logins`; CREATE TABLE `persistent_logins` ( `username` varchar(64) NOT NULL, `series` varchar(64) NOT NULL, `token` varchar(64) NOT NULL, `last_used` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), PRIMARY KEY (`series`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3; -- ---------------------------- -- Records of persistent_logins -- ---------------------------- BEGIN; COMMIT; -- ---------------------------- -- Table structure for sys_permission -- ---------------------------- DROP TABLE IF EXISTS `sys_permission`; CREATE TABLE `sys_permission` ( `ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号', `permission_NAME` varchar(30) DEFAULT NULL COMMENT '菜单名称', `permission_url` varchar(100) DEFAULT NULL COMMENT '菜单地址', `parent_id` int(11) NOT NULL DEFAULT 0 COMMENT '父菜单id', PRIMARY KEY (`ID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3; -- ---------------------------- -- Records of sys_permission -- ---------------------------- BEGIN; COMMIT; -- ---------------------------- -- Table structure for sys_role -- ---------------------------- DROP TABLE IF EXISTS `sys_role`; CREATE TABLE `sys_role` ( `ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号', `ROLE_NAME` varchar(30) DEFAULT NULL COMMENT '角色名称', `ROLE_DESC` varchar(60) DEFAULT NULL COMMENT '角色描述', PRIMARY KEY (`ID`) ) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb3; -- ---------------------------- -- Records of sys_role -- ---------------------------- BEGIN; INSERT INTO `sys_role` VALUES (1, 'ROLE_USER', '测试角色'); INSERT INTO `sys_role` VALUES (6, 'ROLE_ADMIN', '超级管理员'); INSERT INTO `sys_role` VALUES (7, 'ROLE_PRODUCT', '产品'); INSERT INTO `sys_role` VALUES (8, 'ROLE_ORDER', '订单'); COMMIT; -- ---------------------------- -- Table structure for sys_role_permission -- ---------------------------- DROP TABLE IF EXISTS `sys_role_permission`; CREATE TABLE `sys_role_permission` ( `RID` int(11) NOT NULL COMMENT '角色编号', `PID` int(11) NOT NULL COMMENT '权限编号', PRIMARY KEY (`RID`,`PID`), KEY `FK_Reference_12` (`PID`), CONSTRAINT `FK_Reference_11` FOREIGN KEY (`RID`) REFERENCES `sys_role` (`ID`), CONSTRAINT `FK_Reference_12` FOREIGN KEY (`PID`) REFERENCES `sys_permission` (`ID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3; -- ---------------------------- -- Records of sys_role_permission -- ---------------------------- BEGIN; COMMIT; -- ---------------------------- -- Table structure for sys_user -- ---------------------------- DROP TABLE IF EXISTS `sys_user`; CREATE TABLE `sys_user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(32) NOT NULL COMMENT '用户名称', `password` varchar(120) NOT NULL COMMENT '密码', `status` int(1) DEFAULT 1 COMMENT '1开启0关闭', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3; -- ---------------------------- -- Records of sys_user -- ---------------------------- BEGIN; INSERT INTO `sys_user` VALUES (1, 'test', '$2a$10$0AFPibMeFbsCdufbt6ZDweaCAebm8VY1CPCLJJKGmfrIjzX2NM4L.', 1); INSERT INTO `sys_user` VALUES (2, 'zpr', '$2a$10$0AFPibMeFbsCdufbt6ZDweaCAebm8VY1CPCLJJKGmfrIjzX2NM4L.', 0); COMMIT; -- ---------------------------- -- Table structure for sys_user_role -- ---------------------------- DROP TABLE IF EXISTS `sys_user_role`; CREATE TABLE `sys_user_role` ( `UID` int(11) NOT NULL COMMENT '用户编号', `RID` int(11) NOT NULL COMMENT '角色编号', PRIMARY KEY (`UID`,`RID`), KEY `FK_Reference_10` (`RID`), CONSTRAINT `FK_Reference_10` FOREIGN KEY (`RID`) REFERENCES `sys_role` (`ID`), CONSTRAINT `FK_Reference_9` FOREIGN KEY (`UID`) REFERENCES `sys_user` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3; -- ---------------------------- -- Records of sys_user_role -- ---------------------------- BEGIN; INSERT INTO `sys_user_role` VALUES (1, 6); COMMIT; SET FOREIGN_KEY_CHECKS = 1; ``` ### 1.3 配置 spring 容器 ```xml ``` ### 1.4 配置 spring-mvc ```xml ``` ### 1.5 配置 spring-security ```xml ``` ### 1.6 web.xml ```xml Archetype Created Web Application encodingFilter org.springframework.web.filter.CharacterEncodingFilter encoding UTF-8 encodingFilter /* springmvc org.springframework.web.servlet.DispatcherServlet contextConfigLocation classpath:spring-mvc.xml springmvc / org.springframework.web.context.ContextLoaderListener contextConfigLocation classpath:applicationContext.xml springSecurityFilterChain org.springframework.web.filter.DelegatingFilterProxy springSecurityFilterChain /* ``` ### 1.7 项目结构截图 ![image-20210831005149278](https://file-zpr.oss-cn-shanghai.aliyuncs.com/blog/image-20210831005149278.png) ### 1.8 结果展示 ​ 登陆页面,实现 SpringSecurity 的方式登陆并实现 **记住我** 功能 ![image-20210831005826651](https://file-zpr.oss-cn-shanghai.aliyuncs.com/blog/image-20210831005826651.png) ​ 权限不足的判断,并自定义了 403 页面。 通过 security 提供的taglibs 标签对 菜单进行显示控制。 post 表单 提供 csrf 跨站请求伪造 防范。 ![image-20210831005942505](https://file-zpr.oss-cn-shanghai.aliyuncs.com/blog/image-20210831005942505.png) ## 2.相关知识点 ### 2.1 SpringSecurity 依赖模块 ​ 简单了解下 SpringSecurity 有哪些模块。 1. **spring-security-core** - 本模块包含核心身份验证和访问控制类和接口、远程支持和基本预置API。它是任何使用Spring Security的应用程序所必需的。它支持独立应用程序、远程客户端、方法(服务层)安全性和JDBC用户预置。 2. spring-security-remoting - 本模块提供与Spring Remoting的集成。除非您正在编写使用Spring Remoting的远程客户端,否则您不需要此客户端。 3. **spring-security-web** - 如果您需要Spring Security Web身份验证服务和基于URL的访问控制,则需要它。 4. **spring-security-config** - 本模块包含安全命名空间解析代码和Java配置代码。如果您使用Spring Security XML命名空间进行配置或Spring Security的Java配置支持,则需要它。 5. spring-security-ldap - 本模块提供LDAP身份验证和预置代码。如果您需要使用LDAP身份验证或管理LDAP用户条目,这是必需的。 6. spring-security-oAuth2-core - 包含为OAuth 2.0授权框架和OpenID Connect Core 1.0提供支持的核心类和接口。它是使用OAuth 2.0或OpenID Connect Core 1.0的应用程序所必需的,例如客户端、资源服务器和授权服务器。 7. spring-security-oAuth2-client - 包含Spring Security对OAuth 2.0授权框架和OpenID Connect Core 1.0的客户端支持。它是使用OAuth 2.0登录或OAuth客户端支持的应用程序所必需的。 8. spring-security-oAuth2-jose - 包含Spring Security对JOSE(Javascript对象签名和加密)框架的支持。JOSE框架旨在为当事人之间安全转移索赔提供一种方法。 9. spring-security-oAuth2-resource-server - 包含Spring Security对OAuth 2.0资源服务器的支持。它用于通过OAuth 2.0承载令牌保护应用编程接口。 10. spring-security-acl - 本模块包含一个专门的域对象ACL实现。它用于将安全性应用于应用程序中的特定域对象实例。 11. spring-security-cas - 本模块包含Spring Security的CAS客户端集成。如果您想将 Spring Security Web 身份验证与 CAS 单点登录服务器一起使用,则应使用它。 12. spring-security-openid - 本模块包含OpenID Web身份验证支持。它用于针对外部OpenID服务器验证用户。 13. spring-security-test - 本模块支持使用Spring Security进行测试。 14. **spring-security-taglibs** - 提供Spring Security的JSP标签实现。 ### 2.2 过滤链 ​ SpringSecurity 的安全🔐控制是通过 Servlet Filter 来实现。 client 的请求 经过一层一层的 filter 最终达到 处理请求的 Servlet ![过滤器链](https://file-zpr.oss-cn-shanghai.aliyuncs.com/blog/filterchain.png) ![image-20210831114117031](https://file-zpr.oss-cn-shanghai.aliyuncs.com/blog/image-20210831114117031.png) 在 web.xml 中配置了 DelegatingFilterProxy 这是一个Spring Web 提供的 过滤器委托代理,我们在 filter-name 中指定了 被代理的 bean name 是 springSecurityFilterChain ,这是固定写法,不能写错。 ![image-20210831114450005](https://file-zpr.oss-cn-shanghai.aliyuncs.com/blog/image-20210831114450005.png) ![截屏2021-08-31 上午11.48.44](https://file-zpr.oss-cn-shanghai.aliyuncs.com/blog/%E6%88%AA%E5%B1%8F2021-08-31%20%E4%B8%8A%E5%8D%8811.48.44.png) ​ initFilterBean 的初始化Filter Bean方法会把 web.xml 配置的 bean name 拿到,并根据它从WebApplicationContext 上下文中获取对应的 bean FilterChainProxy ```java public class FilterChainProxy extends GenericFilterBean { private static final Log logger = LogFactory.getLog(FilterChainProxy.class); //标记过滤器是否已经执行过 private static final String FILTER_APPLIED = FilterChainProxy.class.getName().concat(".APPLIED"); //注意⚠️看这里 过滤器链 可以有多个 private List filterChains; //省略了 不关心的代码 //进行内部过滤 private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { //根据请求获取对应 过滤链中的 filter 列表 List filters = this.getFilters((HttpServletRequest)fwRequest); if (filters != null && filters.size() != 0) { //创建一个虚拟的过滤器链 FilterChainProxy.VirtualFilterChain vfc = new FilterChainProxy.VirtualFilterChain(fwRequest, chain, filters); vfc.doFilter(fwRequest, fwResponse); } else { //这里是 没有获取到对应过滤器的情况,直接放行,比如我们的静态资源,在配置中配置了放行,就不会有filter 拦截 if (logger.isDebugEnabled()) { logger.debug(UrlUtils.buildRequestUrl(fwRequest) + (filters == null ? " has no matching filters" : " has an empty filter list")); } fwRequest.reset(); chain.doFilter(fwRequest, fwResponse); } } } ``` ​ 下面可以看到获取到springSecurity 提供的默认的 过滤器列表,对filers 做了一层封装 是 FilterChainProxy 的子类 VirtualFilterChain 然后执行 doFilter 方法 。 ps: 默认应该有15个,因为我把 csrf 关闭了 ![截屏2021-08-31 上午11.57.10](https://file-zpr.oss-cn-shanghai.aliyuncs.com/blog/%E6%88%AA%E5%B1%8F2021-08-31%20%E4%B8%8A%E5%8D%8811.57.10.png) ```java private static class VirtualFilterChain implements FilterChain { //原生的过滤器链 private final FilterChain originalChain; //过滤器列表 private final List additionalFilters; private final FirewalledRequest firewalledRequest; //过滤器集合 size private final int size; //过滤器集合执行到什么位置了,主要用于记录位置 private int currentPosition; //上面通过此构造 把 过滤器链和 默认的过滤器传入。 private VirtualFilterChain(FirewalledRequest firewalledRequest, FilterChain chain, List additionalFilters) { this.currentPosition = 0; this.originalChain = chain; this.additionalFilters = additionalFilters; //默认 肯定不为0 this.size = additionalFilters.size(); this.firewalledRequest = firewalledRequest; } public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { if (this.currentPosition == this.size) { //表示过滤器链已经执行完成了 if (FilterChainProxy.logger.isDebugEnabled()) { FilterChainProxy.logger.debug(UrlUtils.buildRequestUrl(this.firewalledRequest) + " reached end of additional filter chain; proceeding with original chain"); } this.firewalledRequest.reset(); //调用originalChain.doFilter 进入原生过滤链 this.originalChain.doFilter(request, response); } else { ++this.currentPosition; Filter nextFilter = (Filter)this.additionalFilters.get(this.currentPosition - 1); if (FilterChainProxy.logger.isDebugEnabled()) { FilterChainProxy.logger.debug(UrlUtils.buildRequestUrl(this.firewalledRequest) + " at position " + this.currentPosition + " of " + this.size + " in additional filter chain; firing Filter: '" + nextFilter.getClass().getSimpleName() + "'"); } //执行当前过滤器,这里,因为当前 VirtualFilterChain 实现了 FilterChain //这里又指定了 过滤链 是 this,所以 当前的 filter list 会递归进入此方法。 nextFilter.doFilter(request, response, this); } } } ``` 梳理一下流程: 1. web.xml 配置 DelegatingFilterProxy 并指定 代理的 bean name 为 springSecurityFilterChain 2. 打断点可得-----通过 第一步的bean name 得到 FilterChainProxy 过滤器链代理 3. 执行 FilterChainProxy 的 doFilterInternal 方法 根据请求获得 相关的 Filter 4. 传入获得的 Filter list 使用 FilteChainProxy 的内部类 VirtualFilterChain 的 doFilter 执行真正的 filter 的 doFilter 操作。 ps:过滤器链可以有多个。 ### 2.3 认证流程 ​ SpringSecurity 的表单登陆 是基于 UsernamePasswordAuthencationFilter 完成。 ```java public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { //指定前端传参 public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username"; public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password"; private String usernameParameter = "username"; private String passwordParameter = "password"; private boolean postOnly = true; public UsernamePasswordAuthenticationFilter() { //指定 登陆请求 地址和请求方式 super(new AntPathRequestMatcher("/login", "POST")); } public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { String username = this.obtainUsername(request); String password = this.obtainPassword(request); //删除多余代码 //把前端传的账号密码 封装成 UsernamePasswordAuthenticationToken UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); this.setDetails(request, authRequest); //获取认证管理器,调用认证方法 return this.getAuthenticationManager().authenticate(authRequest); } } ``` ​ ```java public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean { private static final Log logger = LogFactory.getLog(ProviderManager.class); //提供 认证的 提供商 private List providers; public Authentication authenticate(Authentication authentication) throws AuthenticationException { Class toTest = authentication.getClass(); AuthenticationException lastException = null; AuthenticationException parentException = null; Authentication result = null; Authentication parentResult = null; int currentPosition = 0; int size = this.providers.size(); Iterator var9 = this.getProviders().iterator(); while(var9.hasNext()) { AuthenticationProvider provider = (AuthenticationProvider)var9.next(); //根据封装的 authentication 类型来找到对应的 认证提供商 //这里我们封装的 authentication 是 UsernamePasswordAuthcationFilter if (provider.supports(toTest)) { try { result = provider.authenticate(authentication); } catch (InternalAuthenticationServiceException | AccountStatusException var14) { this.prepareException(var14, authentication); throw var14; } catch (AuthenticationException var15) { lastException = var15; } } } //删除了多余的代码 return result; } ``` ​ 认证提供商是 AbstractUserDetailsAuthenticationProvider ```java public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware { //认证方法 public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> { return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported"); }); //调用 retrieveUser 方法认证,但 当前类 并没有实现 此方法,默认的实现类是 DaoAuthenticationProvider user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication); return this.createSuccessAuthentication(principalToReturn, authentication, user); } } ``` ​ 实际上调用的是 DaoAuthenticationProvider 的 retrieveUser 方法实现认证 ```java public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword"; private PasswordEncoder passwordEncoder; private volatile String userNotFoundEncodedPassword; private UserDetailsService userDetailsService; private UserDetailsPasswordService userDetailsPasswordService; protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { this.prepareTimingAttackProtection(); try { //看到 loadUserByUsername 如果用过 SpringSecurity 的朋友 就很熟悉了 //这一步是要把 我们自己的用户系统接入 SpringSecurity,所有我们需要去 实现 UserDetailsService 并实现 //loadUserByUsername(username) 方法 UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); if (loadedUser == null) { throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation"); } else { return loadedUser; } } catch (UsernameNotFoundException var4) { this.mitigateAgainstTimingAttack(authentication); throw var4; } catch (InternalAuthenticationServiceException var5) { throw var5; } catch (Exception var6) { throw new InternalAuthenticationServiceException(var6.getMessage(), var6); } } } ``` ```java @Service@Transactionalpublic class UserServiceImpl implements UserService { @Autowired private UserDao userDao; public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser sysUser = userDao.findByName(username); if(null == sysUser){ //返回空,表示认证失败。 return null; } List authorities = new ArrayList(); //动态获取权限 List roles = sysUser.getRoles(); for (SysRole role : roles) { authorities.add(new SimpleGrantedAuthority(role.getRoleName())); } authorities.add(new SimpleGrantedAuthority("ROLE_USER")); //{noop} 表示密码是明文,加了 passwordEncoder 就不需要了// return new User(sysUser.getUsername(),"{noop}"+sysUser.getPassword(),authorities); //enable:账号是否启用,这里使用 enable 参数判断账号是否启用 //accountNonExpired:账号没有过期? //credentialsNonExpired:密码没有过期? //accountNonLocked:账号没有锁定? //需要 把自定义的 SysUser 转换成 SpringSecurity 的 User 对象 return new User(sysUser.getUsername(), sysUser.getPassword(), sysUser.getStatus()==1, true, true, true, authorities); }} ``` ​ ok 到这一步,已经实现了根据 传入的用户名查询出 用户信息,下面看看 如何对返回的用户信息处理 ```java public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware { public Authentication authenticate(Authentication authentication) throws AuthenticationException { String username = this.determineUsername(authentication); boolean cacheWasUsed = true; UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { //这里返回用户信息 user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication); } catch (UsernameNotFoundException var6) { } } try { //这里对 用户是否锁定/是否账号过期/是否密码过期 等进行检查 this.preAuthenticationChecks.check(user); //此方法 对比 密码 是否正确 this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication); } catch (AuthenticationException var7) { } this.postAuthenticationChecks.check(user); if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (this.forcePrincipalAsString) { principalToReturn = user.getUsername(); } //此方法会把 认证完的用户信息 重新 封装成 一个 UsernamePasswordAuthenticationToken 对象 返回 return this.createSuccessAuthentication(principalToReturn, authentication, user); } //封装 UsernamePasswordAuthenticationToken 对象 protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) { UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities())); result.setDetails(authentication.getDetails()); this.logger.debug("Authenticated user"); return result; } } ``` ​ ​ 在返回到 ProviderManager 最终 在 AbstractAuthenticationProcessingFilter 的下面的方法,做 成功响应 和 失败响应。 ```java private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { if (!requiresAuthentication(request, response)) { chain.doFilter(request, response); return; } try { //处理返回 重新封装回来的 UsernamePasswordAutehcationFilter Authentication authenticationResult = attemptAuthentication(request, response); if (authenticationResult == null) { // return immediately as subclass has indicated that it hasn't completed return; } this.sessionStrategy.onAuthentication(authenticationResult, request, response); // Authentication success if (this.continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } // 认证成功响应 successfulAuthentication(request, response, chain, authenticationResult); } catch (InternalAuthenticationServiceException failed) { this.logger.error("An internal error occurred while trying to authenticate the user.", failed); //认证失败响应 unsuccessfulAuthentication(request, response, failed); } catch (AuthenticationException ex) { //认证失败响应 unsuccessfulAuthentication(request, response, ex); } } protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { //成功 把 用户信息 放入 Security 的上下文中 SecurityContextHolder.getContext().setAuthentication(authResult); //记住我功能的相关处理 this.rememberMeServices.loginSuccess(request, response, authResult); if (this.eventPublisher != null) { //观察者模式,发布事件消息 this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass())); } //使用successHandler 触发 成功的处理策略 this.successHandler.onAuthenticationSuccess(request, response, authResult); } ``` ​ ```java @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { //ExceptionTranslationFilter 在认证开始前把 request 缓存起来 //获取请求缓存,可以跳转回 用户登陆前 想访问的 url SavedRequest savedRequest = this.requestCache.getRequest(request, response); if (savedRequest == null) { super.onAuthenticationSuccess(request, response, authentication); return; } String targetUrlParameter = getTargetUrlParameter(); if (isAlwaysUseDefaultTargetUrl() || (targetUrlParameter != null && StringUtils.hasText(request.getParameter(targetUrlParameter)))) { this.requestCache.removeRequest(request, response); super.onAuthenticationSuccess(request, response, authentication); return; } clearAuthenticationAttributes(request); // Use the DefaultSavedRequest URL String targetUrl = savedRequest.getRedirectUrl(); //跳转 指定的成功页面 getRedirectStrategy().sendRedirect(request, response, targetUrl); } ``` 流程梳理: 1. UsernamePasswordAuthenticationFilter 拦截到 /login 请求,执行 attemptAuthentication 方法把账号密码封装成 UsernamePasswordAuthenticationToken 对象 传入 ProviderManager authenticate 方法 2. ProviderManager authenticate 方法中 通过循环比对的方式 找到合适的 认证提供商--AbstractUserDetailsAuthenticationProvider 3. AbstractUserDetailsAuthenticationProvider 的 认证方法调用 DaoAuthenticationProvider 的 retrieveUser 方法 4. DaoAuthenticationProvider最终会调用 UserDetailsService(需要我们来实现,整合用户体系) 来返回符合条件的用户 5. AbstractUserDetailsAuthenticationProvider 中会对比密码,并把 认证成功的对象 重新封装成 UsernamePasswordAuthenticationToken对象 6. 最终 重新封装的 UsernamePasswordAuthenticationToken 会返回到 UsernamePasswordAuthenticationFilter,做出成功或失败的响应操作。 ### 2.4 记住我功能实现 ​ 在 spring-security.xml 的配置文件中 配置一下,数据源,过期时间,字段名称(默认为 remember-me) ```xml ``` 登陆页面 ```html ``` 勾选记住我登陆后,后台会产生一个 cookie,数据库 persistent_logins 表会产生一条新纪录,因为我们配置了数据源,如果不配置数据源 将不会写入数据库,表结构和名称是固定的 ![image-20210831224110462](https://file-zpr.oss-cn-shanghai.aliyuncs.com/blog/image-20210831224110462.png) ### 2.5 csrf 跨站请求伪造 防护机制 ​ 什么是跨站请求伪造=》 https://baike.baidu.com/item/跨站请求伪造/13777878?fr=aladdin SpringSecurity 默认开启csrf 验证,所有页面请求的地方都需要 填写 csrf token,如果没有 将返回403 权限拒绝❌,下面在源码中 我们可以看到。 当使用了 jsp 时,SpringSecurity 默认提供了 jsp 的标签库 首先需要先引入标签库 <%@ taglib prefix="security" uri="http://www.springframework.org/security/tags"%> 使用: ```html <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib prefix="security" uri="http://www.springframework.org/security/tags"%> 数据 - AdminLTE2定制版 | Log in ``` ​ csrf 功能 通过 CsrfFilter 提供,可以通过配置文件 配置 ```xml ``` ```java public final class CsrfFilter extends OncePerRequestFilter { //默认的 匹配器 private static final class DefaultRequiresCsrfMatcher implements RequestMatcher { private final HashSet allowedMethods = new HashSet<>(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS")); @Override public boolean matches(HttpServletRequest request) { return !this.allowedMethods.contains(request.getMethod()); } @Override public String toString() { return "CsrfNotRequired " + this.allowedMethods; } } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { request.setAttribute(HttpServletResponse.class.getName(), response); CsrfToken csrfToken = this.tokenRepository.loadToken(request); boolean missingToken = (csrfToken == null); if (missingToken) { csrfToken = this.tokenRepository.generateToken(request); this.tokenRepository.saveToken(csrfToken, request, response); } request.setAttribute(CsrfToken.class.getName(), csrfToken); request.setAttribute(csrfToken.getParameterName(), csrfToken); //post,put,delete 等触发修改的请求 都将被拦截 if (!this.requireCsrfProtectionMatcher.matches(request)) { if (this.logger.isTraceEnabled()) { this.logger.trace("Did not protect against CSRF since request did not match " + this.requireCsrfProtectionMatcher); } //"GET", "HEAD", "TRACE", "OPTIONS" 直接放行 filterChain.doFilter(request, response); return; } String actualToken = request.getHeader(csrfToken.getHeaderName()); if (actualToken == null) { actualToken = request.getParameter(csrfToken.getParameterName()); } if (!equalsConstantTime(csrfToken.getToken(), actualToken)) { this.logger.debug( LogMessage.of(() -> "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request))); AccessDeniedException exception = (!missingToken) ? new InvalidCsrfTokenException(csrfToken, actualToken) : new MissingCsrfTokenException(actualToken); //csrf token 传的有问题,将返回权限拒绝错误🙅 this.accessDeniedHandler.handle(request, response, exception); return; } filterChain.doFilter(request, response); } ``` ### 2.6 SpringSecurity taglibs jsp 标签库 ​ 文档 🚪=》https://docs.spring.io/spring-security/site/docs/5.0.7.RELEASE/reference/htmlsingle/#taglibs 它为访问安全信息和在JSP中应用安全约束提供了基本支持。 使用需要导入 maven依赖 ```xml org.springframework.security spring-security-taglibs 5.5.2 ``` 在 jsp 中导入 taglib 标签库 ```xml <%@ taglib prefix="security" uri="http://www.springframework.org/security/tags"%> ``` ​ 授权校验 ```xml
  • 产品管理
  • ``` csrf token 支持 ```html ``` 页面上显示用户信息 ```html <%----%> ``` ### 2.7 自定义登陆界面 ​ 默认SpringSecurity 提供了 默认的登陆界面 我们可以通过配置的方式 指定我们自己的 登陆页面 和 注销操作 ``` ``` ### 2.8 处理全局异常 ​ 每当 SpringSecurity 出现 状态吗异常 都是原生的丑陋界面 受不了,用户更受不了,所以需要定义 自己的错误页面。 ![image-20210831234358003](https://file-zpr.oss-cn-shanghai.aliyuncs.com/blog/image-20210831234358003.png) ​ 配置: ```xml ``` ![image-20210831235602880](https://file-zpr.oss-cn-shanghai.aliyuncs.com/blog/image-20210831235602880.png) 定义 Spring-mvc 全局异常类,根据异常跳转对应页面。 ```java package com.learingsecurity.controller.advice; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.acls.model.NotFoundException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; /** * @className ControllerExceptionAdvice * @Desc TODO 全局异常类 * @Author 张埔枘 * @Date 2021/8/28 9:11 下午 * @Version 1.0 */ @ControllerAdvice public class ControllerExceptionAdvice { /** * 拦截 403 异常 跳转到 403 页面 * @return */ @ExceptionHandler(AccessDeniedException.class) public String exception403(){ return "forward:/403.jsp"; } } ``` ### 2.9 加密方式 ​ 文档 🚪 =》 https://docs.spring.io/spring-security/site/docs/current/reference/html5/#authentication-password-storage 我们常用的是 BCryptPasswordEncoder 类,采用动态盐的方式加强密码的安全性。 使用需要把它放入 spring 的容器中 ```xml ``` ### 3.0 放行静态资源 ​ 在开发早期,单体项目时,前端代码和后端代码是在一起的。 那么SpringSecurity 对接口拦截的同时,也可能会对 静态资源拦截。所以需要 处理一下,不拦截静态资源 ```xml ``` 源码:https://gitee.com/zhang-purui/spring-spring-security 欢迎 Start 😄😄😄