SpringSecurity学习(Springboot+SpringSecurity+Jwt)_spring boot + spring security + jwt-程序员宅基地

技术标签: 后端  

SpringSecurity

一、什么是SpringSecurity

SpringSecurity是Spring家族的一个安全性框架。相比与Shiro,它提供了更多的功能。

一般来说中大型项目都使用SpringSecurity作为安全框架。小项目用Shiro比较多,因为Shiro相对于SpringSecurity更加容易上手。
一般Web应用需要认证授权
认证:验证访问系统的是不是本系统的用户,而且要确认具体是哪个用户。
授权:经过认证后,如果该用户想访问该系统资源,要判断是否有权限访问。

在SpringSecurity初始化时会注入一个名为SpringSecurityFilterChain
Servlet过滤器,类型为FilterChainProxy,FilterChainProxy中包含许多过滤器,这些过滤器组成一条过滤链,去进行请求的过滤。
在这里插入图片描述

二、SpringSecurity流程

摘自B站三更草堂视频

三、如何引入SpringSecurity

直接在springboot项目中引入他的依赖即可

<dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-security</artifactId>
</dependency>

四、认证

认证过程:
摘自B站三更草堂视频
摘自B站三更草堂视频
摘自B站三更草堂视频

登录和校验
登录:
1、自定义登录接口
  调用ProviderManager的方法进行认证,如果认证通过生成token
  把用户信息存入Redis中
2、自定义UserDetailService
  在这个接口的实现类中查询数据库

校验:
1、定义Jwt认证过滤器
  获取token
  解析token获取其中的userid
  从redis中获取用户信息
  存入SecurityContextHolder

退出登录:
1、获取SecurityContextHolder中的用户id
2、删除redis中的数据

五、授权

1、设置访问资源所需权限
2、封装权限信息
3、从数据库查询权限信息

六、源码

1、认证

在这里插入图片描述

UsernamePasswordAuthenticationFilter是认证过程开始的地方,它继承了AbstractAuthenticationProcessingFilterUsernamePasswordAuthenticationFilter本身没有doFilter方法,它调用的是其父类的AbstractAuthenticationProcessingFilterdoFilter

AbstractAuthenticationProcessingFilterdoFilter方法

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

UsernamePasswordAuthenticationFilterattemptAuthentication方法

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

ProviderManagerauthenticate方法

	@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的父类AbstractUserDetailsAuthenticationProviderauthenticate方法

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

2、授权

在这里插入图片描述
授权主要是通过投票策略去执行,Spring Security内置了三个基于投票的实现类,分别是
AffirmativeBased,ConsensusBasesdUnanimaousBased

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的子类AffirmativeBaseddecide方法去做
一共有三种投票结果:支持、弃权、反对
在这里插入图片描述
AffirmativeBaseddecide方法

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();
	}

七、代码

代码

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_43636205/article/details/124154145

智能推荐

【PrimeTime 基本命令】_这个命令可以创建符合要求的library cell集合,可以将它传递给另一个命令-程序员宅基地

文章浏览阅读950次。https://www.micro-ip.com/drchip.php?mode=2&cid=17_这个命令可以创建符合要求的library cell集合,可以将它传递给另一个命令

ELasticsearch的安装以及与spring-data-elasticsearch的整合使用_lorg/elasticsearch/common/settings/settings;ljava/-程序员宅基地

文章浏览阅读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

oracle 判断是否为小数,Oracle 判断是否为数字型(金额型)-程序员宅基地

文章浏览阅读5k次。在sql server,校验数据是否为数字型比较容易,有提供的方法可以使用:ISNUMERIC当是数字型,方法返回值为1;否则返回值为0例:select * from tablename where isnumeric(data) = 1在Oracle中没有这样的方法,要实现判断是否为数字型需要自己写一个方法来实现。但也可以用其它方法:方法一,用正则的方法:使用 regexp_like例:sele..._oracle判断是否是数字,包括小数

python中的event_Python event-程序员宅基地

文章浏览阅读591次。最新项目游戏感用于SteelSeries GameSense 3.8.x+的Python库安装这个包有两个不同的版本。一个支持普通的同步函数调用,另一个支持python的异步功能。要安装同步版本,请运 ...2021-03-02已阅读: n次此模块验证事件模块中的电话号码,就像base_phone合作伙伴表单中的模块有效电话号码。请参考有关详细信息,请参阅base_phone模块的说明。此模块独立..._python event

android 大前端,大前端 Android 开发日记 10:微信分享-程序员宅基地

文章浏览阅读87次。与之前的微博分享相比,微信分享就没有那么容易了——微信官方的 SDK 太差劲了。文章也写得像一坨屎——因为文档里的代码都是截图的。。。微信分享 SDK 接入按照官方的指南,添加对应的依赖dependencies{compile'com.tencent.mm.opensdk:wechat-sdk-android-with-mta:+'}对应的权限:相关的混淆代码配置:-keepclas..._wxapifactory.createwxapi.sendreq

寻找zcmu(upper_bound算法)_2013: 寻找zcmu-程序员宅基地

文章浏览阅读493次。2013: 寻找zcmuTime Limit: 1 Sec Memory Limit: 128 MBSubmit: 466 Solved: 75[Submit][Status][Web Board]DescriptionInput 多组数据 每组数据包含一个字符串 1 Output 输出一个整数表示最少需要删除的字符数,_2013: 寻找zcmu

随便推点

oracle行标识是什么,oraclerowid在表行中的物理标识-程序员宅基地

文章浏览阅读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 中 每行 数据的 标记

CMPT1.ConfigMgr Prerequisites Tool简介-程序员宅基地

文章浏览阅读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

elasticsearch update 无结果_es updaterequest没有更新-程序员宅基地

文章浏览阅读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没有更新

linux/module.h_module_firmware-程序员宅基地

文章浏览阅读741次。动态加载模块到内核中1、相关的结构体struct kernel_symbolstruct modversion_infostruct module_attributestructmodule_version_attributestruct module_kobjectstruct module// 模块结构体,将上面的变量放到结构体中,有的值进行了初始化2、加载、卸载、以及注册、注销等函数..._module_firmware

『Flutter开发实战』十分钟入门Dart语言_dart语言实战-程序员宅基地

文章浏览阅读441次。参考自:https://dart.cn/samplesDart 编程语言概览本文向你展示的 Dart 语言用法并不全面—这里只是对那些喜欢通过示例了解语言的人提供一个简单的介绍。你也许会对Dart 语言的速查表 CodeLab或 Dart 语言概览和库概览更感兴趣。语言概览包含示例的 Dart 语言全面概览。本文中大部分的阅读更多链接均会跳转到此概览中。库概览通过各种示例向你介绍 Dart 的核心库。通过此概览你可以了解更多关于内置类型、集合、日期时间、异步 Stream..._dart语言实战

问题解决:Conan 报"'ascii' codec can't decode byte 0xe5 in position 36: ordinal not in range(128)"_ascii' codec can't decode byte 0xe5 in position 3:-程序员宅基地

文章浏览阅读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