技术标签: 后端
SpringSecurity是Spring家族的一个安全性框架。相比与Shiro,它提供了更多的功能。
一般来说中大型项目都使用SpringSecurity作为安全框架。小项目用Shiro比较多,因为Shiro相对于SpringSecurity更加容易上手。
一般Web应用需要认证和授权。
认证:验证访问系统的是不是本系统的用户,而且要确认具体是哪个用户。
授权:经过认证后,如果该用户想访问该系统资源,要判断是否有权限访问。
在SpringSecurity初始化时会注入一个名为SpringSecurityFilterChain的
Servlet过滤器,类型为FilterChainProxy,FilterChainProxy中包含许多过滤器,这些过滤器组成一条过滤链,去进行请求的过滤。
直接在springboot项目中引入他的依赖即可
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
认证过程:
登录和校验
登录:
1、自定义登录接口
调用ProviderManager的方法进行认证,如果认证通过生成token
把用户信息存入Redis中
2、自定义UserDetailService
在这个接口的实现类中查询数据库
校验:
1、定义Jwt认证过滤器
获取token
解析token获取其中的userid
从redis中获取用户信息
存入SecurityContextHolder
退出登录:
1、获取SecurityContextHolder中的用户id
2、删除redis中的数据
1、设置访问资源所需权限
2、封装权限信息
3、从数据库查询权限信息
UsernamePasswordAuthenticationFilter是认证过程开始的地方,它继承了AbstractAuthenticationProcessingFilter,UsernamePasswordAuthenticationFilter本身没有doFilter方法,它调用的是其父类的AbstractAuthenticationProcessingFilter的doFilter
AbstractAuthenticationProcessingFilter的doFilter方法
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 判断请求是否需要认证
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
//调用UsernamePasswordAuthenticationFilter的attemptAuthentication方法
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
// return immediately as subclass has indicated that it hasn't completed
return;
}
// 配置session策略
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) {
// Authentication failed
// 认证异常调用
unsuccessfulAuthentication(request, response, ex);
}
}
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
// 认证成功,将用户信息存入SecurityContextHolder
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authResult);
SecurityContextHolder.setContext(context);
this.securityContextRepository.saveContext(context, request, response);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
}
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
SecurityContextHolder.clearContext();
this.logger.trace("Failed to process authentication request", failed);
this.logger.trace("Cleared SecurityContextHolder");
this.logger.trace("Handling authentication failure");
this.rememberMeServices.loginFail(request, response);
this.failureHandler.onAuthenticationFailure(request, response, failed);
}
UsernamePasswordAuthenticationFilter的attemptAuthentication方法
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
// 判断请求是否属于POST
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
// 获取表单提交的用户名密码
String username = obtainUsername(request);
username = (username != null) ? username.trim() : "";
String password = obtainPassword(request);
password = (password != null) ? password : "";
// 将用户名密码封装成Authentication对象
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
// 调用ProviderManager的authenticate方法
return this.getAuthenticationManager().authenticate(authRequest);
}
ProviderManager的authenticate方法
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 获取Authentication对象
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
// 使用DaoAuthenticationProvider去进行匹配
int size = this.providers.size();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
try {
result = provider.authenticate(authentication);
if (result != null) {
// 认证信息复制到authentication中
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw ex;
}
catch (AuthenticationException ex) {
lastException = ex;
}
}
if (result == null && this.parent != null) {
// Allow the parent to try.
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
}
catch (ProviderNotFoundException ex) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException ex) {
parentException = ex;
lastException = ex;
}
}
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful then it
// will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent
// AuthenticationManager already published it
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
new Object[] {
toTest.getName() }, "No AuthenticationProvider found for {0}"));
}
// If the parent AuthenticationManager was attempted and failed then it will
// publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the
// parent AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
DaoAuthenticationProvider中无authenticate,调用DaoAuthenticationProvider的父类AbstractUserDetailsAuthenticationProvider的authenticate方法
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
String username = determineUsername(authentication);
// 第一次发请求还未登录,缓存中无数据
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
// 调用DaoAuthenticationProvider的retrieveUser获取需对比的UserDetails信息
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException ex) {
this.logger.debug("Failed to find user '" + username + "'");
if (!this.hideUserNotFoundExceptions) {
throw ex;
}
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
// 检查上面获取的用户状态是否正常
this.preAuthenticationChecks.check(user);
// 比对user和表单提交的认证信息(密码比对)
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException ex) {
if (!cacheWasUsed) {
throw ex;
}
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
// 判断身份凭证码有无过期
this.postAuthenticationChecks.check(user);
// 将user存进缓存,方便下次获取
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
授权主要是通过投票策略去执行,Spring Security内置了三个基于投票的实现类,分别是
AffirmativeBased,ConsensusBasesd和UnanimaousBased。
AffirmativeBased是Spring Security默认使用的投票方式,他的逻辑是只要有一 个投票通过,就表示通过。
1、只要有一个投票通过了,就表示通过。
2、如果全部弃权也表示通过。
3、如果没有人投赞成票,但是有人投反对票,则抛出AccessDeniedException.
ConsensusBased的逻辑是:多数赞成就通过 1、如果赞成票多于反对票则表示通过
2、如果反对票多于赞成票则抛出AccessDeniedException
3、如果赞成票与反对票相同且不等于0,并且属性allowIfEqualGrantedDeniedDecisions的值为true,则表示通过,否则抛出
异常AccessDeniedException。参数allowIfEqualGrantedDeniedDecisions的值默认 是true。
4、如果所有的AccessDecisionVoter都弃权了,则将视参数allowIfAllAbstainDecisions的值而定,如果该值为true则表示通过,> 否则将抛出异常AccessDeniedException。参数allowIfAllAbstainDecisions的值默认为false。
UnanimousBased相当于一票否决。
1、如果受保护对象配置的某一个ConfifigAttribute被任意的AccessDecisionVoter反对了,则将抛出AccessDeniedException。
2、如果没有反对票,但是有赞成票,则表示通过。
3、如果全部弃权了,则将视参数allowIfAllAbstainDecisions的值而定,true则通过,false则抛出AccessDeniedException。
主要是AbstractAccessDecisionManager的子类AffirmativeBased的decide方法去做
一共有三种投票结果:支持、弃权、反对
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);
switch (result) {
// 支持
case AccessDecisionVoter.ACCESS_GRANTED:
return;
// 反对
case AccessDecisionVoter.ACCESS_DENIED:
deny++;
break;
// 弃权
default:
break;
}
}
// 反对票>0表示无权限
if (deny > 0) {
throw new AccessDeniedException(
this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
// To get this far, every AccessDecisionVoter abstained
checkAllowIfAllAbstainDecisions();
}
文章浏览阅读950次。https://www.micro-ip.com/drchip.php?mode=2&cid=17_这个命令可以创建符合要求的library cell集合,可以将它传递给另一个命令
文章浏览阅读3.1k次。#Elasticsearch安装这里本人使用的是docker镜像安装,至于怎么安装就不说了,贴一下配置最基本的配置文件就好#集群名称,默认为elasticsearch, 命名规则为 es-产品名-ES版本cluster.name: luckyqing#节点名称,es启动时会自动创建节点名称,但你也可进行配置node.name: es-46-68-76#设置索引的分片数#index..._lorg/elasticsearch/common/settings/settings;ljava/lang/string;)v
文章浏览阅读5k次。在sql server,校验数据是否为数字型比较容易,有提供的方法可以使用:ISNUMERIC当是数字型,方法返回值为1;否则返回值为0例:select * from tablename where isnumeric(data) = 1在Oracle中没有这样的方法,要实现判断是否为数字型需要自己写一个方法来实现。但也可以用其它方法:方法一,用正则的方法:使用 regexp_like例:sele..._oracle判断是否是数字,包括小数
文章浏览阅读591次。最新项目游戏感用于SteelSeries GameSense 3.8.x+的Python库安装这个包有两个不同的版本。一个支持普通的同步函数调用,另一个支持python的异步功能。要安装同步版本,请运 ...2021-03-02已阅读: n次此模块验证事件模块中的电话号码,就像base_phone合作伙伴表单中的模块有效电话号码。请参考有关详细信息,请参阅base_phone模块的说明。此模块独立..._python event
文章浏览阅读87次。与之前的微博分享相比,微信分享就没有那么容易了——微信官方的 SDK 太差劲了。文章也写得像一坨屎——因为文档里的代码都是截图的。。。微信分享 SDK 接入按照官方的指南,添加对应的依赖dependencies{compile'com.tencent.mm.opensdk:wechat-sdk-android-with-mta:+'}对应的权限:相关的混淆代码配置:-keepclas..._wxapifactory.createwxapi.sendreq
文章浏览阅读493次。2013: 寻找zcmuTime Limit: 1 Sec Memory Limit: 128 MBSubmit: 466 Solved: 75[Submit][Status][Web Board]DescriptionInput 多组数据 每组数据包含一个字符串 1 Output 输出一个整数表示最少需要删除的字符数,_2013: 寻找zcmu
文章浏览阅读143次。欢迎进入Oracle社区论坛,与200万技术人员互动交流 >>进入 当然最常用的是用rowid去除重复: 查出重复数据: select a.rowid,a.* from 表名 a where a.rowid != ( select max(b.rowid) from 表名 b where a.字段1 = b.字段1 and a.字段2 = b.字段2 ) 删欢迎进入Oracle社区论坛..._oracle 中 每行 数据的 标记
文章浏览阅读230次。ConfigMgr Prerequisites Tool 使用指南系列-1:ConfigMgr Prerequisites Tool简介Lander Zhang 专注外企按需IT基础架构运维服务,IT Helpdesk 实战培训践行者博客:https://blog.51cto.com/lander IT Helpdesk实战培训视频课程:https://edu.51cto.com/lectur..._configmgr tool
文章浏览阅读2.8k次。 这个一直不能用:for id in data_front: res=es.update(index=index, doc_type=doc_type, id=id, body={"doc": {"flag":0}}) print(id,res) break {'total': 0, 'successful'..._es updaterequest没有更新
文章浏览阅读741次。动态加载模块到内核中1、相关的结构体struct kernel_symbolstruct modversion_infostruct module_attributestructmodule_version_attributestruct module_kobjectstruct module// 模块结构体,将上面的变量放到结构体中,有的值进行了初始化2、加载、卸载、以及注册、注销等函数..._module_firmware
文章浏览阅读441次。参考自:https://dart.cn/samplesDart 编程语言概览本文向你展示的 Dart 语言用法并不全面—这里只是对那些喜欢通过示例了解语言的人提供一个简单的介绍。你也许会对Dart 语言的速查表 CodeLab或 Dart 语言概览和库概览更感兴趣。语言概览包含示例的 Dart 语言全面概览。本文中大部分的阅读更多链接均会跳转到此概览中。库概览通过各种示例向你介绍 Dart 的核心库。通过此概览你可以了解更多关于内置类型、集合、日期时间、异步 Stream..._dart语言实战
文章浏览阅读1k次。解决方案:注意文件名的编码解决过程:在用 Conan 编译 C++过程中遇到报错"‘ascii’ codec can’t decode byte 0xe5 in position 36: ordinal not in range(128)",困扰多时,问题如下:***/1.5935.9@***/stable: Retrieving package 6d3344e2e47d9a7babae..._ascii' codec can't decode byte 0xe5 in position 3: ordinal not in range(128