第一章 了解 spring security
spring security 是基于 spring 的安全框架。它提供全面的安全性解决方案,同时在 Web 请求级和方法调用级处理身份确认和授权。在 Spring Framework 基础上,spring security 充分 利用了 依赖 注入(DI)和 面向切 面编 程( AOP)功 能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。是一个轻量级的安全框架。它与 Spring MVC 有很好地集成.
1.1 spring security 核心功能
(1)认证(你是谁,用户/设备/系统)
(2)验证(你能干什么,也叫权限控制/授权,允许执行的操作)
1.2 spring security 原理
基于 Filter , Servlet, AOP 实现身份认证和权限验证
第二章 实例驱动学习
使用的框架和技术
spring boot 2.0.6 版本
spring security 5.0.9 版本
maven 3 以上
jdk8 以上
idea 2019
第一个例子:初探
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ydj</groupId>
<artifactId>ch01-first</artifactId>
<version>1.0-SNAPSHOT</version>
<!--spring boot-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-parent</artifactId>
<version>2.0.6.RELEASE</version>
</parent>
<dependencies>
<!--web开发依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--security依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@RestController
@RequestMapping("/hello")
public class HelloSecurityController {
@RequestMapping("world")
public String sayHello() {
return "Hello Spring Security 安全管理框架";
}
}
启动项目后控制台会出现一下信息
访问http://localhost:8080/hello/world
User:user
Password:刚刚控制台生产的密码
自定义用户名和密码
spring:
security:
user:
name: ydj
password: 123456
关闭验证
//排除Security配置
@SpringBootApplication(exclude = {SecurityAutoConfiguration.class})
public class FirstApplication {
public static void main(String[] args) {
SpringApplication.run(FirstApplication.class, args);
}
}
第二个例子:使用内存中的用户信息
1)使用:WebSecurityConfigurerAdapter 控制安全管理的内容。
需要做的使用:继承 WebSecurityConfigurerAdapter,重写方法。实现自定义的认证信息。重写下面的方法。
protected void configure(AuthenticationManagerBuilder auth)
@Configuration
@EnableWebSecurity//表示启用 spring security 安全框架的功能
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
PasswordEncoder encoder = passwordEncoder();
auth.inMemoryAuthentication().withUser("zhangsan").password(encoder.encode("123456")).roles();
auth.inMemoryAuthentication().withUser("lisi").password(encoder.encode("123456")).roles();
auth.inMemoryAuthentication().withUser("admin").password(encoder.encode("admin")).roles();
}
//创建密码加密类
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
2)spring security 5 版本要求密码比较加密,否则报错
java.lang.IllegalArgumentException: There is no PasswordEncodermapped for the id "null"
实现密码加密
1)创建用来加密的实现类(选择一种加密的算法)
//创建密码加密类
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
2)给每个密码加密
PasswordEncoder pe = passwordEncoder();
pe.encode("123456");
第三个例子:基于角色 Role 的身份认证
基于角色的实现步骤:
1.设置用户的角色
继承 WebSecurityConfigurerAdapter
重写 configure 方法。指定用户的 roles
**
* @EnableGlobalMethodSecurity:启动方法级别的认证
* prePostEnabled:boolean 默认 false
* true:表示可以使用@PreAuthorize注解和@PostAuthorize
*/
@EnableWebSecurity//包含了@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
PasswordEncoder encoder = passwordEncoder();
//定义2个角色 normal admin
auth.inMemoryAuthentication().
withUser("zhangsan").password(encoder.encode("123456")).
roles("normal");
auth.inMemoryAuthentication().
withUser("lisi").password(encoder.encode("123456")).
roles("normal");
auth.inMemoryAuthentication()
.withUser("admin").password(encoder.encode("admin")).
roles("admin","normal");
}
//创建密码加密类
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
@RestController
@RequestMapping("/hello")
public class HelloSecurityController {
@RequestMapping("world")
public String sayHello() {
return "Hello Spring Security 安全管理框架";
}
//指定normal和admin角色都可以访问的方法
@RequestMapping("helloUser")
@PreAuthorize(value = "hasAnyRole('admin','normal')")
public String helloCommonUser() {
return "hello 拥有normal,admin角色的用户";
}
//指定Admin角色的方法
@RequestMapping("helloAdmin")
@PreAuthorize("hasAnyRole('admin')")
public String helloAdminUser() {
return "hello 拥有admin角色的用户";
}
}
第四个例子,基于 jdbc 的用户认证
从数据库 mysql 中获取用户的身份信息(用户名称,密码,角色)
1)在 spring security 框架对象用户信息的表示类是 UserDetails.
UserDetails 是一个接口,高度抽象的用户信息类(相当于项目中的
User 类)
User 类:是 UserDetails 接口的实现类, 构造方法有三个参数:
username,password, authorities
需要向 spring security 提供 User 对象, 这个对象的数据来自数据库
的查询。
2)实现 UserDetailsService 接口,
重写方法 UserDetails loadUserByUsername(String username)
在方法中获取数据库中的用户信息, 也就是执行数据库的查询,条
件是用户名称。
@Configuration
public class ApplicationConfig {
@Autowired
private DataSource dataSource;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService jdbcUserDetailService() {
PasswordEncoder encoder = passwordEncoder();
JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
if (!manager.userExists("admin")){
manager.createUser(User.withUsername("admin").
password(encoder.encode("admin"))
.roles("ADMIN","USER","MAMAGER").build());
}
if (!manager.userExists("zhangsan")) {
manager.createUser(User.withUsername("zhangsan")
.password(encoder.encode("123")).
roles("USER").build());
}
if (!manager.userExists("lisi")) {
manager.createUser(User.withUsername("lisi")
.password(encoder.encode("123"))
.roles("MANAGER","USER").build());
}
return manager;
}
}
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.userDetailsService(userDetailsService);
}
}
第三章 基于角色的权限
3.1 认证和授权
authentication:认证, 认证访问者是谁。 一个用户或者一个其他系统是不是当前要访问的系统中的有效用户。
authorization:授权, 访问者能做什么
3.2 RBAC 是什么?
RBAC 是基于角色的访问控制(Role-Based Access Control )在 RBAC 中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,而把角色又赋予用户,这样的权限设计很清楚,管理起来很方便。
其基本思想是,对系统操作的各种权限不是直接授予具体的用户,而是在用户集合与权限集合之间建立一个角色集合。每一种角色对应一组相应的权限。一旦用户被分配了适当的角色后,该用户就拥有此角色的所有操作权限。这样做的好处是,不必在每次创建用户时都进行分配权限的操作,只要分配用户相应的角色即可,而且角色的权限变更比用户的权限变更要少得多,这样将简化用户的权限管理,减少系统的开销。
权限:能对资源的操作, 比如增加,修改,删除,查看等等。
角色:自定义的, 表示权限的集合。一个角色可以有多个权限。
RBAC 设计中的表:
- 用户表: 用户认证(登录用到的表)
用户名,密码,是否启用,是否锁定等信息。 - 角色表:定义角色信息
角色名称, 角色的描述。 - 用户和角色的关系表: 用户和角色是多对多的关系。
一个用户可以有多个角色, 一个角色可以有多个用户。 - 权限表, 角色和权限的关系表
角色可以有哪些权限。
3.3 spring specurity 中认证的接口和类
1) UserDetails:接口,表示用户信息的。
boolean isAccountNonExpired(); 账号是否过期
boolean isAccountNonLocked();账号是否锁定
boolean isCredentialsNonExpired();证书是否过期
boolean isEnabled();账号是否启用
Collection<? extends GrantedAuthority> getAuthorities(); 权限集合
User 实现类
org.springframework.security.core.userdetails.User
可以:自定义类实现 UserDetails 接口,作为你的系统中的用户类。这
个类可以交给 spring security 使用。
2) UserDetailsService 接口:
主要作用:获取用户信息,得到是 UserDetails 对象。一般项目中都需
要自定义类实现这个接口,从数据库中获取数据。
一个方法需要实现:
UserDetails loadUserByUsername(String var1) :根据用户名称,获取用
户信息(用户名称,密码,角色结合,是否可用,是否锁定等信息)
UserDetailsService 接口的实现类:
- InMemoryUserDetailsManager:在内存中维护用户信息。
优点:使用方便。
缺点:数据不是持久的。系统重启后数据恢复原样。 - JdbcUserDetailsManager :用户信息存放在数据库中,底层使用
jdbcTemplate 操作数据库。 可以 JdbcUserDetailsManager 中的方法完
成用户的管理
createUser : 创建用户
updateUser:更新用户
deleteUser:删除用户
userExists:判断用户是否存在
数据库文件:
org.springframework.security.core.userdetails.jdbc
users.ddl 文件
create table users(
username varchar_ignorecase(50) not null primary key,
password varchar_ignorecase(500) notnull,
enabled boolean not null
);
create table authorities (
username varchar_ignorecase(50) not null,
authority varchar_ignorecase(50) not null,
constraint fk_authorities_users foreign key(username) references
users(username)
);
create unique index ix_auth_username on authorities(username,authority);
3.4 定义用户,角色,角色关系表
用户信息表 sys_user:
sys_role:角色表
sys_user_role: 用户-角色关系表
3.5 pom 依赖
略
3.6 创建实体类
定义用户信息类,实现 spring security 框架中的 UserDetails
@TableName(value ="sys_user")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SysUser implements Serializable, UserDetails {
@TableId(type = IdType.AUTO)
private Integer id;
private String username;
private String password;
private String realname;
private boolean isenable;
private boolean islock;
private boolean iscredentials;
@TableField(exist = false)
private boolean isExpired;
private LocalDate createtime;
private LocalDate logintime;
@TableField(exist = false)
private List<GrantedAuthority> authorities;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public boolean isAccountNonExpired() {
return isExpired;
}
@Override
public boolean isAccountNonLocked() {
return islock;
}
@Override
public boolean isCredentialsNonExpired() {
return iscredentials;
}
@Override
public boolean isEnabled() {
return isenable;
}
}
SysRole 类:
@TableName(value ="sys_role")
@Data
public class SysRole implements Serializable {
/**
*
*/
@TableId(type = IdType.AUTO)
private Integer id;
/**
*
*/
private String rolename;
/**
*
*/
private String rolememo;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}
SysUserRole类
@TableName(value ="sys_user_role")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SysUserRole implements Serializable {
/**
*
*/
@TableId
private Integer userid;
/**
*
*/
private Integer roleid;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}
3.7 Mapper 类
略
3.8 创建 UserDetatilsService 接口的实现类
根据 username 从数据查询账号信息 SysUser 对象。
在根据 user 的 id, 去查 Sys_Role 表获取 Role 信息
@Configuration
public class JdbcUserDetailsService implements UserDetailsService {
@Autowired
SysUserService sysUserService;
@Autowired
SysRoleService sysRoleService;
@Autowired
SysUserRoleService sysUserRoleService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysUser::getUsername,username);
SysUser sysUser = sysUserService.getOne(wrapper);
sysUser.setExpired(true);
List<GrantedAuthority> authorities = new ArrayList<>();
if (sysUser != null) {
List<SysUserRole> sysUserRoles = sysUserRoleService.query().eq("userid", sysUser.getId()).list();
sysUserRoles.
forEach((sysUserRole -> {
Integer roleid = sysUserRole.getRoleid();
SysRole sysRole = sysRoleService.getById(roleid);
GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_"+sysRole.getRolename());
authorities.add(authority);
}));
sysUser.setAuthorities(authorities);
return sysUser;
}
return null;
}
}
3.9 创建 Controller
- IndexController : 转发到首页 index.html
@Controller
public class IndexController {
@GetMapping("/index")
public String toIndexHtml() {
return "forward:/index.html";
}
}
- MyController
@RestController
public class MyController {
@GetMapping(value = "/access/user",produces = "text/html;charset=utf-8")
public String sayUser() {
return "lisi是user角色";
}
@GetMapping(value = "/access/read",produces = "text/html;charset=utf-8")
public String sayRead() {
return "zhangsan是read角色";
}
@GetMapping(value = "/access/admin",produces = "text/html;charset=utf-8")
public String sayAdmin() {
return "admin是admin角色";
}
}
3.10 继承 WebSecurityConfigurerAdapter
注入自定义的 UserDetatilsService
@Configuration
@EnableWebSecurity
public class CustomSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/index","/mylogin.html","/login","/error.html").permitAll()
.antMatchers("/index.html").permitAll()
.antMatchers("/access/user").hasAnyRole("USER")
.antMatchers("/access/admin").hasAnyRole("ADMIN")
.antMatchers("/access/read").hasAnyRole("READ")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/mylogin.html")
.loginProcessingUrl("/login")
.failureUrl("/error.html")
.and()
.csrf().disable();
}
}
3.14 默认的登录页面
- 访问地址 /login
- 请求方式 post
- 请求参数
用户名 username
密码 password
第四章 自定义登录和验证码的使用
4.1 完善自定义登录页面
- 创建页面
登录页面 resources/static/mylogin.html
action: /login 可以自定义
method:post 这个是一定的。
提交表单的name参数: username ,password 可以自定义
http.usernameParameter("myname")
http.passwordParameter("mypwd")
- 设置自定义登录参数
重写 protected void configure(HttpSecurity http) 方法
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// ①:设置访问的白名单,无需登录验证就可以访问的地址
.antMatchers("/index","/mylogin.html","/login","/error.html").permitAll()
.antMatchers("/index.html").permitAll()
.antMatchers("/access/user").hasAnyRole("USER")
.antMatchers("/access/admin").hasAnyRole("ADMIN")
.antMatchers("/access/read").hasAnyRole("READ")
.anyRequest().authenticated()
.and()
.formLogin()
//②:指定登录页面,登录的 uri 地址
.loginPage("/mylogin.html")
.loginProcessingUrl("/login")
//③:指定登录错误的提示页面
.failureUrl("/error.html")
.and()
//3.关闭跨域访问的安全设置
.csrf().disable();
}
4.2 ajax 登录方式
上面的登录方式是 基于表单 form 的。 对于现在的前后端分类的方式不适合。 如果要使用前后端分离, 一般使用 json 作为数据的交互格式。 需要使用另一种方式才可以。
ajax 方式,用户端发起请求, springsecurity 接收请求验证用户的用户名和密码,把验证结果返回给请求方(json 数据)
尚硅谷内容
3.7 注解使用
3.7.1 @Secured
判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_“。使用注解先要开启注解功能!
@EnableGlobalMethodSecurity(securedEnabled=true)
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled=true)
public class DemosecurityApplication {
public static void main(String[] args){
SpringApplication.run(DemosecurityApplication.class, args);
}
}
在控制器方法上添加注解
// 测试注解:
@RequestMapping("testSecured")
@ResponseBody
@Secured({"ROLE_normal","ROLE_admin"})
public String helloUser() {
return "hello,user";
}
@Secured({"ROLE_normal","ROLE_管理员"})
3.7.2 @PreAuthorize
先开启注解功能:
@EnableGlobalMethodSecurity(prePostEnabled = true)
@PreAuthorize:注解适合进入方法前的权限验证, @PreAuthorize 可以将登录用户的 roles/permissions 参数传到方法中。
@RequestMapping("/preAuthorize")
@ResponseBody
//@PreAuthorize("hasRole('ROLE_管理员')")
@PreAuthorize("hasAnyAuthority('menu:system')")
public String preAuthorize(){
System.out.println("preAuthorize");
return "preAuthorize";
}
3.7.3 @PostAuthorize
先开启注解功能:
@EnableGlobalMethodSecurity(prePostEnabled = true)
@PostAuthorize 注解使用并不多,在方法执行后再进行权限验证,适合验证带有返回值
的权限.
@RequestMapping("/testPostAuthorize")
@ResponseBody
@PostAuthorize("hasAnyAuthority('menu:system')")
public String preAuthorize(){
System.out.println("test--PostAuthorize");
return "PostAuthorize";
}
3.7.4 @PostFilter
@PostFilter :权限验证之后对数据进行过滤 留下用户名是 admin1 的数据表达式中的 filterObject 引用的是方法返回值 List 中的某一个元素
@RequestMapping("getAll")
@PreAuthorize("hasRole('ROLE_管理员')")
@PostFilter("filterObject.username == 'admin1'")
@ResponseBody
public List<UserInfo> getAllUser(){
ArrayList<UserInfo> list = new ArrayList<>();
list.add(new UserInfo(1l,"admin1","6666"));
list.add(new UserInfo(2l,"admin2","888"));
return list;
}
3.7.5 @PreFilter
PreFilter: 进入控制器之前对数据进行过滤
@RequestMapping("getTestPreFilter")
@PreAuthorize("hasRole('ROLE_管理员')")
@PreFilter(value = "filterObject.id%2==0")
@ResponseBody
public List<UserInfo> getTestPreFilter(@RequestBody List<UserInfo> list){
list.forEach(t-> {
System.out.println(t.getId()+"\t"+t.getUsername());
});
return list;
}
3.7.6 权限表达式
3.8 基于数据库的记住我
3.8.1 创建表
CREATE 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;
3.8.2 实现 WebSecurityConfigurerAdapter中的方法
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginProcessingUrl("/login")
.loginPage("/login.html")
.defaultSuccessUrl("/hello")
.and()
.authorizeRequests()
.antMatchers("/login.html").permitAll()
.anyRequest().authenticated()
.and()
//记住我配置
.rememberMe()
.tokenRepository(persistentTokenRepository())
.userDetailsService(userDetailsService)
.tokenValiditySeconds(60)
.and()
.csrf().disable();
}
3.8.3声明 PersistentTokenRepository 对象
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl();
repository.setDataSource(dataSource);
// 自动创建表,第一次执行会创建,以后要执行就要删除掉!
// repository.setCreateTableOnStartup(true);
return repository;
}
3.8.4 HTML页面
<form action="/login" method="post">
username:<input type="text" name="username"><br>
password:<input type="password" name="password"><br>
记住我:<input type="checkbox" name="remember-me" title="记住密码"><br>
<input type="submit">
</form>
3.9 测试
略
3.10 CSRF
3.10.1 CSRF 理解
跨站请求伪造(英语:Cross-site request forgery),也被称为 one-clickattack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的 Web 应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了 web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。从 Spring Security 4.0 开始,默认情况下会启用 CSRF 保护,以防止 CSRF 攻击应用
程序,Spring Security CSRF 会针对 PATCH,POST,PUT 和 DELETE 方法进行防护。
3.10.2 案例
在登录页面添加一个隐藏域:
<input type="hidden"th:if="${_csrf}!=null"th:value="${_csrf.token}"name="_csrf"/>
关闭安全配置的类中的 csrf
// http.csrf().disable();
3.10.3 Spring Security 实现 CSRF 的原理:
- 生成 csrfToken 保存到 HttpSession 或者 Cookie 中。
- 请求到来时,从请求中提取 csrfToken,和保存的 csrfToken 做比较,进而判断当
前请求是否合法。主要通过 CsrfFilter 过滤器来完成。