德兴网站建设公司,wordpress团购模板,郑州网站设计报价表,wordpress自己写页面1、SpringSecurity 入门
1.1、简介 Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它是用于保护基于Spring的应用程序的实际标准。Spring Security是一个框架#xff0c;致力于为Java应用程序提供身份验证和授权。与所有Spring项目一样#xff0c;Sp…1、SpringSecurity 入门
1.1、简介 Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它是用于保护基于Spring的应用程序的实际标准。Spring Security是一个框架致力于为Java应用程序提供身份验证和授权。与所有Spring项目一样Spring Security的真正强大之处在于可以轻松扩展以满足自定义要求。官网地址 1.2、技术方案对比 目前在整个Java开发的系统中,需要用于身份验证和访问控制框架的框架除了Spring Security, 还有一个就是Apache shiro框架。 Shiro Shiro是一个强大而灵活的开源安全框架能够非常清晰的处理认证、授权、管理会话以及密码加密。如下是它所具有的特点
易于理解的 Java Security API简单的身份认证登录支持多种数据源LDAPJDBCKerberosActiveDirectory等对角色的简单的鉴权访问控制支持细粒度的鉴权支持一级缓存以提升应用程序的性能内置的基于 POJO 企业会话管理适用于 Web 以及非 Web 的环境异构客户端会话访问非常简单的加密 API不跟任何的框架或者容器捆绑可以独立运行。 SpringSecurity 除了不能脱离Springshiro的功能它都有。而且Spring Security对Oauth、OpenID也有支持,Shiro则需要自己手动实现。Spring Security的权限细粒度更高。 OAuth在客户端与服务提供商之间设置了一个授权层authorization layer。客户端不能直接登录服务提供商只能登录授权层以此将用户与客户端区分开来。客户端登录授权层所用的令牌token与用户的密码不同。用户可以在登录的时候指定授权层令牌的权限范围和有效期。 客户端登录授权层以后服务提供商根据令牌的权限范围和有效期向客户端开放用户储存的资料。 OpenID 系统的第一部分是身份验证即如何通过 URI 来认证用户身份。目前的网站都是依靠用户名和密码来登录认证这就意味着大家在每个网站都需要注册用户名和密码即便你使用的是同样的密码。如果使用 OpenID 你的网站地址URI就是你的用户名而你的密码安全的存储在一个 OpenID 服务网站上你可以自己建立一个 OpenID 服务网站也可以选择一个可信任的 OpenID 服务网站来完成注册。 与OpenID同属性的身份识别服务商还有ⅥeID,ClaimID,CardSpace,Rapleaf,Trufina ID Card等其中ⅥeID通用账户的应用最为广泛。
1.3、应用场景 传统用户登录 用户授权在系统中用户拥有哪些权限 单一登录 一个账号在同一时间只能在一个地方进行登录, 如果在其他地方进行第二次登录,则剔除之前登录操作。 集成CAS 做单点登录,即多个系统只需登录一次无需重复登录。 集成OAuth2 做登录授权, 可以用于app登录和第三方登录(QQ,微信等), 也可以实现cas的功能. 1.4、入门案例
1.4.1、创建SpringBoot项目 1.4.2、未加依赖访问
package com.blnp.net.demo.controller;import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;/*** p/p** author lyb 2045165565qq.com* version 1.0* since 2024/9/3 17:28*/
// 该注解为组合注解等同于Spring中ControllerResponseBody注解
RestController
public class DemoController {RequestMapping(/demo)public String demo(){return Blnp spring Boot;}
}1.4.3、添加依赖后访问
!--添加Spring Security 依赖 --
dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-security/artifactId
/dependency 再次刷新访问后被自动跳转至SpringSecurity内置的登录页面了如下所示 这里有三点需要注意下
表单的提交方式和路径: post /logininput输入项的name值: username password隐藏域input的name: 值为: _csrf value值为d4329889-796a-447a-9d08-69e56bc7c296 SpringBoot已经为SpringSecurity提供了默认配置默认所有资源都必须认证通过才能访问。那么问题来了此刻并没有连接数据库也并未在内存中指定认证用户如何认证呢其实SpringBoot已经提供了默认用户名user密码在项目启动时随机生成如图 认证通过后可以继续访问处理器资源 2、SpringSecurity认证
2.1、认证方式基本原理 在使用SpringSecurity框架该框架会默认自动地替我们将系统中的资源进行保护每次访问资源的时候都必须经过一层身份的校验如果通过了则重定向到我们输入的url中否则访问是要被拒绝的。那么SpringSecurity框架是如何实现的呢? Spring Security功能的实现主要是由一系列过滤器相互配合完成。也称之为过滤器链。
2.1.1、过滤器链介绍 过滤器是一种典型的AOP思想下面简单了解下这些过滤器链,后续再源码剖析中在涉及到过滤器链在仔细说明。 1、WebAsyncManagerIntegrationFilter 完整路径org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter作用根据请求封装获取WebAsyncManager从WebAsyncManager获取/注册的安全上下文可调用处理拦截器 2、SecurityContextPersistenceFilter 完整路径org.springframework.security.web.context.SecurityContextPersistenceFilter作用SecurityContextPersistenceFilter主要是使用SecurityContextRepository在session中保存或更新一个SecurityContext并将SecurityContext给以后的过滤器使用来为后续fifilter建立所需的上下文。SecurityContext中存储了当前用户的认证以及权限信息。 3、HeaderWriterFilter 完整路径org.springframework.security.web.header.HeaderWriterFilter作用向请求的Header中添加相应的信息,可在http标签内部使用security:headers来控制 4、CsrfFilter 完整路径org.springframework.security.web.csrf.CsrfFilter作用csrf又称跨域请求伪造SpringSecurity会对所有post请求验证是否包含系统生成的csrf的token信息如果不包含则报错。起到防止csrf攻击的效果。 5、LogoutFilter 完整路径org.springframework.security.web.authentication.logout.LogoutFilter作用匹配URL为/logout的请求实现用户退出,清除认证信息。 6、UsernamePasswordAuthenticationFilter 完整路径org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter作用表单认证操作全靠这个过滤器默认匹配URL为/login且必须为POST请求。 7、DefaultLoginPageGeneratingFilter 完整路径org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter作用如果没有在配置文件中指定认证页面则由该过滤器生成一个默认认证页面。 8、DefaultLogoutPageGeneratingFilter 完整路径org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter作用由此过滤器可以生产一个默认的退出登录页面 9、BasicAuthenticationFilter 完整路径org.springframework.security.web.authentication.www.BasicAuthenticationFilter作用此过滤器会自动解析HTTP请求中头部名字为Authentication且以Basic开头的头信息。 10、RequestCacheAwareFilter 完整路径org.springframework.security.web.savedrequest.RequestCacheAwareFilter作用通过HttpSessionRequestCache内部维护了一个RequestCache用于缓存 HttpServletRequest 11、SecurityContextHolderAwareRequestFilter 完整路径org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter作用针对ServletRequest进行了一次包装使得request具有更加丰富的API 12、AnonymousAuthenticationFilter 完整路径org.springframework.security.web.authentication.AnonymousAuthenticationFilter作用当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中。spring security为了兼容未登录的访问也走了一套认证流程只不过是一个匿名的身份。 13、SessionManagementFilter 完整路径org.springframework.security.web.session.SessionManagementFilter作用securityContextRepository限制同一用户开启多个会话的数量 14、ExceptionTranslationFilter 完整路径org.springframework.security.web.access.ExceptionTranslationFilter作用异常转换过滤器位于整个springSecurityFilterChain的后方用来转换整个链路中出现的异常 15、FilterSecurityInterceptor 完整路径org.springframework.security.web.access.intercept.FilterSecurityInterceptor作用获取所配置资源访问的授权信息根据SecurityContextHolder中存储的用户信息来决定其是否有权限。
2.1.2、认证方式
1、HttpBasic 认证 HttpBasic登录验证模式是Spring Security实现登录验证最简单的一种方式也可以说是最简陋的一种方式。它的目的并不是保障登录验证的绝对安全而是提供一种“防君子不防小人”的登录验证。 在使用的Spring Boot早期版本为1.X版本,依赖的Security 4.X版本那么就无需任何配置启动项目访问则会弹出默认的httpbasic认证。现在使用的是spring boot2.0以上版本依赖Security5.X版本HttpBasic不再是默认的验证模式在spring security 5.x默认的验证模式已经是表单模式。 HttpBasic模式要求传输的用户名密码使用Base64模式进行加密。如果用户名是 admin 密码是“ admin”则将字符串admin:admin 使用Base64编码算法加密。加密结果可能是YWtaW46YWRtaW4。HttpBasic模式真的是非常简单又简陋的验证模式Base64的加密算法是可逆的,想要破解并不难. 2、FormLogin 登录认证 Spring Security的HttpBasic模式该模式比较简单只是进行了通过携带Http的Header进行简单的登录验证而且没有定制的登录页面所以使用场景比较窄。对于一个完整的应用系统与登录验证相关的页面都是高度定制化的非常美观而且提供多种登录方式。这就需要SpringSecurity支持我们自己定制登录页面, spring boot2.0以上版本依赖Security 5.X版本默认会生成一个登录页面. 2.2、表单认证
2.2.1、自定义表单登录页面 在config包下编写SecurityConfiguration配置类。 特别说明这里我列举两种配置方法至于为什么两种配置方法是因为跟Spring Security 的版本有关扩展 WebSecurityConfigurerAdapter 的配置方法在5.7.1版本以后或者SpringBoot 2.7.0版本以后已经被弃用了。 SpringBoot 2.7.0 版本前用法
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {/*** http请求处理方法** param http* throws Exception*/Overrideprotected void configure(HttpSecurity http) throws Exception {/*http.httpBasic()//开启httpbasic认证.and().authorizeRequests().anyRequest().authenticated();//所有请求都需要登录认证才能访问*/http.formLogin() //开启表单认证.and().authorizeRequests()..anyRequest().authenticated(); //所有请求都需要登录认证才能访问;}
}
SpringBoot 2.7.0 版本以后
Slf4j
Configuration
EnableWebSecurity //开启SpringSecurity的默认行为
RequiredArgsConstructor //bean注解
// 新版不需要继承WebSecurityConfigurerAdapter
public class WebSecurityConfig {// 这个类主要是获取库中的用户信息交给securityprivate final UserDetailServiceImpl userDetailsService;// 这个的类是认证失败处理我在这里主要是把错误消息以json方式返回private final JwtAuthenticationEntryPoint authenticationEntryPoint;// 鉴权失败的时候的处理类private final JwtAccessDeniedHandler jwtAccessDeniedHandler;// 登录成功处理private final LoginSuccessHandler loginSuccessHandler;// 登录失败处理private final LoginFailureHandler loginFailureHandler;// 登出成功处理private final LoginLogoutSuccessHandler loginLogoutSuccessHandler;// token过滤器private final JwtTokenFilter jwtTokenFilter;Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {return authenticationConfiguration.getAuthenticationManager();}// 加密方式Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** 核心配置*/Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {log.info(------------filterChain------------);http// 禁用basic明文验证.httpBasic(Customizer.withDefaults())// 基于 token 不需要 csrf.csrf(AbstractHttpConfigurer::disable)// 禁用默认登录页.formLogin(fl - fl.loginPage(PathMatcherUtil.FORM_LOGIN_URL).loginProcessingUrl(PathMatcherUtil.TO_LOGIN_URL).usernameParameter(username).passwordParameter(password).successHandler(loginSuccessHandler).failureHandler(loginFailureHandler).permitAll())// 禁用默认登出页.logout(lt - lt.logoutSuccessHandler(loginLogoutSuccessHandler))// 基于 token 不需要 session.sessionManagement(session - session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))// 设置 处理鉴权失败、认证失败.exceptionHandling(exceptions - exceptions.authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(jwtAccessDeniedHandler))// 下面开始设置权限.authorizeHttpRequests(authorizeHttpRequest - authorizeHttpRequest// 允许所有 OPTIONS 请求.requestMatchers(PathMatcherUtil.AUTH_WHITE_LIST).permitAll()// 允许直接访问 授权登录接口// .requestMatchers(HttpMethod.POST, /web/authenticate).permitAll()// 允许 SpringMVC 的默认错误地址匿名访问// .requestMatchers(/error).permitAll()// 其他所有接口必须有Authority信息Authority在登录成功后的UserDetailImpl对象中默认设置“ROLE_USER”//.requestMatchers(/**).hasAnyAuthority(ROLE_USER)// .requestMatchers(/heartBeat/**, /main/**).permitAll()// 允许任意请求被已登录用户访问不检查Authority.anyRequest().authenticated())// 添加过滤器.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);//可以加载fram嵌套页面http.headers(headers - headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin));return http.build();}Beanpublic UserDetailsService userDetailsService() {return userDetailsService::loadUserByUsername;}/*** 调用loadUserByUserName获取userDetail信息在AbstractUserDetailsAuthenticationProvider里执行用户状态检查** return*/Beanpublic AuthenticationProvider authenticationProvider() {DaoAuthenticationProvider authProvider new DaoAuthenticationProvider();authProvider.setUserDetailsService(userDetailsService);authProvider.setPasswordEncoder(passwordEncoder());return authProvider;}/*** 配置跨源访问(CORS)** return*/BeanCorsConfigurationSource corsConfigurationSource() {UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource();source.registerCorsConfiguration(/**, new CorsConfiguration().applyPermitDefaultValues());return source;}
} 弃用原因这是因为Spring框架的开发人员鼓励用户转向基于组件的安全配置。 这里开始我也将SpringBoot的版本从3.3.3版本降回到2.3.4版本为了方便后文的说明与复现。
1、发现问题 问题一重定向次数过多 因为设置登录页面为login.html 后面配置的是所有请求都登录认证,陷入了死循环. 所以需要将login.html放行不需要登录认证。
//开启表单认证http.formLogin().and().authorizeRequests()//放行登录页.antMatchers(/login.html).permitAll().anyRequest()//表示所有请求都需要登录认证才能访问.authenticated(); 问题二访问登录页显示404 spring boot整合thymeleaf 之后 所有的静态页面以放在resources/templates下面,所以得通过请求访问到模板页面, 将/login.html修改为 /toLoginPage
//开启表单认证http.formLogin().loginPage(/toLoginPage).and().authorizeRequests()//放行登录页.antMatchers(/toLoginPage).permitAll().anyRequest()//表示所有请求都需要登录认证才能访问.authenticated(); 问题三访问登录页页面样式丢失 因为访问login.html需要一些js , css , image等静态资源信息, 所以需要将静态资源放行, 不需要认证。
//解决静态资源被拦截的问题
web.ignoring().antMatchers(/css/**,/images/**,/js/**,/favion.ico,/login/**); Spring Security 中安全构建器HttpSecurity 和WebSecurity 的区别是
WebSecurity 不仅通过HttpSecurity 定义某些请求的安全控制也通过其他方式定义其他某些请求可以忽略安全控制;HttpSecurity 仅用于定义需要安全控制的请求(当然HttpSecurity 也可以指定某些请求不需要安全控制);可以认为HttpSecurity 是WebSecurity 的一部分 WebSecurity 是包含HttpSecurity 的更大的一个概念;构建目标不同 WebSecurity 构建目标是整个Spring Security 安全过滤器FilterChainProxy,HttpSecurity 的构建目标仅仅是FilterChainProxy 中的一个SecurityFilterChain 。
2.2.2、表单登录 通过讲解过滤器链中我们知道有个过滤器UsernamePasswordAuthenticationFilter是处理表单登录的. 那么下面我们来通过源码观察下这个过滤器. 在源码中可以观察到, 表单中的input的name值是username和password, 并且表单提交的路径为/login , 表单提交方式method为post , 这些可以修改为自定义的值.
Override
protected void configure(HttpSecurity http) throws Exception {//开启表单认证http.formLogin()//配置自定义登录页面.loginPage(/toLoginPage)//配置登录请求的URL.loginProcessingUrl(/login)//修改登录表单字段信息.usernameParameter(username).passwordParameter(password)//修改登录成功后跳转地址.successForwardUrl(/).and().authorizeRequests()//放行登录页.antMatchers(/toLoginPage).permitAll().anyRequest()//表示所有请求都需要登录认证才能访问.authenticated();//开闭CSRF防护http.csrf().disable();
} 最终登录成功后显示如下图 可以看到虽然登录成功了但是页面又有一个新问题。 发现行内框架iframe这里出现问题了. Spring Security下X-Frame-Options默认为DENY,非SpringSecurity环境下X-Frame-Options的默认大多也是DENY这种情况下浏览器拒绝当前页面加载任何Frame页面设置含义如下
DENY浏览器拒绝当前页面加载任何Frame页面 此选择是默认的.SAMEORIGINframe页面的地址只能为同源域名下的页面
Override
protected void configure(HttpSecurity http) throws Exception {//开启表单认证http.formLogin()//配置自定义登录页面.loginPage(/toLoginPage)//配置登录请求的URL.loginProcessingUrl(/login)//修改登录表单字段信息.usernameParameter(username).passwordParameter(password)//修改登录成功后跳转地址.successForwardUrl(/).and().authorizeRequests()//放行登录页.antMatchers(/toLoginPage).permitAll().anyRequest()//表示所有请求都需要登录认证才能访问.authenticated();//开闭CSRF防护http.csrf().disable();//允许Iframe页面加载配置http.headers().frameOptions().sameOrigin();
}
2.2.3、基于数据库实现认证功能 之前我们所使用的用户名和密码是来源于框架自动生成的, 那么我们如何实现基于数据库中的用户名和密码功能呢? 要实现这个得需要实现security的一个UserDetailsService接口, 重写这个接口里面 loadUserByUsername 即可。 编写MyUserDetailsService并实现UserDetailsService接口,重写loadUserByUsername方法。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collection;Service
public class MyUserDetailsService implements UserDetailsService {AutowiredUserService userService;/*** 根据username查询用户实体** param username* return* throws UsernameNotFoundException*/Overridepublic UserDetails loadUserByUsername(String username) throwsUsernameNotFoundException {User user userService.findByUsername(username);if(user null) {throw new UsernameNotFoundException(username); // 用户名没有找到}// 先声明一个权限集合, 因为构造方法里面不能传入nullCollection ? extends GrantedAuthority authorities new ArrayList ();// 需要返回一个SpringSecurity的UserDetails对象UserDetails userDetails new org.springframework.security.core.userdetails.User(user.getUsername(), {noop} user.getPassword(), // {noop}表示不加密认证。true, // 用户是否启用 true 代表启用true, // 用户是否过期 true 代表未过期true, // 用户凭据是否过期 true 代表未过期true, // 用户是否锁定 true 代表未锁定authorities);return userDetails;}
} 在SecurityConfiguration配置类中指定自定义用户认证
/*** 身份验证管理器** param auth* throws Exception*/
Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(myUserDetailsService); // 使用自定义用户认证
}
2.2.4、密码加密认证 在基于数据库完成用户登录的过程中,我们所是使用的密码是明文的规则是通过对密码明文添加 {noop} 前缀。那么下面 Spring Security 中的密码编码进行一些探讨。 Spring Security 中PasswordEncoder 就是我们对密码进行编码的工具接口。该接口只有两个功能一个是匹配验证。另一个是密码编码。 算法介绍 BCrypt算法 任何应用考虑到安全绝不能明文的方式保存密码。密码应该通过哈希算法进行加密。 有很多标准的算法比如SHA或者MD5结合salt(盐)是一个不错的选择。 Spring Security 提供了BCryptPasswordEncoder类,实现Spring的PasswordEncoder接口使用BCrypt强哈希方法来加密密码。BCrypt强哈希方法 每次加密的结果都不一样,所以更加的安全。 bcrypt算法相对来说是运算比较慢的算法在密码学界有句常话越慢的算法越安全。黑客破解成本越高.通过salt和const这两个值来减缓加密过程它的加密时间百ms级远远超过md5大概1ms左右。对于计算机来说Bcrypt 的计算速度很慢但是对于用户来说这个过程不算慢。bcrypt是单向的而且经过salt和cost的处理使其受攻击破解的概率大大降低同时破解的难度也提升不少相对于MD5等加密方式更加安全而且使用也比较简单。 bcrypt加密后的字符串形如$2a$10$wouq9P/HNgvYj2jKtUN8rOJJNRVCWvn1XoWy55N3sCkEHZPo3lyWq 其中$是分割符无意义2a是bcrypt加密版本号10是const的值而后的前22位是salt值再然后的字符串就是密码的密文了这里的const值即生成salt的迭代次数默认值是10推荐值12。
在项目中使用BCrypt 首先看下PasswordEncoderFactories 密码器工厂 之前我们在项目中密码使用的是明文的是noop , 代表不加密使用明文密码, 现在用BCrypt只需要将 noop 换成 bcrypt 即可。
Override
public UserDetails loadUserByUsername(String username) throws
UsernameNotFoundException {User user userService.findByUsername(username);if(user null) {throw new UsernameNotFoundException(username); // 用户名没有找到}// 先声明一个权限集合, 因为构造方法里面不能传入nullCollection ? extends GrantedAuthority authorities new ArrayList ();// 需要返回一个SpringSecurity的UserDetails对象UserDetails userDetails neworg.springframework.security.core.userdetails.User(user.getUsername(), {bcrypt} user.getPassword(), // {noop}表示不加密认证。 {bcrypt}加密认证 true, // 用户是否启用 true 代表启用true, // 用户是否过期 true 代表未过期true, // 用户凭据是否过期 true 代表未过期true, // 用户是否锁定 true 代表未锁定authorities);return userDetails;
} 同时需要将数据库中的明文密码修改为加密密码 选择一个放入数据库即可。 2.2.5、获取当前登录用户 在传统web系统中, 我们将登录成功的用户放入session中, 在需要的时候可以从session中获取用户,那么Spring Security中我们如何获取当前已经登录的用户呢?
SecurityContextHolder 保留系统当前的安全上下文SecurityContext其中就包括当前使用系统的用户的信息。
SecurityContext 安全上下文,获取当前经过身份验证的主体或身份验证请求令牌。
/*** 获取当前登录用户** return*/
RequestMapping(/loginUser)
ResponseBody
public UserDetails getCurrentUser() {UserDetails userDetails (UserDetails)SecurityContextHolder.getContext().getAuthentication().getPrincipal();return userDetails;
} 除了上述方法, Spring Security 还提供了2种方式可以获取.
/*** 获取当前登录用户** return*/
RequestMapping(/loginUser2)
ResponseBody
public UserDetails getCurrentUser(Authentication authentication) {UserDetails userDetails (UserDetails) authentication.getPrincipal();return userDetails;
}/*** 获取当前登录用户** return*/
RequestMapping(/loginUser3)
ResponseBody
public UserDetails getCurrentUser(AuthenticationPrincipal UserDetails userDetails) {return userDetails;
}
2.2.6、记住我 在大多数网站中都会实现RememberMe这个功能方便用户在下一次登录时直接登录避免再次输入用户名以及密码去登录,Spring Security针对这个功能已经帮助我们实现, 下面我们来看下他的原理图。 简单的Token生成方法 TokenMD5(username分隔符expiryTime分隔符password) 注意: 这种方式不推荐使用, 有严重的安全问题. 就是密码信息在前端浏览器cookie中存放. 如果cookie被盗取很容易破解.
div classform-groupdiv!--记住我 name为remember-me value值可选true yes 1 on 都行--input typecheckbox nameremember-me valuetrue /记住我/div
/div
Override
protected void configure(HttpSecurity http) throws Exception {//开启表单认证http.formLogin()//配置自定义登录页面.loginPage(/toLoginPage)//配置登录请求的URL.loginProcessingUrl(/login)//修改登录表单字段信息.usernameParameter(username).passwordParameter(password)//修改登录成功后跳转地址.successForwardUrl(/)//开启记住我的功能.and().rememberMe()//设置token失效时间为两周.tokenValiditySeconds(1209600)//自定义表单name值.rememberMeParameter(remember-me).and().authorizeRequests()//放行登录页.antMatchers(/toLoginPage).permitAll().anyRequest()//表示所有请求都需要登录认证才能访问.authenticated();//开闭CSRF防护http.csrf().disable();//允许Iframe页面加载配置http.headers().frameOptions().sameOrigin();
} 持久化的Token生成方法 存入数据库Token包含
token: 随机生成策略,每次访问都会重新生成series: 登录序列号随机生成策略。用户输入用户名和密码登录时该值重新生成。使用 remember-me功能该值保持不变expiryTime: token过期时间。 CookieValueencode(seriestoken) /*** http请求处理方法** param http* throws Exception*/
Override
protected void configure(HttpSecurity http) throws Exception {/*http.httpBasic()//开启httpbasic认证.and().authorizeRequests().anyRequest().authenticated();//所有请求都需要登录认证才能访问*/http.formLogin() //开启表单认证.loginPage(/toLoginPage) //自定义登录页面.loginProcessingUrl(/login) // 登录处理Url//.usernameParameter().passwordParameter(). 修改自定义表单name值..successForwardUrl(/) // 登录成功后跳转路径.and().authorizeRequests().antMatchers(/toLoginPage).permitAll() //放行登录页面与静态资源.anyRequest().authenticated() //所有请求都需要登录认证才能访问;.and().rememberMe() //开启记住我功能.tokenValiditySeconds(1209600) // token失效时间默认2周.rememberMeParameter(remember-me) // 自定义表单name值.tokenRepository(getPersistentTokenRepository()); // 设置tokenRepository// 关闭csrf防护http.csrf().disable();// 允许iframe加载页面http.headers().frameOptions().sameOrigin();
}Autowired
DataSource dataSource;/*** 持久化token,负责token与数据库之间的相关操作** return*/
Bean
public PersistentTokenRepository getPersistentTokenRepository() {JdbcTokenRepositoryImpl tokenRepository newJdbcTokenRepositoryImpl();tokenRepository.setDataSource(dataSource); //设置数据源// 启动时创建一张表, 第一次启动的时候创建, 第二次启动的时候需要注释掉, 否则会报错tokenRepository.setCreateTableOnStartup(true);return tokenRepository;
} 项目启动成功后,观察数据库,会帮助我们创建persistent_logins表。 当再次访问登录功能时观察数据库,会插入一条记录.说明持久化token方式已经生效。 Cookie 伪造演示 使用网页登录系统,记录remember-me的值使用postman 伪造cookie。 安全验证 /*** 根据用户ID查询用户** return*/
GetMapping(/{id})
ResponseBody
public User getById(PathVariable Integer id) {//获取认证信息Authentication authentication SecurityContextHolder.getContext().getAuthentication();// 判断认证信息是否来源于RememberMeif(RememberMeAuthenticationToken.class.isAssignableFrom(authentication.getClas s())) {throw new RememberMeAuthenticationException(认证信息来源于RememberMe, 请重新登录 );}User user userService.getById(id);return user;} 在重要操作步骤可以加以验证, true代表自动登录,则引导用户重新表单登录, false正常进行。
2.2.7、自定义登录成功失败处理 在某些场景下,用户登录成功或失败的情况下用户需要执行一些后续操作,比如登录日志的搜集,或者在现在目前前后端分离的情况下用户登录成功和失败后需要给前台页面返回对应的错误信息, 有前台主导登录成功或者失败的页面跳转. 这个时候需要要到用到AuthenticationSuccessHandler与AnthenticationFailureHandler. 自定义成功处理 实现AuthenticationSuccessHandler接口并重写onAnthenticationSuccesss()方法。 自定义失败处理 实现AuthenticationFailureHandler接口并重写onAuthenticationFailure()方法
Override
protected void configure(HttpSecurity http) throws Exception {//开启表单认证http.formLogin()//配置自定义登录页面.loginPage(/toLoginPage)//配置登录请求的URL.loginProcessingUrl(/login)//修改登录表单字段信息.usernameParameter(username).passwordParameter(password)//修改登录成功后跳转地址.successForwardUrl(/).successHandler(myAuthenticationService).failureHandler(myAuthenticationService) //登录成功或者失败的处理//开启记住我的功能.and().rememberMe()//设置token失效时间为两周.tokenValiditySeconds(1209600)//自定义表单name值.rememberMeParameter(remember-me).and().authorizeRequests()//放行登录页.antMatchers(/toLoginPage).permitAll().anyRequest()//表示所有请求都需要登录认证才能访问.authenticated();//开闭CSRF防护http.csrf().disable();//允许Iframe页面加载配置http.headers().frameOptions().sameOrigin();
}
/*** 自定义登录成功或失败处理器,退出登录处理器*/
Service
public class MyAuthenticationService implements AuthenticationSuccessHandler,AuthenticationFailureHandler, LogoutSuccessHandler {RedirectStrategy redirectStrategy new DefaultRedirectStrategy();AutowiredObjectMapper objectMapper;/*** 登录成功后处理逻辑** param request* param response* param authentication* throws IOException* throws ServletException*/Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {System.out.println(登录成功后继续处理......);// 重定向到index页面// redirectStrategy.sendRedirect(request, response, /);Map result new HashMap();result.put(code, HttpStatus.OK.value());//200result.put(message, 登录成功);response.setContentType(application/json;charsetUTF-8);response.getWriter().write(objectMapper.writeValueAsString(result));}/*** 登录失败的处理逻辑** param request* param response* param exception* throws IOException* throws ServletException*/Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {System.out.println(登录失败后继续处理......);// 重定向到login页面// redirectStrategy.sendRedirect(request, response, /toLoginPage);Map result new HashMap();result.put(code, HttpStatus.UNAUTHORIZED.value());//401result.put(message, exception.getMessage());response.setContentType(application/json;charsetUTF-8);response.getWriter().write(objectMapper.writeValueAsString(result));}Overridepublic void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {System.out.println(退出之后继续处理...);redirectStrategy.sendRedirect(request, response, /toLoginPage);}
} 异步用户登录实现 form idformLogin action/login methodpostdiv classpanel loginbox.....div stylepadding:30px;input typebutton onclicklogin()classbutton button-block bg-main textbig
input-big value登录/div/div
/form
/div
/div
/div
scriptfunction login() {$.ajax({type: POST,//方法类型dataType: json,//服务器预期返回类型url: /login, // 登录urldata: $(#formLogin).serialize(),success: function (data) {console.log(data)if (data.code 200) {window.location.href /;} else {alert(data.message);}}});}
/script
2.2.8、退出登录 实现拓展类 类全路径org.springframework.security.web.authentication.logout.LogoutFilter作用匹配URL为/logout的请求实现用户退出,清除认证信息。 只需要发送请求,请求路径为/logout即可, 当然这个路径也可以自行在配置类中自行指定, 同时退出操作也有对应的自定义处理LogoutSuccessHandler,退出登录成功后执行,退出的同时如果有remember-me的数据,同时一并删除。
前端页面
a classbutton button-little bg-red href/logoutspan classicon-power-off/span退出登录
/a
后端代码
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Service;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;/*** 自定义登录成功,失败,退出处理类*/
Service
public class MyAuthenticationService implements AuthenticationSuccessHandler,AuthenticationFailureHandler, LogoutSuccessHandler {private RedirectStrategy redirectStrategy new DefaultRedirectStrategy();//................Overridepublic void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throwsIOException, ServletException {System.out.println(退出成功后续处理....);redirectStrategy.sendRedirect(request, response, /toLoginPage);}
}
Overrideprotected void configure(HttpSecurity http) throws Exception {//开启表单认证http.formLogin()//配置自定义登录页面.loginPage(/toLoginPage)//配置登录请求的URL.loginProcessingUrl(/login)//修改登录表单字段信息.usernameParameter(username).passwordParameter(password)//修改登录成功后跳转地址.successForwardUrl(/)//开启记住我的功能.and().rememberMe()//设置token失效时间为两周.tokenValiditySeconds(1209600)//自定义表单name值.rememberMeParameter(remember-me)/*** 退出相关操作配置**/.and().logout()//设置退出url地址.logoutUrl(/logout).logoutSuccessHandler(myAuthenticationService).and().authorizeRequests()//放行登录页.antMatchers(/toLoginPage).permitAll().anyRequest()//表示所有请求都需要登录认证才能访问.authenticated();//开闭CSRF防护http.csrf().disable();//允许Iframe页面加载配置http.headers().frameOptions().sameOrigin();}
2.3、图形验证码认证 图形验证码一般是防止恶意人眼看起来都费劲何况是机器。不少网站为了防止用户利用机器人自动注册、登录、灌水都采用了验证码技术。所谓验证码就是将一串随机产生的数字或符号生成一幅图片 图片里加上一些干扰, 也有目前需要手动滑动的图形验证码. 这种可以有专门去做的第三方平台. 比如极验。spring security添加验证码大致可以分为三个步骤
根据随机数生成验证码图片将验证码图片显示到登录页面认证流程中加入验证码校验 Spring Security的认证校验是由UsernamePasswordAuthenticationFilter过滤器完成的所以我们的验证码校验逻辑应该在这个过滤器之前。验证码通过后才能到后续的操作. 流程如下 自定义验证码过滤器 ValidateCodeFilter import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;Component
public class ValidateCodeFilter extends OncePerRequestFilter {AutowiredMyAuthenticationService myAuthenticationService;AutowiredStringRedisTemplate stringRedisTemplate;Overrideprotected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throwsServletException, IOException {// 判断是否是登录请求,只有登录请求才去校验验证码if(httpServletRequest.getRequestURI().equals(/login) httpServletRequest.getMethod().equalsIgnoreCase(post)) {try {validate(httpServletRequest);}catch (ValidateCodeException e) {myAuthenticationService.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);return;}}//如果不是登录请求直接调用后面的过滤器链filterChain.doFilter(httpServletRequest, httpServletResponse);}private void validate(HttpServletRequest request) throws ServletRequestBindingException {//获取ipString remoteAddr request.getRemoteAddr();//拼接redis的keyString redisKey ValidateCodeController.REDIS_KEY_IMAGE_CODE - remoteAddr;//从redis中获取imageCodeString redisImageCode stringRedisTemplate.boundValueOps(redisKey).get();String imageCode request.getParameter(imageCode);if(!StringUtils.hasText(imageCode)) {throw new ValidateCodeException(验证码的值不能为空);}if(redisImageCode null) {throw new ValidateCodeException(验证码已过期);}if(!redisImageCode.equals(imageCode)) {throw new ValidateCodeException(验证码不正确);}// 从redis中删除imageCodestringRedisTemplate.delete(redisKey);}
} 自定义验证码异常类 import org.springframework.security.core.AuthenticationException;/*** 验证码异常类*/
public class ValidateCodeException extends AuthenticationException {public ValidateCodeException(String msg) {super(msg);}
} security配置类 Autowired
ValidateCodeFilter validateCodeFilter;/*** http请求处理方法** param http* throws Exception*/
Override
protected void configure(HttpSecurity http) throws Exception {/*http.httpBasic()//开启httpbasic认证.and().authorizeRequests().anyRequest().authenticated();//所有请求都需要登录认证才能访问*/// 加在用户名密码过滤器的前面http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);http.formLogin() //开启表单认证.loginPage(/toLoginPage) //自定义登录页面.loginProcessingUrl(/login) // 登录处理Url//.usernameParameter().passwordParameter(). 修改自定义表单name 值..successForwardUrl(/) // 登录成功后跳转路径.successHandler(myAuthenticationService) //自定义登录成功处理.failureHandler(myAuthenticationService) //自定义登录失败处理.and().logout().logoutUrl(/logout) //设置退出url.logoutSuccessHandler(myAuthenticationService) //自定义退出处理.and().authorizeRequests().antMatchers(/toLoginPage, /code/**).permitAll() //放行登录页面与静态资源.anyRequest().authenticated() //所有请求都需要登录认证才能访问;.and().rememberMe() //开启记住我功能.tokenValiditySeconds(1209600) // token失效时间默认2周.rememberMeParameter(remember-me) // 自定义表单name值.tokenRepository(getPersistentTokenRepository()); // 设置tokenRepository// 关闭csrf防护http.csrf().disable();// 允许iframe加载页面http.headers().frameOptions().sameOrigin();
}
2.4、session 管理 Spring Security可以与Spring Session库配合使用只需要做一些简单的配置就可以实现一些功能如(会话过期、一个账号只能同时在线一个、集群session等)。
2.4.1、会话超时 配置session会话超时时间默认为30分钟但是Spring Boot中的会话超时时间至少为60秒。当session超时后, 默认跳转到登录页面。
#session设置
#配置session超时时间
server.servlet.session.timeout60 自定义设置session超时后地址设置session管理和失效后跳转地址。
http.sessionManagement()//设置session管理.invalidSessionUrl(/toLoginPage)// session无效后跳转的路径,默认是登录页面
2.4.2、并发控制 并发控制即同一个账号同时在线个数,同一个账号同时在线个数如果设置为1表示该账号在同一时间内只能有一个有效的登录如果同一个账号又在其它地方登录那么就将上次登录的会话过期即后面的登录会踢掉前面的登录。 1、修改超时时间 #session设置
#配置session超时时间
server.servlet.session.timeout600 2、设置最大会话数量 http.sessionManagement() //设置session管理.invalidSessionUrl(/toLoginPage) // session无效后跳转的路径, 默认是登录页面.maximumSessions(1) //设置session最大会话数量 ,1同一时间只能有一个用户登录.expiredUrl(/toLoginPage); //设置session过期后跳转路径 3、阻止用户第二次登录 sessionManagement也可以配置 maxSessionsPreventsLoginboolean值当达到maximumSessions设置的最大会话个数时阻止登录。
http.sessionManagement()//设置session管理.invalidSessionUrl(/toLoginPage)// session无效后跳转的路径, 默认是登录页面.maximumSessions(1)//设置session最大会话数量 ,1同一时间只能有一个用户登录.maxSessionsPreventsLogin(true)//当达到最大会话个数时阻止登录。.expiredUrl(/toLoginPage);//设置session过期后跳转路径
2.4.3、集群 Session 实际场景中一个服务会至少有两台服务器在提供服务在服务器前面会有一个nginx做负载均衡用户访问nginxnginx再决定去访问哪一台服务器。当一台服务宕机了之后另一台服务器也可以继续提供服务保证服务不中断。如果我们将session保存在Web容器(比如tomcat)中如果一个用户第一次访问被分配到服务器1上面需要登录当某些访问突然被分配到服务器二上因为服务器二上没有用户在服务器一上登录的会话session信息服务器二还会再次让用户登录用户已经登录了还让登录就感觉不正常了。 解决这个问题的思路是用户登录的会话信息不能再保存到Web服务器中而是保存到一个单独的库(redis、mongodb、jdbc等)中所有服务器都访问同一个库都从同一个库来获取用户的session信息如用户在服务器一上登录将会话信息保存到库中用户的下次请求被分配到服务器二服务器二从库中检查session是否已经存在如果存在就不用再登录了可以直接访问服务了。 引用依赖 !-- 基于redis实现session共享 --
dependencygroupidorg.springframework.session/groupidartifactidspring-session-data-redis/artifactid
/dependency 设置session存储类型 #使用redis共享session
spring.session.store-typeredis
2.5、CSRF 防护机制
2.5.1、什么是 CSRF CSRFCross-site request forgery中文名称跨站请求伪造。 你这可以这么理解CSRF攻击攻击者盗用了你的身份以你的名义发送恶意请求。CSRF能够做的事情包括以你名义发送邮件发消息盗取你的账号甚至于购买商品虚拟货币转账......造成的问题包括个人隐私泄露以及财产安全。 CSRF这种攻击方式在2000年已经被国外的安全人员提出但在国内直到06年才开始被关注08年国内外的多个大型社区和交互网站分别爆出CSRF漏洞如NYTimes.com纽约时报、Metafilter一个大型的BLOG网站YouTube和百度HI......而现在互联网上的许多站点仍对此毫无防备以至于安全业界称CSRF为“沉睡的巨人”。
2.5.2、CSRF 的原理 从上图可以看出要完成一次CSRF攻击受害者必须依次完成三个步骤
登录受信任网站A并在本地生成Cookie。在不登出A的情况下访问危险网站B。触发网站B中的一些元素
2.5.3、CSRF 的防御策略 在业界目前防御 CSRF 攻击主要有三种策略验证 HTTP Referer 字段在请求地址中添加 token 并验证在 HTTP 头中自定义属性并验证。 1、验证 HTTP Referer 字段 根据 HTTP 协议在 HTTP 头中有一个字段叫 Referer它记录了该 HTTP 请求的来源地址。在通常情况下访问一个安全受限页面的请求来自于同一个网站在后台请求验证其 Referer 值如果是以自身安全网站开头的域名则说明该请求是是合法的。如果 Referer 是其他网站的话则有可能是黑客的 CSRF 攻击拒绝该请求。 2、在请求地址中添加 token 并验证 CSRF 攻击之所以能够成功是因为黑客可以完全伪造用户的请求该请求中所有的用户验证信息都是存在于 cookie 中因此黑客可以在不知道这些验证信息的情况下直接利用用户自己的cookie 来通过安全验证。要抵御 CSRF关键在于在请求中放入黑客所不能伪造的信息并且该信息不存在于 cookie 之中。可以在 HTTP 请求中以参数的形式加入一个随机产生的 token并在服务器端建立一个拦截器来验证这个 token如果请求中没有 token 或者 token 内容不正确则认为可能是 CSRF 攻击而拒绝该请求。 3、在 HTTP 头中自定义属性并验证 这种方法也是使用 token 并进行验证和上一种方法不同的是这里并不是把 token 以参数的形式置于 HTTP 请求之中而是把它放到 HTTP 头中自定义的属性里。
2.5.4、SpringSecurity CSRF 的防御机制
完整类路径org.springframework.security.web.csrf.CsrfFilter作用csrf又称跨站请求伪造SpringSecurity会对所有post请求验证是否包含系统生成的csrf的token信息如果不包含则报错。起到防止csrf攻击的效果。(1. 生成token 2.验证token) 1、开启CSRF防护 //开启csrf防护, 可以设置哪些不需要防护
http.csrf().ignoringAntMatchers(/user/save); 2、页面需要添加token值 input typehidden th:name${_csrf.parameterName} th:value${_csrf.token}/
2.6、跨域与CORS
2.6.1、跨域 跨域实质上是浏览器的一种保护处理。如果产生了跨域服务器在返回结果时就会被浏览器拦截(注意此时请求是可以正常发起的只是浏览器对其进行了拦截)导致响应的内容不可用. 产生跨域的几种情况有一下
当前页面URL被请求页面URL是否跨域原因http://www.blnp.com/http://www.blnp.com/index.html否同源协议、域名、端口号等一致http://www.blnp.com/https://www.blnp.com/index.html跨域协议不同http/httpshttp://www.blnp.com/百度一下你就知道跨域主域名不同blnp/baiduhttp://www.blnp.com/http://kuale.blnp.com/跨域子域名不同www/kualehttp://www.blnp.com:8080http://www.blnp.com:8090跨域端口号不同8080/8090
2.6.2、解决跨域 1、JSONP 浏览器允许一些带src属性的标签跨域也就是在某些标签的src属性上写url地址是不会产生跨域问题。 2、CORS 解决跨域 CORS是一个W3C标准全称是跨域资源共享Cross-origin resource sharing。CORS需要浏览器和服务器同时支持。目前所有浏览器都支持该功能IE浏览器不能低于IE10。浏览器在发起真正的请求之前会发起一个OPTIONS类型的预检请求用于请求服务器是否允许跨域在得到许可的情况下才会发起请求。
2.6.3、SpringSecurity 的CORS 支持
1、声明跨域配置源
/*** 跨域配置信息源** return*/
public CorsConfigurationSource corsConfigurationSource() {CorsConfiguration corsConfiguration new CorsConfiguration();// 设置允许跨域的站点corsConfiguration.addAllowedOrigin(*);// 设置允许跨域的http方法corsConfiguration.addAllowedMethod(*);// 设置允许跨域的请求头corsConfiguration.addAllowedHeader(*);// 允许带凭证corsConfiguration.setAllowCredentials(true);// 对所有的url生效UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource();source.registerCorsConfiguration(/**, corsConfiguration);return source;
}
2、开启跨域支持
//允许跨域
http.cors().configurationSource(corsConfigurationSource());
3、前端跨域测试代码
function toCors() {$.ajax({// 默认情况下标准的跨域请求是不会发送cookie的xhrFields: {withCredentials: true},// 登录urlurl: http://localhost:8090/user/1,success: function(data) {alert(请求成功. data)}});
}
3、SpringSecurity 授权
3.1、简介
3.1.1、SpringSecurity 授权定义 说明安全权限控制问题其实就是控制能否访问url
3.1.2、SpringSecurity 授权原理 在我们应用系统里面,如果想要控制用户权限,需要有2部分数据。
系统配置信息数据:写着系统里面有哪些URL,每一个url拥有哪些权限才允许被访问。另一份数据就是用户权限信息请求用户拥有权限 系统用户发送一个请求:系统配置信息和用户权限信息作比对如果比对成功则允许访问。 当一个系统授权规则比较简单,基本不变时候系统的权限配置信息可以写在我们的代码里面去的。比如前台门户网站等权限比较单一,可以使用简单的授权配置即可完成,如果权限复杂, 例如办公OA, 电商后台管理系统等就不能使用写在代码里面了. 需要RBAC权限模型设计.
3.2、SpringSecurity 授权
3.2.1、内置权限表达式 Spring Security 使用Spring EL来支持主要用于Web访问和方法安全上, 可以通过表达式来判断是否具有访问权限. 下面是Spring Security常用的内置表达式. ExpressionUrlAuthorizationConfigurer 定义了所有的表达式
表达式说明permitAll指定任何人都允许访问。denyAll指定任何人都不允许访问anonymous指定匿名用户允许访问。rememberMe指定已记住的用户允许访问。authenticated指定任何经过身份验证的用户都允许访问,不包含 anonymousfullyAuthenticated指定由经过身份验证的用户允许访问,不包含 anonymous和rememberMehasRole(role)指定需要特定的角色的用户允许访问, 会自动在角色前面插入ROLE_hasAnyRole([role1,role2])指定需要任意一个角色的用户允许访问, 会自动在角色前面插入ROLE_hasAuthority(authority)指定需要特定的权限的用户允许访问hasAnyAuthority([authority,authority])指定需要任意一个权限的用户允许访问hasIpAddress(ip)指定需要特定的IP地址可以访问
3.2.2、URL安全表达式 基于web访问使用表达式保护url请求路径.
1、设置 URL 访问权限
// 设置/user/** 访问需要ADMIN角色
http.authorizeRequests().antMatchers(/user/**).hasRole(ADMIN);
// 设置/user/** 访问需要PRODUCT角色和IP地址为127.0.0.1 .hasAnyRole(PRODUCT,ADMIN)
http.authorizeRequests().antMatchers(/product/**).access(hasAnyRole(ADMIN,PRODUCT) and hasIpAddress(127.0.0.1));
// 设置自定义权限不足信息.
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
2、MyAccessDeniedHandler自定义权限不足类
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/*** 自定义权限不足信息*/
Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {Overridepublic void handle(HttpServletRequest httpServletRequest, HttpServletResponse resp, AccessDeniedException e) throws IOException,ServletException {resp.setStatus(HttpServletResponse.SC_FORBIDDEN);resp.setContentType(text/html;charsetUTF-8);resp.getWriter().write(权限不足请联系管理员!);}
}
3、设置用户对应的角色权限
// 先声明一个权限集合, 因为构造方法里面不能传入null
Collection GrantedAuthority authorities new ArrayList ();
if(admin.equalsIgnoreCase(user.getUsername())) {authorities.add(new SimpleGrantedAuthority(ROLE_ADMIN));
}
else {authorities.add(new SimpleGrantedAuthority(ROLE_PRODUCT));
}
3.2.3、Web 安全表达式引用自定义 Bean 授权
1、定义自定义授权类
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
/*** 自定义授权类*/
Component
public class MyAuthorizationService {/*** 检查用户是否有对应的访问权限** param authentication 登录用户* param request 请求对象* return*/public boolean check(Authentication authentication, HttpServletRequest request) {User user (User) authentication.getPrincipal();// 获取用户所有权限Collection GrantedAuthority authorities user.getAuthorities();// 获取用户名String username user.getUsername();// 如果用户名为admin,则不需要认证if(username.equalsIgnoreCase(admin)) {return true;}else {// 循环用户的权限, 判断是否有ROLE_ADMIN权限, 有返回truefor(GrantedAuthority authority: authorities) {String role authority.getAuthority();if(ROLE_ADMIN.equals(role)) {return true;}}}return false;}
}
2、配置类
//使用自定义Bean授权
http.authorizeRequests().antMatchers(/user/**).access(myAuthorizationService.check(authentication,request));
3、携带路径变量
/*** 检查用户是否有对应的访问权限** param authentication 登录用户* param request 请求对象* param id 参数ID* return*/
public boolean check(Authentication authentication, HttpServletRequest request, Integer id) {if(id 10) {return false;}return true;
}//使用自定义Bean授权,并携带路径参数
http.authorizeRequests().antMatchers(/user/delete/{id}).access(myAuthorizationService.check(authentication,request,#id));
3.2.4、Method 安全表达式 针对方法级别的访问控制比较复杂 spring security 提供了4种注解分别是
PreAuthorizePostAuthorizePreFilterPostFilter
1、开启方法级别的注解配置 在security配置类中添加注解
/*** Security配置类*/
Configuration
EnableGlobalMethodSecurity(prePostEnabled true) //开启注解支持
public class SecurityConfiguration extends WebSecurityConfigurerAdapter
2、在方法上使用注解 ProAuthorize: 注解适合进入方法前的权限验证 /*** 查询所有用户** return*/
RequestMapping(/findAll)
PreAuthorize(hasRole(ADMIN)) //需要ADMIN权限
public String findAll(Model model) {List User userList userService.list();model.addAttribute(userList, userList);return user_list;
}
/*** 用户修改页面跳转** return*/
RequestMapping(/update/{id})
PreAuthorize(#id10) //针对参数权限限定 id10可以访问
public String update(PathVariable Integer id, Model model) {User user userService.getById(id);model.addAttribute(user, user);return user_update;
} PostAuthorize PostAuthorize在方法执行后再进行权限验证适合验证带有返回值的权限 Spring EL 提供返回对象能够在表达式语言中获取到返回对象的 returnObject。
/*** 根据ID查询用户** return*/
GetMapping(/{id})
ResponseBody
PostAuthorize(returnObject.username authentication.principal.username ) //判断查询用户信息是否是当前登录用户信息.否则没有权限
public User getById(PathVariable Integer id) {User user userService.getById(id);return user;
} PreFilter 可以用来对集合类型的参数进行过滤, 将不符合条件的元素剔除集合。
/*** 商品删除-多选删除** return*/
GetMapping(/delByIds)
PreFilter(filterTarget ids, value filterObject%20) //剔除参数为基数的值
public String delByIds(RequestParam(value id) List Integer ids) {for(Integer id: ids) {System.out.println(id);}return redirect:/user/findAll;
} PostFilter 可以用来对集合类型的返回值进行过滤, 将不符合条件的元素剔除集合。
/*** 查询所有用户-返回json数据** return*/
RequestMapping(/findAllTOJson)
ResponseBody
PostFilter(filterObject.id%20) //剔除返回值ID为偶数的值
public List User findAllTOJson() {List User userList userService.list();return userList;
}
3.3、 基于数据库的RBAC数据模型的权限控制 我们开发一个系统必然面临权限控制的问题不同的用户具有不同的访问、操作、数据权限。形成理论的权限控制模型有自主访问控制DAC: Discretionary Access Control、强制访问控制MAC: Mandatory Access Control、基于属性的权限验证ABAC: Attribute-Based Access Control等。最常被开发者使用也是相对易用、通用的就是RBAC权限模型Role-Based Access Control。
3.3.1、RBAC 权限模型简介 RBAC权限模型Role-Based Access Control即基于角色的权限控制。模型中有几个关键的术语
用户系统接口及访问的操作者权限能够访问某接口或者做某操作的授权资格角色具有一类相同操作权限的总称 RBAC权限模型核心授权逻辑如下
某用户是什么角色某角色具有什么权限通过角色对应的权限推导出用户的权限
3.3.2、RBAC 的演化
1、用户与权限直接关联 想到权限控制人们最先想到的一定是用户与权限直接关联的模式简单地说就是某个用户具有某些权限。如图
张三具有所有权限他可能是一个超级管理员.李四,王五 具有添加商品和审核商品的权限有可能是一个普通业务员 这种模型能够清晰的表达用户与权限之间的关系足够简单。但同时也存在问题
现在用户是张三、李四王五以后随着人员增加每一个用户都需要重新授权操作人员的他的权限发生变更后,需要对每个一个用户重新授予新的权限
2、用户与角色关联 这样只需要维护角色和权限之间的关系就可以了. 如果业务员的权限发生变更, 只需要变动业务员角色和权限之前的关系进行维护就可以了. 用户和权限就分离开来了. 如下图 3.3.3、基于 RBAC 设计权限表结构
一个用户有一个或多个角色一个角色包含多个用户一个角色有多种权限一个权限属于多个角色 3.3.4、基于SpringSecurity 实现RBAC权限管理
1、动态查询数据库中用户对应的权限
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lagou.domain.Permission;
import org.apache.ibatis.annotations.Select;
import java.util.List;
public interface PermissionMapper extends BaseMapper Permission {/*** 根据用户ID查询权限** param id* return*/Select(SELECT p.* FROM t_permission p,t_role_permission rp,t_roler, t_user_role ur, t_user u WHERE p.id rp.PID AND rp.RID r.id AND r.id ur.RID AND ur.UID u.id AND u.id #{id})List Permission findByUserId(Integer id);
}
2、给登录用户授权
// 先声明一个权限集合, 因为构造方法里面不能传入null
Collection GrantedAuthority authorities new ArrayList ();
// 查询用户对应所有权限
List Permission permissions permissionService.findByUserId(user.getId());
for(Permission permission: permissions) {// 授权authorities.add(new SimpleGrantedAuthority(permission.getPermissionTag()));
}
3、设置访问权限
// 查询数据库所有权限列表
List Permission permissions permissionService.list();
for(Permission permission: permissions) {//添加请求权限http.authorizeRequests().antMatchers(permission.getPermissionUrl()).hasAuthority(permission.getPermissionTag());
}
3.4、基于页面标签的权限控制 在jsp页面或者thymeleaf模板页面中我们可以使用spring security提供的权限标签来进行权限控制.要想使用thymeleaf为SpringSecurity提供的标签属性首先需要引入thymeleaf-extras-springsecurity依赖支持。 引入依赖 !--添加thymeleaf为SpringSecurity提供的标签 依赖 --
dependencygroupIdorg.thymeleaf.extras/groupIdartifactIdthymeleaf-extras-springsecurity5/artifactIdversion3.0.4.RELEASE/version
/dependency 声明使用 !DOCTYPE html
html xmlns:thhttp://www.thymeleaf.org xmlns:sechttp://www.thymeleaf.org/extras/spring-security
3.4.1、常用SpringSecurity 标签属性
sec:authorizeisAuthenticated() 判断用户是否已经登陆认证引号内的参数必须是isAuthenticated()。
sec:authentication“name” 获得当前用户的用户名引号内的参数必须是name。
sec:authorize“hasRole(‘role’)” 判断当前用户是否拥有指定的权限。引号内的参数为权限的名称
3.4.2、标签使用
div classleftnavdiv classleftnav-titlediv sec:authorizeisAuthenticated()span sec:authenticationname/spanimg srcimages/y.jpg classradius-circle rotate-hover height50alt //div/divdiv sec:authorizehasAuthority(user:findAll)h2span classicon-user/span系统管理/h2ul styledisplay:blocklia href/user/findAll targetrightspan classicon-caretright/ span用户管理/a/lilia hrefjavascript:void(0) onclicktoCors() targetrightspan classicon-caret-right/span跨域测试/a/li/ul/divdiv sec:authorizehasAuthority(product:findAll)h2span classicon-pencil-square-o/span数据管理/h2ullia href/product/findAll targetrightspan classiconcaret-right/span商品管理/a/li/ul/div
/div
4、源码剖析
4.1、过滤器链加载源码
4.1.1、过滤器链加载流程分析 4.1.2、源码分析
1、配置信息
# 安全过滤器自动配置
org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration
2、SecurityFilterAutoConfiguration类
Configuration(proxyBeanMethods false)
ConditionalOnWebApplication(type Type.SERVLET)
EnableConfigurationProperties(SecurityProperties.class) // Security配置类
ConditionalOnClass({AbstractSecurityWebApplicationInitializer.class,SessionCreationPolicy.class
})
AutoConfigureAfter(SecurityAutoConfiguration.class) // 这个类加载完后去加载SecurityAutoConfiguration配置
public class SecurityFilterAutoConfiguration {//.....
}
3、SecurityAutoConfiguration类
Configuration(proxyBeanMethods false)
ConditionalOnClass(DefaultAuthenticationEventPublisher.class)
EnableConfigurationProperties(SecurityProperties.class)
Import({SpringBootWebSecurityConfiguration.class,//web安全启用配置WebSecurityEnablerConfiguration.class, SecurityDataConfiguration.class
})
public class SecurityAutoConfiguration {//.....
}
4、WebSecurityEnablerConfiguration类
Configuration(proxyBeanMethods false)
ConditionalOnBean(WebSecurityConfigurerAdapter.class)
ConditionalOnMissingBean(name BeanIds.SPRING_SECURITY_FILTER_CHAIN)
ConditionalOnWebApplication(type ConditionalOnWebApplication.Type.SERVLET)
EnableWebSecurity
public class WebSecurityEnablerConfiguration {}
Retention(value java.lang.annotation.RetentionPolicy.RUNTIME)
Target(value {java.lang.annotation.ElementType.TYPE
})
Documented
Import({WebSecurityConfiguration.class,SpringWebMvcImportSelector.class,OAuth2ImportSelector.class
})
EnableGlobalAuthentication
Configuration
public interface EnableWebSecurity {/*** Controls debugging support for Spring Security. Default is false.* return if true, enables debug support with Spring Security*/boolean debug() default false;
} EnableWebSecurity注解有两个作用
加载了WebSecurityConfiguration配置类, 配置安全认证策略加载了AuthenticationConfiguration, 配置了认证信息
5、WebSecurityConfiguration类
// springSecurity过滤器链声明
Bean(name AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain() throws Exception {boolean hasConfigurers webSecurityConfigurers ! null !webSecurityConfigurers.isEmpty();if(!hasConfigurers) {WebSecurityConfigurerAdapter adapter objectObjectPostProcessor.postProcess(new WebSecurityConfigurerAdapter() {});webSecurity.apply(adapter);}return webSecurity.build(); //构建filter
}
4.2、认证流程源码
4.2.1、认证流程分析 在整个过滤器链中, UsernamePasswordAuthenticationFilter是来处理整个用户认证的流程的, 所以下面我们主要针对用户认证来看下源码是如何实现的? 4.2.2、源码跟踪 UsernamePasswordAuthenticationFilter public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {//1.检查是否是post请求if(postOnly !request.getMethod().equals(POST)) {throw new AuthenticationServiceException(Authentication method not supported: request.getMethod());}//2.获取用户名和密码String username obtainUsername(request);String password obtainPassword(request);if(username null) {username ;}if(password null) {password ;}username username.trim();//3.创建AuthenticationToken,此时是未认证的状态UsernamePasswordAuthenticationToken authRequest newUsernamePasswordAuthenticationToken(username, password);// Allow subclasses to set the details propertysetDetails(request, authRequest);//4.调用AuthenticationManager进行认证.return this.getAuthenticationManager().authenticate(authRequest);
} UsernamePasswordAuthenticationToken public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {super(null);this.principal principal; //设置用户名this.credentials credentials; //设置密码setAuthenticated(false); //设置认证状态为-未认证
} AuthenticationManager--ProviderManager--AbstractUserDetailsAuthenticationProvider public Authentication authenticate(Authentication authentication)
throws AuthenticationException {Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,() - messages.getMessage(AbstractUserDetailsAuthenticationProvider.onlySupports, Only UsernamePasswordAuthenticationToken is supported ));// 1.获取用户名String username (authentication.getPrincipal() null) ? NONE_PROVIDED : authentication.getName();// 2.尝试从缓存中获取boolean cacheWasUsed true;UserDetails user this.userCache.getUserFromCache(username);if(user null) {cacheWasUsed false;try {//3.检索Useruser retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);}//.....}try {//4. 认证前检查user状态preAuthenticationChecks.check(user);//5. 附加认证证检查additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);}//.....//6. 认证后检查user状态postAuthenticationChecks.check(user);//.....// 7. 创建认证成功的UsernamePasswordAuthenticationToken并将认证状态设置为truereturn createSuccessAuthentication(principalToReturn, authentication, user);
} retrieveUser方法 protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {prepareTimingAttackProtection();try {//调用自定义UserDetailsService的loadUserByUsername的方法UserDetails loadedUser this.getUserDetailsService().loadUserByUsername(username);if(loadedUser null) {throw new InternalAuthenticationServiceException(UserDetailsService returned null, which is an interface contract violation );}return loadedUser;}//....
} additionalAuthenticationChecks方法 protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {//.....// 1.获取前端密码String presentedPassword authentication.getCredentials().toString();// 2.与数据库中的密码进行比对if(!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {logger.debug(Authentication failed: password does not match stored value );throw new BadCredentialsException(messages.getMessage(AbstractUserDetailsAuthenticationProvider.badCredentials, Bad credentials));}
} AbstractAuthenticationProcessingFilter--doFilter方法 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {//.....Authentication authResult;try {//1.调用子类方法authResult attemptAuthentication(request, response);//...//2.session策略验证sessionStrategy.onAuthentication(authResult, request, response);}//....// 3.成功身份验证successfulAuthentication(request, response, chain, authResult);
} successfulAuthentication方法 protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {//....// 1.将认证的用户放入SecurityContext中SecurityContextHolder.getContext().setAuthentication(authResult);// 2.检查是不是记住我rememberMeServices.loginSuccess(request, response, authResult);...// 3.调用自定义MyAuthenticationService的onAuthenticationSuccess方法successHandler.onAuthenticationSuccess(request, response, authResult);
}
4.3、记住我流程源码
4.3.1、流程分析 4.3.2、源码跟踪 AbstractAuthenticationProcessingFilter--successfulAuthentication方法 protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {//....// 1.将认证的用户放入SecurityContext中SecurityContextHolder.getContext().setAuthentication(authResult);// 2.检查是不是记住我rememberMeServices.loginSuccess(request, response, authResult);//...// 3.调用自定义MyAuthenticationService的onAuthenticationSuccess方法successHandler.onAuthenticationSuccess(request, response, authResult);
} loginSuccess方法--onLoginSuccess protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {// 1.获取用户名String username successfulAuthentication.getName();// 2.创建persistentTokenPersistentRememberMeToken persistentToken new PersistentRememberMeToken(username, generateSeriesData(), generateTokenData(), new Date());try {// 3. 插入数据库tokenRepository.createNewToken(persistentToken);// 4. 写入浏览器cookieaddCookie(persistentToken, request, response);}catch (Exception e) {logger.error(Failed to save persistent token , e);}
} RememberMeAuthenticationFilter public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {HttpServletRequest request (HttpServletRequest) req;HttpServletResponse response (HttpServletResponse) res;if(SecurityContextHolder.getContext().getAuthentication() null) {// 1. 检查是否是记住我登录. 如果是完成自动登录Authentication rememberMeAuth rememberMeServices.autoLogin(request, response);if(rememberMeAuth ! null) {try {// 2.调用authenticationManager再次认证rememberMeAuth authenticationManager.authenticate(rememberMeAuth);// 3.将认证的用户在重新放入SecurityContext中SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);//......}//......}//.....// 4.调用下一个过滤器chain.doFilter(request, response);}
} autoLogin方法 public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {// 1.获取rememberMeCookieString rememberMeCookie extractRememberMeCookie(request);// 2.检查是否存在if(rememberMeCookie null) {return null;}//.....UserDetails user null;try {// 3.解码CookieString[] cookieTokens decodeCookie(rememberMeCookie);// 4.根据cookie完成自动登录user processAutoLoginCookie(cookieTokens, request, response);// 5.检查user状态userDetailsChecker.check(user);logger.debug(Remember-me cookie accepted);// 6.创建认证成功的RememberMeAuthenticationToken并将认证状态设置为truereturn createSuccessfulAuthentication(request, user);}//.....return null;
} processAutoLoginCookie方法 protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {//....// 1.获取系列码和tokenfinal String presentedSeries cookieTokens[0];final String presentedToken cookieTokens[1];// 2.根据token去数据库中查询PersistentRememberMeToken token tokenRepository.getTokenForSeries(presentedSeries);.......// 3.在创建一个新的tokenPersistentRememberMeToken newToken new PersistentRememberMeToken(token.getUsername(), token.getSeries(), generateTokenData(), new Date());try {// 4.修改数据库token信息tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());// 5.写入浏览器addCookie(newToken, request, response);}// 6.根据用户名调用UserDetailsService查询UserDetailreturn getUserDetailsService().loadUserByUsername(token.getUsername());
}
4.4、CSRF流程源码
4.4.1、流程分析 4.4.2、源码跟踪 CsrfFilter protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {request.setAttribute(HttpServletResponse.class.getName(), response);// 1.取出tokenCsrfToken csrfToken this.tokenRepository.loadToken(request);final boolean missingToken csrfToken null;if(missingToken) {// 2. 如果没有token,就重新生成tokencsrfToken this.tokenRepository.generateToken(request);this.tokenRepository.saveToken(csrfToken, request, response);}// 3. 将csrfToken值放入request域中request.setAttribute(CsrfToken.class.getName(), csrfToken);request.setAttribute(csrfToken.getParameterName(), csrfToken);// 4. 匹配请求是否为post请求,不是则放行if(!this.requireCsrfProtectionMatcher.matches(request)) {filterChain.doFilter(request, response);return;}String actualToken request.getHeader(csrfToken.getHeaderName());if(actualToken null) {// 5.从request请求参数中取出csrfTokenactualToken request.getParameter(csrfToken.getParameterName());}// 6.匹配两个token是否相等.if(!csrfToken.getToken().equals(actualToken)) {if(this.logger.isDebugEnabled()) {this.logger.debug(Invalid CSRF token found for UrlUtils.buildFullRequestUrl(request));}if(missingToken) {this.accessDeniedHandler.handle(request, response, new MissingCsrfTokenException(actualToken));}else {this.accessDeniedHandler.handle(request, response, new InvalidCsrfTokenException(csrfToken, actualToken));}return;}// 7. 如果相等则放行filterChain.doFilter(request, response);
}
4.5、授权流程源码
4.5.1、流程分析 AffirmativeBased基于肯定的逻辑是: 一票通过权ConsensusBased基于共识的逻辑是: 赞成票多于反对票则表示通过,反对票多于赞成票则将抛出 AccessDeniedExceptionUnanimousBased基于一致的逻辑: 一票否决权
4.5.2、源码跟踪 FilterSecurityInterceptor public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {FilterInvocation fi new FilterInvocation(request, response, chain);// 调用invoke(fi);
}public void invoke(FilterInvocation fi) throws IOException, ServletException {if((fi.getRequest() ! null).....}else {//...//前置调用InterceptorStatusToken token super.beforeInvocation(fi);//....// 后置调用super.afterInvocation(token, null);}
} AbstractSecurityInterceptor的beforeInvocation方法 protected InterceptorStatusToken beforeInvocation(Object object) {// 1. 获取security的系统配置权限Collection ConfigAttribute attributes this.obtainSecurityMetadataSource().getAttributes(object);//.....// 2. 获取用户认证的信息Authentication authenticated authenticateIfRequired();// Attempt authorizationtry {// 3.调用决策管理器this.accessDecisionManager.decide(authenticated, object, attributes);}catch (AccessDeniedException accessDeniedException) {publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, accessDeniedException));// 4.无权限则抛出异常让ExceptionTranslationFilter捕获throw accessDeniedException;}//....
} AffirmativeBased的decide方法 public void decide(Authentication authentication, Object object, Collection ConfigAttribute configAttributes) throws
AccessDeniedException {int deny 0;for(AccessDecisionVoter voter: getDecisionVoters()) {int result voter.vote(authentication, object, configAttributes);if(logger.isDebugEnabled()) {logger.debug(Voter: voter , returned: result);}switch(result) {//一票通过只要有一个投票器通过就允许访问case AccessDecisionVoter.ACCESS_GRANTED:return;case AccessDecisionVoter.ACCESS_DENIED:deny;break;default:break;}}if(deny 0) {throw new AccessDeniedException(messages.getMessage(AbstractAccessDecisionManager.accessDenied, Access isdenied));}//...
} ExceptionTranslationFilter public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {HttpServletRequest request (HttpServletRequest) req;HttpServletResponse response (HttpServletResponse) res;try {// 1.调用下一个过滤器即FilterSecurityInterceptorchain.doFilter(request, response);logger.debug(Chain processed normally);}catch (IOException ex) {throw ex;}catch (Exception ex) {// 2.捕获FilterSecurityInterceptor并判断异常类型Throwable[] causeChain throwableAnalyzer.determineCauseChain(ex);RuntimeException ase (AuthenticationException)throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);if(ase null) {ase (AccessDeniedException)throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);}if(ase ! null) {if(response.isCommitted()) {throw new ServletException(Unable to handle the Spring Security Exception because the response is already committed. , ex);}// 3.如果是AccessDeniedException异常则处理Spring Security异常handleSpringSecurityException(request, response, chain, ase);}//.....}} handleSpringSecurityException方法 private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception)
throws IOException, ServletException {if(exception instanceof AuthenticationException) {//...}else if(exception instanceof AccessDeniedException) {Authentication authentication SecurityContextHolder.getContext().getAuthentication();if(authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {//.....}else {logger.debug(Access is denied (user is not anonymous); delegating to AccessDeniedHandler , exception);//如果是AccessDeniedException异常则调用AccessDeniedHandler的handle方法accessDeniedHandler.handle(request, response,(AccessDeniedException) exception);}}
}