
SpringSecurity的使用
springsecurity的一些常用方法和在springboot中整合
学习目标
SpringSecruity简介
安全框架概述
什么是安全框架? 解决系统安全问题的框架。如果没有安全框架,我们需要手动处理每个资源的访问 控制,非常麻烦。使用安全框架,我们可以通过配置的方式实现对资源的访问限制。
常用安全框架
- Spring Security:Spring家族一员。是一个能够为基于Spring的企业应用系统提供声明式的安全访 问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了 Spring IoC , DI(控制反转Inversion of Control,DI:Dependency Injection 依赖注入) 和 AOP(面向切面编程) 功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安 全控制编写大量重复代码的工作。
- Apache Shiro:一个功能强大且易于使用的Java安全框架,提供了认证,授权,加密,和会话管理。 Spring Security简介 概述 Spring Security是一个高度自定义的安全框架。利用 Spring IoC/DI和AOP功能,为系统提供了声明 式安全访问控制功能,减少了为系统安全而编写大量重复代码的工作。使用 Spring Secruity 的原因有 很多,但大部分都是发现了 javaEE的 Servlet 规范或 EJB 规范中的安全功能缺乏典型企业应用场景。同 时认识到他们在 WAR 或 EAR 级别无法移植。因此如果你更换服务器环境,还有大量工作去重新配置你 的应用程序。使用 Spring Security解决了这些问题,也为你提供许多其他有用的、可定制的安全功能。 正如你可能知道的两个应用程序的两个主要区域是“认证”和“授权”(或者访问控制)。这两点也是 Spring Security 重要核心功能。“认证”,是建立一个他声明的主体的过程(一个“主体”一般是指用户, 设备或一些可以在你的应用程序中执行动作的其他系统),通俗点说就是系统认为用户是否能登录。 “授权”指确定一个主体是否允许在你的应用程序执行一个动作的过程。通俗点讲就是系统判断用户是否 有权限去做某些事情。
概述
Spring Security是一个高度自定义的安全框架。利用 Spring IoC/DI和AOP功能,为系统提供了声明 式安全访问控制功能,减少了为系统安全而编写大量重复代码的工作。使用 Spring Secruity 的原因有 很多,但大部分都是发现了 javaEE的 Servlet 规范或 EJB 规范中的安全功能缺乏典型企业应用场景。同 时认识到他们在 WAR 或 EAR 级别无法移植。因此如果你更换服务器环境,还有大量工作去重新配置你 的应用程序。使用 Spring Security解决了这些问题,也为你提供许多其他有用的、可定制的安全功能。 正如你可能知道的两个应用程序的两个主要区域是“认证”和“授权”(或者访问控制)。这两点也是 Spring Security 重要核心功能。“认证”,是建立一个他声明的主体的过程(一个“主体”一般是指用户, 设备或一些可以在你的应用程序中执行动作的其他系统),通俗点说就是系统认为用户是否能登录。 “授权”指确定一个主体是否允许在你的应用程序执行一个动作的过程。通俗点讲就是系统判断用户是否 有权限去做某些事情。
其核心就是一组过滤器链,项目启动后将会自动配置。最核心的就是 Basic Authentication Filter 用来认证用户的身份,一个在spring security中一种过滤器处理一种认证方式。
快速入门
导入依赖
1 | <dependency> |
编写前端页面login.html
1 |
|
访问页面
导入spring-boot-starter-security 启动器后,Spring Security 已经生效,默认拦截全部请求,如果用 户没有登录,跳转到内置登录页面。
默认的用户名为user,密码在控制台中
登陆后即可访问login.html和main.html
UserDetailsService详解
当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。而在实际项目中账号和密码都是从数据库中查询出来的。所以我们要通过自定义逻辑控制认证逻辑。如果需要自定义逻辑时,只需要实现 UserDetailsService 接口即可。接口定义如下:
1 | public interface UserDetailsService { |
返回值
返回值 UserDetails 是一个接口,定义如下:
1 | public interface UserDetails extends Serializable { |
要想返回 UserDetails 的实例就只能返回接口的实现类。SpringSecurity 中提供了如下的实例。我们只需要使用里面的 User 类即可。
注意 User 的全限定路径是: org.springframework.security.core.userdetails.User此处经常和系统中自己开发的 User 类弄混。
在 User 类中提供了很多方法和属性。 其中构造方法有两个,调用其中任何一个都可以实例化
UserDetails 实现类 User 类的实例。而三个参数的构造方法实际上也是调用 7 个参数的构造方法。
- username :用户名
- password :密码
- authorities :用户具有的权限。此处不允许为 null
1 | public User(String username, String password, |
此处的用户名应该是客户端传递过来的用户名。而密码应该是从数据库中查询出来的密码。Spring Security 会根据 User 中的 password 和客户端传递过来的 password 进行比较。如果相同则表示认证 通过,如果不相同表示认证失败。
Authorities 里面的权限对于后面学习授权是很有必要的,包含的所有内容为此用户具有的权限, 如有里面没有包含某个权限,而在做某个事情时必须包含某个权限则会出现 403。通常都是通过 AuthorityUtils.commaSeparatedStringToAuthorityList(“”) 来创建 authorities 集合对象 的。参数是一个字符串,多个权限使用逗号分隔。
方法参数
方法参数表示用户名。此值是客户端表单传递过来的数据。默认情况下必须叫 username ,否则无法接收。
异常
UsernameNotFoundException 用户名没有发现异常。在 loadUserByUsername 中是需要通过自己的 逻辑从数据库中取值的。如果通过用户名没有查询到对应的数据,应该抛出 UsernameNotFoundException ,系统就知道用户名没有查询到。
PasswordEncoder 密码解析器详解
Spring Security 要求容器中必须有 PasswordEncoder 实例。所以当自定义登录逻辑时要求必须给容 器注入 PaswordEncoder 的bean对象。
接口介绍
- encode() :把参数按照特定的解析规则进行解析。
- matches() :验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹配, 则返回 true;如果不匹配,则返回 false。第一个参数表示需要被解析的密码。第二个参数表示存 储的密码。
- upgradeEncoding() :如果解析的密码能够再次进行解析且达到更安全的结果则返回 true,否则 返回 false。默认返回 false。
1 | public interface PasswordEncoder { |
内置解析器介绍
在 Spring Security 中内置了很多解析器。
BCryptPasswordEncoder 简介
BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,平时多使用这个解析器。
BCryptPasswordEncoder是对 bcrypt 强散列方法的具体实现。是基于Hash算法实现的单向加密。 可以通过strength控制加密强度,默认 10,长度越长安全性越高。
Bcrypt 有两个特点:
- 每一次 HASH 出来的值不一样
- 计算非常缓慢
因此使用 Bcrypt 进行加密后,攻击者破解密码成本变得不可接受,但代价是应用自身也会性能受到影响,不过登录行为并不是随时在发生,因此能够忍受。
代码演示
1 |
|
需要注意的是,Spring Security要求:当进行自定义登录逻辑时容器内必须有PasswordEncoder实例。所以需要编写一个配置类,将密码解析器先注入进去:
1 |
|
自定义登录
当进行自定义登录逻辑时需要用到之前讲解的UserDetailsService和PasswordEncoder 。
自定义逻辑
在 Spring Security 中实现 UserDetailService 就表示为用户详情服务。在这个类中编写用户认证逻辑。
1 | package com.springsecurity.demo.service; |
在实际操作中我们是通过查询数据库获取用户名和密码
本文简单说明即把admin和123当作是数据库查询出的密码
自定义登录页面
虽然 Spring Security 给我们提供了登录页面,但是对于实际项目中,大多喜欢使用自己的登录页 面。所以 Spring Security 中不仅仅提供了登录页面,还支持用户自定义登录页面。实现过程也比较简 单,只需要修改配置类即可。
编写登录页面
1 |
|
修改配置类
修改配置类中主要是设置哪个页面是登录页面。配置类需要继承WebSecurityConfigurerAdapte,并重 写 configure 方法。
- successForwardUrl() :登录成功后跳转地址
- loginPage() :登录页面
- loginProcessingUrl :登录页面表单提交地址,此地址可以不真实存在。
- antMatchers() :匹配内容
- permitAll() :允许
1 |
|
编写控制器
1 |
|
认证过程其他常用配置
失败跳转
表单处理中成功会跳转到一个地址,失败也可以跳转到一个地址
编写页面error.html
1 |
|
修改表单配置
在配置方法中表单认证部分添加 failureForwardUrl() 方法,表示登录失败跳转的 url。此处依然是POST 请求,所以跳转到可以接收 POST请求的控制器/error中。
1 | //表单提交 |
添加控制器的方法
在控制器类中添加控制器方法,方法映射路径/error。此处要注意:由于是 POST 请求访问/error。所以如果返回值直接转发到 error.html 中,即使有效果,控制台也会报警告,提示 error.html 不支持POST 访问方式。
设置请求账户和密码的参数名
源码简介
当进行登录时会执行 UsernamePasswordAuthenticationFilter 过滤器。
- usernamePasrameter :账户参数名
- passwordParameter :密码参数名
- postOnly=true :默认情况下只允许POST请求。
所以我们可以设置参数来改变默认的名字
1 | //表单提交 |
自定义登录成功处理器
源码分析
使用successForwardUrl()时表示成功后转发请求到地址。内部是通过 successHandler() 方法进行 控制成功后交给哪个类进行处理
ForwardAuthenticationSuccessHandler内部就是最简单的请求转发。由于是请求转发,当遇到需要 跳转到站外或在前后端分离的项目中就无法使用了。
当需要控制登录成功后去做一些事情时,可以进行自定义认证成功控制器。
1 | package com.springsecurity.demo.handler; |
修改配置项
使用 successHandler()方法设置成功后交给哪个对象进行处理
1 | http.formLogin() |
自定义失败处理器也是一样的编写一个failureHandler()
同样不能和failureForwardUrl共存
访问控制url匹配
在前面讲解了认证中所有常用配置,主要是对 http.formLogin() 进行操作。而在配置类中 http.authorizeRequests() 主要是对url进行控制,也就是我们所说的授权(访问控制)。 http.authorizeRequests() 也支持连缀写法,总体公式为:
- url 匹配规则.权限控制方法
通过上面的公式可以有很多 url 匹配规则和很多权限控制方法。这些内容进行各种组合就形成了 Spring Security中的授权。
在所有匹配规则中取所有规则的交集。配置顺序影响了之后授权效果,越是具体的应该放在前面,越是笼统的应该放到后面。
anyRequest()
在之前认证过程中我们就已经使用过 anyRequest(),表示匹配所有的请求。一般情况下此方法都会 使用设置全部内容都需要进行认证。
1 | .anyRequest().authenticated(); |
antMatcher()
方法定义如下
1 | public C antMatchers(String... antPatterns) |
参数是不定向参数,每个参数是一个 ant 表达式,用于匹配 URL规则。
规则如下:
- ? : 匹配一个字符
- *:匹配 0 个或多个字符
- ** :匹配 0 个或多个目录 *
在实际项目中经常需要放行所有静态资源,下面演示放行 js 文件夹下所有脚本文件。
1 | .antMatchers("/js/**","/css/**").permitAll() |
还有一种配置方式是只要是.js 文件都放行
1 | .antMatchers("/**/*.js").permitAll() |
regexMatchers()
介绍
使用正则表达式进行匹配。和 antMatchers() 主要的区别就是参数, antMatchers() 参数是 ant 表达式, regexMatchers() 参数是正则表达式。
演示所有以.js 结尾的文件都被放行。
1 | .regexMatchers( ".+[.]js").permitAll() |
两个参数时使用方式
无论是 antMatchers() 还是 regexMatchers() 都具有两个参数的方法,其中第一个参数都是 HttpMethod ,表示请求方式,当设置了 HttpMethod 后表示只有设定的特定的请求方式才执行对应的 权限设置。
枚举类型 HttpMethod 内置属性如下:
mvcMatchers()
mvcMatchers()适用于配置了 servletPath 的情况。
servletPath 就是所有的 URL 的统一前缀。在 SpringBoot 整合SpringMVC 的项目中可以在application.properties 中添加下面内容设置 ServletPath。
在 Spring Security 的配置类中配置 .servletPath() 是 mvcMatchers()返回值特有的方法, antMatchers()和 regexMatchers()没有这个方法。在 servletPath() 中配置了 servletPath 后, mvcMatchers()直接写 SpringMVC 中@RequestMapping()中设置的路径即可。
1 | .mvcMatchers("/demo").servletPath("/xxxx").permitAll() |
如果不习惯使用 mvcMatchers()也可以使用 antMatchers(),下面代码和上面代码是等效
1 | .antMatchers("/xxxx/demo").permitAll() |
内置访问控制方法
Spring Security 匹配了 URL 后调用了 permitAll() 表示不需要认证,随意访问。在 Spring Security中提供了多种内置控制。
- permitAll() 表示所匹配的 URL 任何人都允许访问。
- authenticated() 表示所匹配的 URL 都需要被认证才能访问。
- anonymous() 表示可以匿名访问匹配的URL。和permitAll()效果类似,只是设置为 anonymous()的 url会执行 filter 链中
- denyAll() 表示所匹配的 URL 都不允许被访问。
- rememberMe() 被“remember me”的用户允许访问
- fullyAuthenticated() 如果用户不是被 remember me 的,才可以访问。
角色权限判断
除了之前讲解的内置权限控制。Spring Security 中还支持很多其他权限控制。这些方法一般都用于用户已经被认证后,判断用户是否具有特定的要求。
hasAuthority(String) 判断用户是否具有特定的权限,用户的权限是在自定义登录逻辑中创建 User 对象时指定的。其中admin和normal 就是用户的权限。admin和normal 严格区分大小写。
即在userdetails中设置
1
return newUser(username,password,AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal"));
在配置类中通过 hasAuthority(“admin”)设置具有 admin 权限时才能访问。
1
.antMatchers("/main1.html").hasAuthority("admin")
hasAnyAuthority(String …) 如果用户具备给定权限中某一个,就允许访问。
下面代码中由于大小写和用户的权限不相同,所以用户无权访问
1
.antMatchers("/main1.html").hasAnyAuthority("adMin","admiN")
hasRole(String)
如果用户具备给定角色就允许访问。否则出现 403。 参数取值来源于自定义登录逻辑 UserDetailsService 实现类中创建 User 对象时给 User 赋予的授 权。 在给用户赋予角色时角色需要以: ROLE_开头 ,后面添加角色名称。例如:ROLE_abc 其中 abc 是角 色名,ROLE_是固定的字符开头。 使用 hasRole()时参数也只写 abc 即可。否则启动报错。
给用户赋予角色:
1
return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal,ROLE_abc"));
在配置类中直接写 abc 即可。
1
.antMatchers("/main1.html").hasRole("abc")
hasAnyRole(String …) 如果用户具备给定角色的任意一个,就允许被访问
hasIpAddress(String)
如果请求是指定的 IP 就运行访问。
可以通过 request.getRemoteAddr() 获取 ip 地址。
需要注意的是在本机进行测试时 localhost 和 127.0.0.1 输出的 ip地址是不一样的。
localhost打印出的是:0:0:0:0:0:0:0:1
1
.antMatchers("/main1.html").hasIpAddress("127.0.0.1")
基于表达式的访问控制
access()方法使用
之前学习的登录用户权限判断实际上底层实现都是调用access(表达式)
可以通过 access() 实现和之前学习的权限控制完成相同的功能。
以 hasRole 和 和 permitAll 举例
1 | .antMatchers("/showLogin").access("permitAll") |
使用自定义方法
虽然这里面已经包含了很多的表达式(方法)但是在实际项目中很有可能出现需要自己自定义逻辑的情况。
判断登录用户是否具有访问当前 URL 权限。
新建接口及实现类
1 | package com.springsecurity.demo.service; |
修改配置类
在 access 中通过@bean的id名.方法(参数)的形式进行调用配置类中修改如下:
1 | //url拦截 |
基于注解的访问控制
在 Spring Security 中提供了一些访问控制的注解。这些注解都是默认是都不可用的,需要通过@EnableGlobalMethodSecurity 进行开启后使用。
如果设置的条件允许,程序正常执行。如果不允许会报 500
这些注解可以写到 Service 接口或方法上,也可以写到 Controller或 Controller 的方法上。通常情况下 都是写在控制器方法上的,控制接口URL是否允许被访问。
开启注解
在 启 动 类 ( 也 可 以 在 配 置 类 等 能 够 扫 描 的 类 上 ) 上 添 加 @EnableGlobalMethodSecurity(securedEnabled = true)
在控制器方法上添加@Secured等 注解
@Secured
@Secured 是专门用于判断是否具有角色的。能写在方法或类上。参数要以 ROLE_开头。
@PreAuthorize/@PostAuthorize
@PreAuthorize 和@PostAuthorize 都是方法或类级别注解。
@PreAuthorize 表示访问方法或类在执行之前先判断权限,大多情况下都是使用这个注解,注解的参数和access()方法参数取值相同,都是权限表达式。
@PostAuthorize 表示方法或类执行结束后判断权限,此注解很少被使用到。
RememberMe功能实现
Spring Security 中 Remember Me 为“记住我”功能,用户只需要在登录时添加 remember-me复选框,取值为true。Spring Security 会自动把用户信息存储到数据源中,以后就可以不登录进行访问。
添加依赖
Spring Security 实 现 Remember Me 功 能 时 底 层 实 现 依 赖Spring-JDBC,所以需要导入 SpringJDBC。以后多使用 MyBatis 框架而很少直接导入 spring-jdbc,所以此处导入 mybatis 启动器同时还需 要添加 MySQL 驱动。
1 | <!-- mybatis 依赖 --> |
配置数据源
在 application.properties 中配置数据源。请确保数据库中已经存在shop数据库
1 | com.mysql.cj.jdbc.Driver = |
编写配置
1 |
|
1 | //记住我 |
有效时间
默认情况下重启项目后登录状态失效了。但是可以通过设置状态有效时间,即使项目重新启动下次也可以正常登录。
退出登录
用户只需要向 Spring Security 项目中发送 /logout 退出请求即可。
实现退出非常简单,只要在页面中添加 /logout 的超链接即可。
1 | <a href="/logout">退出登录</a> |
为了实现更好的效果,通常添加退出的配置。默认的退出 url 为 /logout ,退出成功后跳转到 /login?login
如果不希望使用默认值,可以通过下面的方法进行修改。
1 | //退出 |
logout其他常用配置源码解读
clearAuthentication(boolean)
是否清除认证状态,默认为 true
invalidateHttpSession(boolean)
是否销毁 HttpSession 对象,默认为 true
具体看上图
logoutSuccessHandler(LogoutSuccessHandler)
退出成功处理器
也可以自己进行定义退出成功处理器。只要实现了 LogoutSuccessHandler 接口。与之前讲解的登录 成功处理器和登录失败处理器极其类似。