SpringSecurity学习
一、初始SpringSecurity
Spring 是非常流行和成功的 Java 应用开发框架,Spring Security 正是 Spring 家族中的成员。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性
的完整解决方案。
1.1 相关概念
不管你有没有了解过安全框架,但这里都要了解以下两个概念:
白话:
- 授权:你能干什么? 通俗点讲就是系统判断用户是否有权限去做某些事情。
- 认证:你是谁? 通俗点说就是系统认为用户是否能登录
官方解释:
- 授权:
验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码
来完成认证过程。 - 认证:
是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色
,而每个角色则对应一系列的权限。
1.2 类似产品 PK🕵️
1.2.1 Shiro
Apache 旗下的轻量级权限控制框架。
官方网址:https://shiro.apache.org/
特点:
- 轻量级。Shiro 主张的理念是把复杂的事情变简单。针对对性能有更高要求
的互联网应用有更好表现。 - 通用性。
- 好处:不局限于 Web 环境,可以脱离 Web 环境使用。
- 缺陷:在 Web 环境下一些特定的需求需要手动编写代码定制
Shiro在整合SSM框架的时候,相比于SpringSedcurity而言,比较简单,在早些年SpringBoot和SpringCloud没有流行的时候,Shiro很火爆
1.2.2 SpringSecurity
Spring 技术栈的组成部分。
官方网址:https://spring.io/projects/spring-security
特点:
- 和 Spring 无缝整合。
- 全面的权限控制。
- 专门为 Web 开发而设计。
- 旧版本不能脱离 Web 环境使用。
- 新版本对整个框架进行了分层抽取,分成了核心模块和 Web 模块。单独引入核心模块就可以脱离 Web 环境。
- 重量级。
二、SpringSecurity实现登录验证授权的三种方式
2.1 环境搭建
- 新建一个SpringBoot的项目
- 引入WEB模块和SpringSecurity模块
- 编写controller测试
- 输入映射地址,则会跳转至springsecurity提供的登录页,进行登录认证
- 输入用户名和密码
密码默认是在编辑器的控制台输出,用户名默认是user
2.1 方式一、通过配置文件
在application.properties/application.yml里编写配置
server.port: 8888
# SpringSecurity配置文件方式配置用户名和密码 【在实际用户中不用,都是查找数据库中的用户和密码】
spring.security.user.name=root
spring.security.user.password=root
2.2 方式二、通过编写配置类实现
1、新建一个Config的配置类
2、集成WebSecurityConfigurerAdapter
的类
3、重写里面的方法·configure(AuthenticationManagerBuilder auth)
4、注意:如果使用密码加密进行加密密码,则必须要通过@Bean注解注入PasswordEncoder
【实例】:
@Configuration
public class SecurityAuthConfig extends WebSecurityConfigurerAdapter{
/**
* 手动创建一个SpringSecurity的接口实现来完成对密码的加密操作
* @return
*/
@Bean
PasswordEncoder password() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 加密操作 必须要PasswordEncoder这个接口的支持
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String encodePwd = bCryptPasswordEncoder.encode("123456");
// 把密码和用户放到内存中
auth.inMemoryAuthentication().withUser("lucy").password(encodePwd).roles("admin");
}
}
2.3 方式三、通过数据库进行数据查询验证登录
1、新建一个数据库Student
CREATE DATABASE springsecurity;
DROP TABLE IF EXISTS `student`;
CREATE TABLE `student` (
`id` INT NOT NULL AUTO_INCREMENT,
`username` VARCHAR(30) NOT NULL,
`password` VARCHAR(200) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=INNODB;
2、导入MyBatis-Plus依赖、MySQL8.0.27依赖、Lombok依赖[可有可无]
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version><!--$NO-MVN-MAN-VER$-->
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
3、新建一个vo实体Student
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class Student {
private Integer id;
private String username;
private String password;
}
4、编写一个接口实现BaseMapper
@Repository("studentMapper")
public interface StudentMapper extends BaseMapper<Student>{
}
5、编写一个Service调用Mapper的方法
@Service("userDetailsService")
public class MyUserDetailService implements UserDetailsService {
/**
* 注入StudentMapper接口
*/
@Autowired
@Qualifier("studentMapper")
private StudentMapper studentMapper;
/**
* 根据用户名实现操作 返回UserDetail
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 在这里面可以查询数据库 将查询到的结构封装成一个User对象 User对象是Security的一个内置的对象
QueryWrapper<Student> warpper = new QueryWrapper<>();
warpper.eq("username", username);
Student student = studentMapper.selectOne(warpper);
// 判断 数据是否存在
if (student==null) { // 数据库无此对象 认证失败
// 如果不存在直接抛出异常 用户不存在
throw new UsernameNotFoundException("用户名不存在");
}
/**
* 权限添加一般是数据库中查找
*/
// 角色授予 授权
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
/**
* 三个参数:1、用户名2、密码(一般都要加密)3、权限
*/
// 认证
return new User(student.getUsername(), new BCryptPasswordEncoder().encode(student.getPassword()), auths);
}
}
7、编写一个配置类
@Configuration
public class SecurityConstomAuthConfig extends WebSecurityConfigurerAdapter{
// 通过 数据库查找完成自定义的登陆用户名和密码设置
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
8、配置核心配置文件 用于连接数据库的操作
# 配置数据库信息
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/springsecurity?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
9、测试
输入用户名和密码后即可登录
三、自定义登陆页面
1、在配置类里去实现一个configure(HttpSecurity http)的方法,基于http请求的自定义方式
/**
* 自定义登陆页面
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login.html") // 指定登录页面
.loginProcessingUrl("/stu/login") // 登陆表单action的值 登陆处理拦截的请求地址 SpringSecuirty帮我做好了 我们不需要在做
.defaultSuccessUrl("/test/index").permitAll() // 登陆成功后跳转的路径
.and().authorizeRequests() // 授权请求
.antMatchers("/","/test/hello","/stu/login").permitAll() // 设置那些路径可以直接访问,无需认证
.anyRequest().authenticated()
.and().csrf().disable(); // 关闭csrf的防护
}
2、创建一个controller 映射地址给index,即成功后跳转的操作映射
@RequestMapping("/index")
public String secuirty() {
return "hello111";
}
3、新建一个登陆页面在static目录下。注意:static目录是springboot默认的静态资源目录,默认就是可以访问的。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<form action="/stu/login" method="post">
用户名:<input type="text" name="username"> <br>
密码:<input type="text" name ="password"> <br>
<input type="submit" value="提交">
</form>
</body>
</html>
测试:访问/test/hello可以直接访问,访问test/index 跳转至login.html进行登录
三、基于角色或权限访问 RBAC
3.1 hasAuthority 方法
如果当前的主体具有指定的权限,则返回true,否则返回false
局限性:只能设置某一个权限 当权限查过一个则无法设置
在配置类的重写的configuration的类添加一个antMatchers("/test/index").hasAuthority("admin")
的方法,允许只有admin的权限才可以登录
/**
* 自定义登陆页面
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login.html") // 指定登录页面
.loginProcessingUrl("/stu/login") // 登陆表单action的值 登陆处理拦截的请求地址 SpringSecuirty帮我做好了 我们不需要在做
.defaultSuccessUrl("/test/index").permitAll() // 登陆成功后跳转的路径
.and().authorizeRequests() // 授权请求
.antMatchers("/","/test/hello","/stu/login").permitAll() // 设置那些路径可以直接访问,无需认证
.antMatchers("/test/index").hasAuthority("admin") // 当前登录的admin角色权限的才可以访问
.anyRequest().authenticated()
.and().csrf().disable(); // 关闭csrf的防护
}
然后再去service的类里去为查询到的用户添加角色,首先把授予的角色设置成abc,与自定义的登陆页面所规定的角色admin不一致,
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("abc");
测试:如下图:出现403, (type=Forbidden, status=403).的错误,进制无权用户访问。
底层源码:
private static String hasAuthority(String authority) {
return "hasAuthority('" + authority + "')";
}
3.2 hasAnyAuthority 方法
102人
如果当前的主体有任何提供的角色(给定的作为一个逗号分隔的字符串列表)的话,
返回true.
访问ht://localhost:8090/find.
在配置类中添加一个hasAnyAuthority("admin,manager")
可以实现基于多个权限的访问
/**
* 自定义登陆页面
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login.html") // 指定登录页面
.loginProcessingUrl("/stu/login") // 登陆表单action的值 登陆处理拦截的请求地址 SpringSecuirty帮我做好了 我们不需要在做
.defaultSuccessUrl("/test/index").permitAll() // 登陆成功后跳转的路径
.and().authorizeRequests() // 授权请求
.antMatchers("/","/test/hello","/stu/login").permitAll() // 设置那些路径可以直接访问,无需认证
// .antMatchers("/test/index").hasAuthority("admin") // 当前登录的admin角色权限的才可以访问
.antMatchers("/test/index").hasAnyAuthority("admin,manager") // 支持多个角色
.anyRequest().authenticated()
.and().csrf().disable(); // 关闭csrf的防护
}
底层源码:
private static String hasAnyAuthority(String... authorities) {
String anyAuthorities = StringUtils.arrayToDelimitedString(authorities, "','");
return "hasAnyAuthority('" + anyAuthorities + "')";
}
参数是一个String类型的可变形参,根据形参的逗号作为分割符,来分配权限
3.3. hasrole 方法
如果用户具备指定角色及可以访问,否则出现403,如果当前主体具有指定的角色,则返回true
/**
* 自定义登陆页面
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login.html") // 指定登录页面
.loginProcessingUrl("/stu/login") // 登陆表单action的值 登陆处理拦截的请求地址 SpringSecuirty帮我做好了 我们不需要在做
.defaultSuccessUrl("/test/index").permitAll() // 登陆成功后跳转的路径
.and().authorizeRequests() // 授权请求
.antMatchers("/","/test/hello","/stu/login").permitAll() // 设置那些路径可以直接访问,无需认证
.antMatchers("/test/index").hasRole("sale") // 只能授予单个权限
.anyRequest().authenticated()
.and().csrf().disable(); // 关闭csrf的防护
}
底层源码:
private static String hasRole(String rolePrefix, String role) {
Assert.notNull(role, "role cannot be null");
Assert.isTrue(rolePrefix.isEmpty() || !role.startsWith(rolePrefix), () -> "role should not start with '"
+ rolePrefix + "' since it is automatically inserted. Got '" + role + "'");
return "hasRole('" + rolePrefix + role + "')";
}
发现这里需要给角色加上一个前缀Role_
,因为底层进行了封装
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 在这里面可以查询数据库 将查询到的结构封装成一个User对象 User对象是Security的一个内置的对象
QueryWrapper<Student> warpper = new QueryWrapper<>();
warpper.eq("username", username);
Student student = studentMapper.selectOne(warpper);
// 判断 数据是否存在
if (student==null) { // 数据库无此对象 认证失败
// 如果不存在直接抛出异常 用户不存在
throw new UsernameNotFoundException("用户名不存在");
}
/**
* 权限添加一般是数据库中查找
*/
// List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
// 角色授予 授权 给查出的用户服务admin的角色权限
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_sale");
/**
* 三个参数:1、用户名2、密码(一般都要加密)3、权限
*/
//return new User("user", new BCryptPasswordEncoder().encode("123456"), auths);
// 认证
return new User(student.getUsername(), new BCryptPasswordEncoder().encode(student.getPassword()), auths);
}
3.4 hasAnyRole 方法
同理和hasAnyAuthority
```java
/**
* 自定义登陆页面
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login.html") // 指定登录页面
.loginProcessingUrl("/stu/login") // 登陆表单action的值 登陆处理拦截的请求地址 SpringSecuirty帮我做好了 我们不需要在做
.defaultSuccessUrl("/test/index").permitAll() // 登陆成功后跳转的路径
.and().authorizeRequests() // 授权请求
.antMatchers("/","/test/hello","/stu/login").permitAll() // 设置那些路径可以直接访问,无需认证
.antMatchers("/test/index").hasRole("sale") // 只能授予单个权限
.anyRequest().authenticated()
.and().csrf().disable(); // 关闭csrf的防护
}
底层源码
private static String hasAnyRole(String rolePrefix, String... authorities) {
String anyAuthorities = StringUtils.arrayToDelimitedString(authorities, "','" + rolePrefix);
return "hasAnyRole('" + rolePrefix + anyAuthorities + "')";
}
3.5 自定义403页面
403:即没有权限访问的页面
设置只需要在configure(HttpSecurity http)方法中配置一个http.exceptionHandling().accessDeniedPage("/unAuth.html");
的方法,参数集为指定的403页面
四、认证授权注解
4.1 @Secured
判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_”
使用注解先要开启注解功能@EnableGlobalMethodSecurity(securedEnabled = true)
在controller层中方法上添加上该注解,注解的参数可以添加角色,是一个数组
@RequestMapping("/update")
@Secured({"ROLE_sale","ROLE_manager"})
public String update() {
return "hello update";
}
测试访问:http:localhost:8088/test/update,输入用户名和密码;
4.2 @PreAuthorize
- 在启动类上开启注解支持
@EnableGlobalMethodSecurity(prePostEnabled = true)
- 在controller的方法上加上该注解
@PreAuthorize("hasAnyAuthority('menu')")
- 在servcie层设置权限
List auths = AuthorityUtils.commaSeparatedStringToAuthorityList(“menu”);
@RequestMapping("/update")
@PreAuthorize("hasAnyAuthority('menu')")
public String update() {
return "hello update";
}
4.3 @PostAuthorize
在方法之后进行校验
- 在启动类上开启注解支持
@EnableGlobalMethodSecurity(prePostEnabled = true)
- 在controller的方法上加上该注解
@PostAuthorize("hasAnyAuthority('menus')")
@RequestMapping("/update")
// 在方法执行之后校验
@PostAuthorize("hasAnyAuthority('menus')")
public String update() {
System.out.println("update...");
return "hello update";
}
- 在servcie层设置权限
menu
理论上讲是访问无权限的,这是看控制台输出结果,看是否有update。。。的输出结果
五、用户注销功能
- 在配置类中添加退出的page
// 配置退出功能 logoutUrl("")退出的映射url logoutSuccessUrl退出成功后的执行映射 permitAll允许所有权限
http.logout().logoutUrl("/logout").logoutSuccessUrl("/test/hello").permitAll();
- 修改配置类,登陆成功后,跳转至成功的页面
- 在成功的页面里添加一个超链接,链接地址为刚才设置的logoutUrl中的地址映射
原理类似于Session,注销将session销毁
六、基于数据库自动登录
传统方式cookie:
缺点:存储在客户端安全性不高,且当cookie众多的时候,容易造成系统性能下降的缺点
6.1 springsecurity的自动登录
底层实现原理:
1.创建一张实体表:
CREATE TABLE persistent_logins (
`username` VARCHAR(64) NOT NULL,
`series` VARCHAR(64) PRIMARY KEY,
`token` VARCHAR(64) NOT NULL,
`last_used` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE
CURRENT_TIMESTAMP
) ENGINE=INNODB DEFAULT CHARSET=UTF8;
- 在配置类中诸如数据源,配置操作数据库的对象,创建
JdbcTokenRepositoryImpl
// 注入数据源
@Autowired
private DataSource dataSource;
//创建对象PersistentTokenRepository
@Bean
public PersistentTokenRepository getPersistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepositoryImpl = new JdbcTokenRepositoryImpl();
tokenRepositoryImpl.setDataSource(dataSource);
// tokenRepositoryImpl.setCreateTableOnStartup(true);
return tokenRepositoryImpl;
}
- 在配置类里面配置自动登录
@Override
protected void configure(HttpSecurity http) throws Exception {
// 配置退出功能 logoutUrl("")退出的映射url logoutSuccessUrl退出成功后的执行映射 permitAll允许所有权限
http.logout().logoutUrl("/logout").logoutSuccessUrl("/test/hello").permitAll();
// 配置没有权限访问的也面
http.exceptionHandling().accessDeniedPage("/unAuth.html");
http.formLogin()
.loginPage("/login.html") // 指定登录页面
.loginProcessingUrl("/stu/login") // 登陆表单action的值 登陆处理拦截的请求地址 SpringSecuirty帮我做好了 我们不需要在做
.defaultSuccessUrl("/success.html").permitAll() // 登陆成功后跳转的路径
.and().authorizeRequests() // 授权请求
.antMatchers("/","/test/hello","/stu/login").permitAll() // 设置那些路径可以直接访问,无需认证
.antMatchers("/test/index").hasAnyRole("admin,sale")
.anyRequest().authenticated()
.and()
.rememberMe().tokenRepository(getPersistentTokenRepository())
.tokenValiditySeconds(60) // 设置token的过期时间 单位是以秒为单位
.userDetailsService(userDetailsService) // 设置服务
.and().csrf().disable(); // 关闭csrf的防护
}
- 在登陆页面中完成复选框是实现自动登录,在登录页上设置复选框的name属性值必须为
remember-me
<form action="/stu/login" method="post">
用户名:<input type="text" name="username"> <br>
密码:<input type="text" name ="password"> <br>
<input type="submit" value="提交">
<input type="checkbox" name="remember-me"> 自动登录
</form>