01基础篇
02-1运维实用篇(打包与运行、多环境配置、日志)
02-2开发实用篇(热部署、配置、NoSQL、整合第三方技术)
03SpringCache
04SpringSecurity
SpringBoot(B站尚硅谷)学习笔记 04SpringSecurity
Spring 是非常流行和成功的 Java 应用开发框架,Spring Security 正是 Spring 家族中的成员。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。
正如你可能知道的关于安全方面的两个主要区域是“认证”和“授权”(或者访问控制),一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分,这两点也是 Spring Security 重要核心功能。
SpringSecurity
官网:https://spring.io/projects/spring-security
SpringSecurity 特点:
Shiro
Shiro特点:
轻量级。Shiro 主张的理念是把复杂的事情变简单。针对对性能有更高要求的互联网应用有更好表现。
通用性。
Spring Security 是 Spring 家族中的一个安全管理框架,实际上,在 Spring Boot 出现之前,Spring Security 就已经发展了多年了,但是使用的并不多,安全管理这个领域,一直是 Shiro 的天下。
相对于 Shiro,在 SSM 中整合 Spring Security 都是比较麻烦的操作,所以,Spring Security 虽然功能比 Shiro 强大,但是使用反而没有 Shiro 多(Shiro 虽然功能没有Spring Security 多,但是对于大部分项目而言,Shiro 也够用了)。
自从有了 Spring Boot 之后,Spring Boot 对于 Spring Security 提供了自动化配置方案,可以使用更少的配置来使用 Spring Security。
web项目 SpringBoot版本:2.3.7.RELEASE
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
测试
测试localhost:8080/test/hello回车会自动跳转login页面,说明security已经起作用了。
怎么登录呢,默认用户名叫user,密码是idea控制台打印的一串字符
SpringSecurity 本质是一个过滤器链(Filter):
从启动后控制台是可以获取到过滤器链
[org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@4d8126f, org.springframework.security.web.context.SecurityContextPersistenceFilter@340b7ef6, org.springframework.se…]
重点看三个过滤器
FilterSecurityInterceptor:是一个方法级的权限过滤器,判断哪些方法可以访问哪些方法不可访问, 基本位于过滤链的最底部。
ExceptionTranslationFilter:是个异常过滤器,用来处理在认证授权过程中抛出的异常
UsernamePasswordAuthenticationFilter :对/login 的 POST 请求做拦截,校验表单中用户名,密码。
当什么也没有配置的时候,账号和密码是由 Spring Security自动生成的。而在实际项目中账号和密码都是从数据库中查询出来的。 所以我们要通过自定义逻辑控制认证逻辑。
UserDetailsService接口:查询数据库用户名和密码过程
创建类实现UserdetailService接口,重写loadUserByUsername,编写查询数据过程
重写的loadUserByUsername方法要返回UserDetails,UserDetails也是一个接口,其内有一些方法,而User实体类实现了UserDetails接口,所以最终返回的是User实体类,这个实体类是SpringSecurity提供的一个实体类,返回的User需要传入的是用户名密码和对应的权限。
SpringSecurity是不允许密码进行明文显示的,所以需要进行加密,PasswordEncoder是Security提供的一个密码加密接口,用于返回User对象里面密码加密。
BCryptPasswordEncoder实现了这个接口,调用这个对象进行加密,Security只认这个加密,我们可以重写它的encode和matches方法指定加密技术
环境准备:接着用入门案例的工程
在入门案例中用户名和密码是security生成的,我们也可以设置用户名和密码
三种方式:1.通过配置文件 2.通过配置类 3.自定义编写实现类
配置文件指定了用户名和密码控制台就不会生成了
方式二:通过配置类
编写配置类继承WebSecurityConfigurerAdapter类,继承的这个类是由Security提供的,所以我们写配置类要继承它,重写里面的configure方法(参数是AuthenticationManagerBuilder auth)。使用这个auth设置登录名和密码
测试运行
输入用户名密码登录没反应,发现 控制台报错了,说没有为id“null”映射PasswordEncoder。
原因是我们的密码要进行加密,加密的时候要用到PasswordEncoder接口,我们需要把这个接口创建出一个对象,要不然默认做加密过程中会报错,因为Spring Security 要求spring容器中必须有 PasswordEncoder实例。所以当自定义登录逻辑时要求必须给容器注入 PaswordEncoder的bean对象。这个bean返回BCryptPasswordEncoder(它实现了PasswordEncoder接口,是security提供的加密技术)。
细节处理
BCryptPasswordEncoder是security提供的加密技术,我们也可以指定加密技术,如图指定MD5的加密方式,此处使用了匿名内部类的写法。
方式三:自定义编写实现类
方式一和方式二都是静态的用户名和密码,在实际开发中都是通过数据库查询认证的。在security认证过程中首先去找配置文件然后是配置类,如果它发现配置文件中或配置类中有用户名和密码,它就会根据这个用户名和密码进行登录认证。如果这两处都没设置它就会去找UserDetailsService接口,在这个接口中找到通过查数据库或者其它方式设置的用户名和密码进行登录认证。所以我们需要编写写这个接口的实现类去实现数据库查询认证。
继承接口重写的loadUserByUsername方法要返回UserDetails,UserDetails也是一个接口,其内有一些方法,而User实体类实现了UserDetails接口,所以最终返回的是User实体类,这个实体类是SpringSecurity提供的一个实体类,返回的User需要传入的是用户名密码和对应的权限。
测试运行
但发现密码对输入什么用户名都行,原因是loadUserByUserName是利用用户名去查用户,只校验了密码,用户名没有,直接new User,没有做账号判断,PasswordEncoder只做了密码判断,所以用户名随便输入,只要密码校验对就可以了。理论上账号是去数据库查询的,查到存在该账号,然后在匹配密码;
实现数据库认证只需要在方式三:自定义编写实现类上加上查询数据库即可。
环境准备
新建springboot web项目,并引入相关依赖(SpringSecurity、mybatis-plus、mysql驱动、lombok),复制入门案例中config controller service包下的代码过来
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.4.2version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
dependencies>
数据库准备
测试运行
前面看到的登录页面是springsecurity自带的登录页面,项目中肯定不会用自带登录页面,我们可以自定义登录页面。并且我们可以规定哪些接口可以允许直接访问不需要认证
重写里面的configure方法(参数是HttpSecurity http)
测试运行
/test/hello可以直接访问
细节处理:
form表单中用户名和密码框name必须叫username和password,不然security得不到用户名和密码,自定义的话需要在配置中加上自定义的名称
在前面的操作中,只要认证成功就能访问内容,但我们可以在访问的过程中做个授权,例如登录只让管理员才能访。
如果当前的主体具有指定的权限,则返回true,否则返回fales
测试运行
如果当前的主体有任何提供的角色(给定的作为一个逗号分隔的字符串列表)的话,返回
true。就是此方法针对多个权限,比如某路径管理员可以访问,普通用户也可以访问。
细节处理
.hasAnyAuthority(“admins”,“root”)和.hasAnyAuthority(“admins,root”)两种写法都可以,其内部用可变参数写法
测试运行
只有root权限或只有admins权限都可以访问
如果当前主体具有指定的角色,则返回true允许访问,否则出现403
为什么MyUserDetailService返回的权限要加上ROLE_而.hasRole(manager)不用,因为security把权限和角色都放进同一个List auths,使用","隔开,而使用ROLE_是为了区分哪个是角色哪个是权限,所以我们数据库写角色字段时要加上ROLE_或者在查出来的字段补上ROLE_。那为什么.hasRole(manager)不用写ROLE_,通过查看源码可以发现,他把.hasRole()括号里的参数字段加上了ROLE_,如果再加上就会报异常,这么做的目的是为了统一格式。
前面我们都把权限和角色都直接写在代码里,一般项目中都是从数据库查询的,下面就来详细介绍从数据库实现权限和角色访问控制。
环境准备
用的还是上方的工程,
注:因为是需要复杂的多表查询,这里选择使用原生Mybatis写法(MP不影响Mybatis使用),自定义sql语句。相关配置详细查看springboot整合Mybatis
在MyUserDetailService调用mapper里面的方法查询数据进行权限认证
为什么查询出的角色要加上ROLE_,通过代码我们可以看到security把权限和角色都放进同一个List auths,而使用ROLE_是为了区分哪个是角色哪个是权限,所以我们数据库写角色字段时要加上ROLE_或者在查出来的字段补上ROLE_,这里我们选择在查出来的字段补上ROLE_。
至于为什么要把每个单独的权限或角色先放进SimpleGrantedAuthority对象再放进List auths里,还没理解,后续理解了再回来补充
在SecurityConfig配置类设置当前访问地址允许哪些权限或角色就行访问
为什么.hasRole(“管理员”)不用加上ROLE_,(前面有讲)通过查看源码可以发现,他把.hasRole()括号里的参数字段加上了ROLE_,如果再加上就会报异常,这么做的目的是为了统一格式。
测试运行
lisi用户没有管理员角色也没用menu:system权限,被拒绝访问
就算有menu:system也会被拒绝访问,因为他没有管理员角色
当用户无权限访问时页面会跳转到403页面如下图,此页面我们通过相关配置实现自定义403页面
http.exceptionHandling().accessDeniedPage("/403.html") // 配置没有权限访问跳转自定义403页面
使用注解可以方便我们进行开发,简化很多配置。springsecurity中也可以使用注解进行角色和权限控制
作用:判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_“
注:使用注解前要先开启security注解功能!
在启动类上添加@EnableGlobalMethodSecurity(securedEnabled = true)
该注解适合进入方法前的权限验证,可以将登录用户的 roles/permissions 参数传到方法中(就是直接用hasRole或hasAnyAuthority跟配置类一样写)
注:使用注解前要先开启security注解功能!
在启动类上添加@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
该注解使用并不多,在方法执行后再进行权限验证,适合验证带有返回值的权限
注:使用注解前要先开启security注解功能!
在启动类上添加@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
测试运行
权限验证之后对数据进行过滤(返回数据做过滤)
在controller添加注解
留下用户名是 admin1 的数据。表达式中的 filterObject 引用的是方法返回值List 中的某一个元素
进入控制器之前对数据进行过滤(传入数据做过滤)
跟在前面加.and()后写也行,这里方便阅读。
.logoutUrl(“/logout”)里面的注销接口跟登录接口一样,跟前端匹配就行
测试(这个测试演示麻烦点)
当我们把浏览器关闭重新打开后访问每次都要重新登录,很麻烦。而在我们平时浏览的网站中都可以实现自动登录,下面就来详细介绍springsecurity中的记住我功能实现重启浏览器后能够自动登录。
在JavaWeb中我们学过如何使用Cookie来实现记住我功能,但Cookie是把数据存到客户端,这并不安全。我们可以使用SpringSecurity安全框架机制实现自动登录,它已经把过程大部分都进行了封装。
浏览器发起登录进行认证请求,认证成功后security会把一串加密串(token)写入浏览器Cookie,然后再把加密串和用户登录信息存入数据库。重新打开浏览器后浏览器会发起服务请求,经过security读取Cookie中的加密串再到数据库查找到对应加密串的登录信息,匹配成功就能实现自动登录功能
写进数据库的过程security也帮我们完成了,从源码可以看到有建表语句,还有增加、删除、查找、修改等sql语句也都是封装好的。
(其实可以自动生成,这里手动创建是为了方便理解,建表语句是直接用源码的。自己建的话表必须叫persistent_logins)
create table persistent_logins(
username varchar(64) not null,
series varchar(64) primary key,
token varchar(64) not null,
last_used timestamp not null
)
注意:复选框中的name属性必须叫remember-me,不然security找不到
测试运行
60s内重新打开浏览器依旧可以正常访问
60s后再打开就要重新登录了
(它还会自动续时!)
在前面的过程中我们做过一个配置 ‘http.csrf().disable(); //关闭从csrf防护’ 这是干什么的呢?
跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的 Web 应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。
跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的
举例:比如我在浏览器中打开了淘宝网站(假设它没开csrf防护),打开后进行了登录认证。这时候再打开另一个网站,它两目前都在同一个浏览器中,那另一个网站它能得到当前浏览器中所有的cookie信息,那就也能得到淘宝网站中的所有内容。这么做就很不安全,这就叫跨站请求攻击
从 Spring Security 4.0 开始,默认情况下会启用CSRF 保护,以防止CSRF 攻击应用程序,Spring Security CSRF 会针对 PATCH,POST,PUT 和DELETE 方法进行防护。我们之前把csrf防护关掉是为了测试方便,如果上线后没用csrf防护对网站的风险是很大的
security做跨站请求伪造防护时会在认证后生成一个Token(按照一定规则生成的字符串)保存得到Session或Cookie中,而每次请求都带着token值进行请求,拿着每次请求传回来的token值和session中的内容做比较,如果两个都一样就允许访问,反之不允许访问。以此来预防跨站攻击,因为跨站攻击不知道token值而且每次都不一样。
<input type="hidden" th:if="${_csrf}!=null" th:value="${_csrf.token}" name="_csrf"/>
暂时跳过,后面再补
SpringSecurity-微服务权限方案
数据库
create table users(
id bigint primary key auto_increment,
username varchar(20) unique not null,
password varchar(100)
);
-- 密码 atguigu
insert into users values(1,'zhangsan','$2a$10$2R/M6iU3mCZt3ByG7kwYTeeW0w7/UqdeXrb27zkBIizBvAven0/na');
-- 密码 atguigu
insert into users values(2,'lisi','$2a$10$2R/M6iU3mCZt3ByG7kwYTeeW0w7/UqdeXrb27zkBIizBvAven0/na');
create table role(
id bigint primary key auto_increment,
name varchar(20)
);
insert into role values(1,'管理员');
insert into role values(2,'普通用户');
create table role_user(
uid bigint,
rid bigint
);
insert into role_user values(1,1);
insert into role_user values(2,2);
create table menu(
id bigint primary key auto_increment,
name varchar(20),
url varchar(100),
parentid bigint,
permission varchar(20)
);
insert into menu values(1,'系统管理','',0,'menu:system');
insert into menu values(2,'用户管理','',0,'menu:user');
create table role_menu(
mid bigint,
rid bigint
);
insert into role_menu values(1,1);
insert into role_menu values(2,1);
insert into role_menu values(2,2);
该内容是根据B站尚硅谷学习时所记,相关资料可在B站查询:尚硅谷SpringSecurity框架教程(spring security源码剖析从入门到精通)