一、Spring Security框架简介
1、概要
2、历史
3、同款产品对比
3.1、Spring Security
Spring 技术栈的组成部分。
通过提供完整可扩展的认证和授权支持保护你的应用程序。
https://spring.io/projects/spring-sercurity
Spring Security 特点:
- 和Spring无缝整合
- 全面的权限控制
- 专门为Web开发而设计
- 旧版本不能脱离Web环境使用
- 新版本对整个框架进行了分层抽取,分成了核心模块和Web模块,单独引入了核心模块就可以脱离Web环境
- 重量级
3.2、Shiro
Apache旗下的轻量级权限控制框架
特点:
- 轻量级,Shiro主张的理念就是把复杂的事情变简单,针对对性能有更高要求的互联网应用有更好的表现。
- 通用性
- 好处:不局限于Web环境,可以脱离Web环境使用
- 缺陷:在Web环境下一些特定的需求需要手动编写代码定制
4、模块划分
二、Spring Security 入门案例
1、创建一个SpringBoot项目
2、引入相关依赖
1 | <dependency> |
3、编写Controller进行测试
1 | package com.lxg.securitydemo1.controller; |
默认的用户名:user
密码在运行项目后在启动控制台随机生成
4、权限管理中的相关概念
5、Spring Security基本原理
代码底层流程:重点看三个过滤器
FilterSecurityInterceptor:是一个方法级的权限过滤器,基本位于过滤链的最底部。
1 | public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException { |
super.beforeInvocation(fi)表示查看之前的filter是否放行
fi.getChain().doFilter(fi.getRequest(),fi.getResponse());表示真正的调用后台的服务。
ExceptionTranslationFilter:是个异常过滤器,用来处理在认证授权过程中抛出的异常。
1 | private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) |
UsernamePasswordAuthenticationFilter:对/login的POST请求做拦截,校验表单中的用户名和密码。
1 |
|
6、过滤器如何进行加载的?
7、UserDetailsService接口讲解
当什么都没配置的时候,账号和密码是由SpringSecurity定义生成的,而实际项目中账号和密码都是从数据库中查询出来的,所以我们要通过自定义逻辑控制认证逻辑。
如果需要自定义逻辑时,只需要实现UserDetailService接口即可。
接口定义如下:
1 | /* |
返回值UserDetails
这个类是系统默认的用户“主体”
1 | public interface UserDetails extends Serializable { |
UserDetails的实现类User
以后只需要使用User这个实体类即可!
方法参数username
表示用户名,此值是客户端表单传递过来的数据,默认情况下必须叫username,否则无法接收。
8、PasswordEncoder接口讲解
数据加密接口,用于返回User对象里面密码加密
接口定义如下:
1 | public interface PasswordEncoder { |
接口的实现类:
BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,平时多使用这个解析 器。 BCryptPasswordEncoder 是对 bcrypt 强散列方法的具体实现。是基于 Hash 算法实现的单 向加密。可以通过 strength 控制加密强度,默认 10
测试:
1 |
|
三、Spring Security Web权限方案
1、认证
1、设置登录的用户名和密码
第一种方式:通过配置文件
在application.properties:
1 | spring.security.user.name=xiaolin |
第二种方式:通过配置类
1 |
|
1 |
|
第三种方式:自定义编写实现类
第一步、创建配置类,设置使用哪个userDetailService实现类
1 |
|
第二步、编写实现类,返回User对象,User对象有用户名、密码和操作权限
1 |
|
2、实现数据库认证来完成用户登录(整合MyBatisPlus)
1、准备数据库和表
2、添加依赖
1 | <dependency> |
3、创建实体类及其mapper接口和xml
1 |
|
4、
1 |
|
5、注意在启动类上添加MapperScan注解
6、配置数据源
1 | spring: |
7、测试
3、自定义设置登录界面以及设置不需要认证可以访问
1、在配置类实现相关的配置
1 |
|
2、创建相关页面,controller
1 |
|
1 |
|
4、授权
1、基于角色或权限进行访问控制
1、hasAuthority方法
如果当前的主体有指定的权限,则返回true,否则返回false
1、修改配置类:
1 | //当前登录用户,只有具有admins权限才可以进行访问这个路径 |
2、在UserDetailsService中设置返回User对象的权限
1 | List<GrantedAuthority> authorityList = AuthorityUtils.commaSeparatedStringToAuthorityList("admins"); |
没有yhu权限的情况
2、hasAnythority方法
如果当前的主体有任何提供的权限(给定的作为一个逗号分隔的字符串列表)的话,返回true
1 | .antMatchers("/test/index").hasAnyAuthority("admins,manager") |
3、hasRole方法
如果用户具备给定角色就允许访问,否则出现403.
如果当前主体具有指定的角色,则返回true。
1 | //hasRole方法 ROLE_sale |
4、hasAnyRole方法
如果用户具备给定的任意一个角色就允许访问,否则出现403
如果当前主体具备指定的角色,则返回true
1 | //hasAnyRole方法 |
2、自定义403没有权限访问页面
在配置类中进行配置
1 |
|
3、注解使用
1、@Secured
判断是否具有某个角色,另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_”。
使用注解先要开启注解功能。(在启动类或者配置类上添加开启注解注解)
1 |
|
在控制器方法上添加注解
1 |
|
设置用户角色或权限
1 | List<GrantedAuthority> authorityList = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_manager"); |
2、@PreAuthorize
先开启注解功能
1 |
注解适合进入方法前的权限验证 ,可以将用户的roles/permissions参数传到方法中。
1 |
|
3、PostAuthorize
先开启注解功能
1 |
该注解使用并不多,在方法执行后再进行权限验证,适合验证带有返回值的权限。
1 |
|
控制台能打印说明先执行方法了,但是依然是没有权限访问
4、@PostFilter
权限验证之后对数据进行过滤,留下用户名是admin1的数据
表达式中的filterObject引用的是方法返回值List的某一个元素。
1 |
|
5、@PreFilter
进入控制器方法之前对数据进行过滤
1 |
|
4、用户注销
1、在登录成功页面添加一个退出链接
success.html
1 | <h1>登录成功!</h1> |
2、在配置类中添加退出的映射地址
1 | //退出 |
3、测试
- 修改配置类,登录成功之后跳转到成功页面
- 在成功页面添加超链接,设置退出路径
- 登录成功之后,在成功页面点击退出,再去访问其他controller不能进行访问了
1 |
|
5、基于数据库实现权限认证(记住我)
自动登录:
- cookie技术
- 安全框架机制实现自动登录
实现原理:
具体实现:
创建数据库表
1
2
3
4
5
6
7
8CREATE 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=utf8配置类,注入数据源,配置操作数据库对象
1
2
3
4
5
6
7
8
9
10
11
12
13//注入数据源
private DataSource dataSource;
//配置对象
public PersistentTokenRepository persistentTokenRepository(DataSource dataSource){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//生成表
//jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}配置类中配置自动登录
1
2
3.and().rememberMe().tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(60)//设置有效时长,单位秒
.userDetailsService(userDetailsService)在登录界面设置复选框
1
<input type="checkbox" name="remember-me"/>自动登录
name值固定,底层封装好了
6、CSRF理解
网络安全:https://www.bilibili.com/video/BV1Lf4y1t7Mc?spm_id_from=333.337.search-card.all.click
四、Spring Security 微服务权限方案
1、什么是微服务?
2、微服务认证和授权实现过程
单点登录、授权
1、认证授权过程分析
1 | (1)如果是基于 Session,那么 Spring-security 会对 cookie 里的 sessionid 进行解析,找 到服务器存储的 session 信息,然后判断当前用户是否符合请求的要求。 |
3、完成基于SpringSecurity认证授权案例
1、实现功能
1 | 1、登录(认证) |
2、权限管理数据模型
3、案例涉及技术说明
1、Maven
- 创建父工程:管理项目依赖版本
- 创建子模块:使用具体依赖
2、SpringBoot
- 本质就是Spring
3、MyBatisPlus
- 操作数据库框架
4、SpringCloud
- GateWay网关
- 注册中心 Nacos
5、Redis
6、Jwt
7、Swagger
8、前端技术
4、搭建项目工程
1、创建父工程acl_parent:管理依赖版本
2、在父工程创建子模块
- common
- service_base:工具类
- spring_security:权限配置
- infrastructure
- api_gateway:网关
- service
- service_acl:权限管理微服务模块
5、启动Redis和Nacos
1、启动Redis
输入命令redis-server redis.windows.conf 如下图所示则启动成功
输入redis-cli.exe -h 127.0.0.1 -p 6379 客户端访问
查找所有符合给定模式的key KEYS *
获取指定key的值 get name
设置指定key的值 set name “hello world”
2、启动Nacos
注册中心
访问地址:http://localhost:8848/nacos
默认用户名和密码都是:nacos
6、编写common里面需要的工具类
7、编写SpringSecurity认证授权工具类和处理器
密码处理工具类:
1 |
|
token操作工具类:
使用jwt生成token
1 |
|
8、jwt介绍
1、访问令牌的类型
2、JWT的组成
典型的,一个 JWT 看起来如下图:
该对象为一个很长的字符串,字符之间通过”.”分隔符分为三个子串。 每一个子串表示了一个功能块,总共有以下三个部分:JWT 头、有效载荷和签名
JWT 头
JWT 头部分是一个描述 JWT 元数据的 JSON 对象,通常如下所示。
{
“alg”: “HS256”,
“typ”: “JWT”
}
在上面的代码中,alg 属性表示签名使用的算法,默认为 HMAC SHA256(写为 HS256); typ 属性表示令牌的类型,JWT 令牌统一写为 JWT。最后,使用 Base64 URL 算法将上述 JSON 对象转换为字符串保存。
有效载荷
有效载荷部分,是 JWT 的主体内容部分,也是一个 JSON 对象,包含需要传递的数据。 JWT 指定七个默认字段供选择。
iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID 用于标识该 JWT
除以上默认字段外,我们还可以自定义私有字段,如下例:
{
“sub”: “1234567890”,
“name”: “Helen”,
“admin”: true
}
请注意,默认情况下 JWT 是未加密的,任何人都可以解读其内容,因此不要构建隐私信息 字段,存放保密信息,以防止信息泄露。
JSON 对象也使用 Base64 URL 算法转换为字符串保存
签名哈希
签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡 改。
首先,需要指定一个密码(secret)。该密码仅仅为保存在服务器中,并且不能向用户公 开。然后,使用标头中指定的签名算法(默认情况下为 HMAC SHA256)根据以下公式生成 签名。
HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(claims), secret)
在计算出签名哈希后,JWT 头,有效载荷和签名哈希的三个部分组合成一个字符串,每个 部分用”.”分隔,就构成整个 JWT 对象。
Base64URL算法
如前所述,JWT 头和有效载荷序列化的算法都用到了 Base64URL。该算法和常见 Base64 算 法类似,稍有差别。
作为令牌的 JWT 可以放在 URL 中(例如 api.example/?token=xxx)。 Base64 中用的三个 字符是”+”,”/“和”=”,由于在 URL 中有特殊含义,因此 Base64URL 中对他们做了替换: “=”去掉,”+”用”-“替换,”/“用”_”替换,这就是 Base64URL 算法。
9、编写退出处理器
1 | //退出处理器 |
10、编写未授权统一处理类
1 | public class UnauthEntryPoint implements AuthenticationEntryPoint { |
11、编写认证的过滤器
1 | public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter { |
12、编写授权过滤器
1 | public class TokenAuthFilter extends BasicAuthenticationFilter { |
13、编写核心配置类
1 |
|
14、编写UserDetailsServiceImpl
1 |
|
15、service_acl模块代码说明
1、配置文件
1 | # 服务端口 |
16、编写网关相关代码
配置类:
1 | package com.lxg.gateway.config; |
五、Spring Security 原理总结
1、 SpringSecurity 的过滤器介绍
SpringSecurity 采用的是责任链的设计模式,它有一条很长的过滤器链。现在对这条过滤 器链的 15 个过滤器进行说明:
(1) WebAsyncManagerIntegrationFilter:将 Security 上下文与 Spring Web 中用于 处理异步请求映射的 WebAsyncManager 进行集成。
(2) SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上 下文信息加载到 SecurityContextHolder 中,然后在该次请求处理完成之后,将 SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将 SecurityContextHolder 中的信息清除,例如在 Session 中维护一个用户的安全信 息就是这个过滤器处理的。
(3) HeaderWriterFilter:用于将头信息加入响应中。
(4) CsrfFilter:用于处理跨站请求伪造。
(5)LogoutFilter:用于处理退出登录。
(6)UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中 获取用户名和密码。默认情况下处理来自 /login 的请求。从表单中获取用户名和密码 时,默认使用的表单 name 值为 username 和 password,这两个值可以通过设置这个 过滤器的 usernameParameter 和 passwordParameter 两个参数的值进行修改。
(7)DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会 配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。
(8)BasicAuthenticationFilter:检测和处理 http basic 认证。
(9)RequestCacheAwareFilter:用来处理请求的缓存。
(10)SecurityContextHolderAwareRequestFilter:主要是包装请求对象 request。 (11)AnonymousAuthenticationFilter:检测 SecurityContextHolder 中是否存在 Authentication 对象,如果不存在为其提供一个匿名 Authentication。
(12)SessionManagementFilter:管理 session 的过滤器
(13)ExceptionTranslationFilter:处理 AccessDeniedException 和 AuthenticationException 异常。
(14)FilterSecurityInterceptor:可以看做过滤器链的出口。
(15)RememberMeAuthenticationFilter:当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的 remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。
2、SpringSecurity 基本流程
Spring Security 采取过滤链实现认证与授权,只有当前过滤器通过,才能进入下一个 过滤器:
绿色部分是认证过滤器,需要我们自己配置,可以配置多个认证过滤器。认证过滤器可以 使用 Spring Security 提供的认证过滤器,也可以自定义过滤器(例如:短信验证)。认 证过滤器要在 configure(HttpSecurity http)方法中配置,没有配置不生效。下面会重 点介绍以下三个过滤器:
UsernamePasswordAuthenticationFilter 过滤器:该过滤器会拦截前端提交的 POST 方式 的登录表单请求,并进行身份认证。
ExceptionTranslationFilter 过滤器:该过滤器不需要我们配置,对于前端提交的请求会 直接放行,捕获后续抛出的异常并进行处理(例如:权限访问限制)。
FilterSecurityInterceptor 过滤器:该过滤器是过滤器链的最后一个过滤器,根据资源 权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常,并 由 ExceptionTranslationFilter 过滤器进行捕获和处理。
1、认证流程
当前端提交的是一个 POST 方式的登录表单请求,就会被该过滤器拦截,并进行身份认 证。该过滤器的 doFilter() 方法实现在其抽象父类
*** 上述的 第二 过程调用了 UsernamePasswordAuthenticationFilter 的 attemptAuthentication() 方法,源码如下:
上述的(3)过程创建的 UsernamePasswordAuthenticationToken 是 Authentication 接口的实现类,该类有两个构造器,一个用于封装前端请求传入的未认 证的用户信息,一个用于封装认证成功后的用户信息:
Authentication 接口的实现类用于存储用户认证信息,查看该接口具体定义
上述过程中,UsernamePasswordAuthenticationFilter 过滤器的 attemptAuthentication() 方法的(5)过程将未认证的 Authentication 对象传入 ProviderManager 类的 authenticate() 方法进行身份认证。
ProviderManager 是 AuthenticationManager 接口的实现类,该接口是认证相关的核心接 口,也是认证的入口。在实际开发中,我们可能有多种不同的认证方式,例如:用户名+ 密码、邮箱+密码、手机号+验证码等,而这些认证方式的入口始终只有一个,那就是 AuthenticationManager。在该接口的常用实现类 ProviderManager 内部会维护一个 List列表,存放多种认证方式,实际上这是委托者模式 (Delegate)的应用。每种认证方式对应着一个 AuthenticationProvider, AuthenticationManager 根据认证方式的不同(根据传入的 Authentication 类型判断)委托 对应的 AuthenticationProvider 进行用户认证。
上述认证成功之后的(6)过程,调用 CredentialsContainer 接口定义的 eraseCredentials() 方法去除敏感信息。查看 UsernamePasswordAuthenticationToken 实现的 eraseCredentials() 方法,该方 法实现在其父类中
上述过程就是认证流程的最核心部分,接下来重新回到 UsernamePasswordAuthenticationFilter 过滤器的 doFilter() 方法,查看认证成 功/失败的处理:
认证成功和认证失败源码:
2、权限访问流程
上一个部分通过源码的方式介绍了认证流程,下面介绍权限访问流程,主要是对 ExceptionTranslationFilter 过滤器和 FilterSecurityInterceptor 过滤器进行介绍。
1、 ExceptionTranslationFilter 过滤器
该过滤器是用于处理异常的,不需要我们配置,对于前端提交的请求会直接放行,捕获后 续抛出的异常并进行处理(例如:权限访问限制)。具体源码如下:
2、FilterSecurityInterceptor 过滤器
FilterSecurityInterceptor 是过滤器链的最后一个过滤器,该过滤器是过滤器链 的最后一个过滤器,根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果 访问受限会抛出相关异常,最终所抛出的异常会由前一个过滤器 ExceptionTranslationFilter 进行捕获和处理。具体源码如下:
需要注意,Spring Security 的过滤器链是配置在 SpringMVC 的核心组件 DispatcherServlet 运行之前。也就是说,请求通过 Spring Security 的所有过滤器, 不意味着能够正常访问资源,该请求还需要通过 SpringMVC 的拦截器链。
3、SpringSecurity 请求间共享认证信息
一般认证成功后的用户信息是通过 Session 在多个请求之间共享,那么 Spring Security 中是如何实现将已认证的用户信息对象 Authentication 与 Session 绑定的进行 具体分析.
在前面讲解认证成功的处理方法 successfulAuthentication() 时,有以下代码:
查 看 SecurityContext 接 口 及 其 实 现 类 SecurityContextImpl , 该 类 其 实 就 是 对 Authentication 的封装:
查 看 SecurityContextHolder 类 , 该 类 其 实 是 对 ThreadLocal 的 封 装 , 存 储 SecurityContext 对象:
1、 SecurityContextPersistenceFilter 过滤器
前面提到过,在 UsernamePasswordAuthenticationFilter 过滤器认证成功之 后,会在认证成功的处理方法中将已认证的用户信息对象 Authentication 封装进 SecurityContext,并存入 SecurityContextHolder。
之后,响应会通过 SecurityContextPersistenceFilter 过滤器,该过滤器的位置 在所有过滤器的最前面,请求到来先进它,响应返回最后一个通过它,所以在该过滤器中 处理已认证的用户信息对象 Authentication 与 Session 绑定。
认证成功的响应通过 SecurityContextPersistenceFilter 过滤器时,会从 SecurityContextHolder 中取出封装了已认证用户信息对象 Authentication 的 SecurityContext,放进 Session 中。当请求再次到来时,请求首先经过该过滤器,该过滤 器会判断当前请求的 Session 是否存有 SecurityContext 对象,如果有则将该对象取出再次 放入 SecurityContextHolder 中,之后该请求所在的线程获得认证用户信息,后续的资源访 问不需要进行身份认证;当响应再次返回时,该过滤器同样从 SecurityContextHolder 取出 SecurityContext 对象,放入 Session 中。具体源码如下: