Spring Security 简介
基于Spring 框架,提供了一套Web 应用安全性的完整解决方案。
关于安全方面的主要两个区域是 “认证” 和 “授权” (或者访问控制),一般来说,Web应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分,这两点是 Spirng Security 重要核心功能。
- 用户认证:**就是系统认为用户是否能登录。**验证某个用户是否为系统中的合法体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。
- 用户授权:**就是系统判断用户是否有权限去做某些事请。**在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。
入门案例
创建 Spring Boot 工程
使用spring boot 快速创建工具创建一个boot工程
引入相关依赖
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.kanan</groupId>
<artifactId>secruity-demo-1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>security-demo-1</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
编写 Controller 测试
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("hello")
public String hello(){
return "Hello Security";
}
}
localhost:80/test/hello //进行访问
打开了一个登录页面,表示开启成功
默认的用户名:user
默认密码:每次启动时,在控制台会生成一个随机密码
Using generated security password: a2364a8b-5504-43d4-9ab4-93d690fe264b
登录成功后就可以看的我们的 hello security
基本原理
- 它底层其实是一个过滤器链
代码底层流程:重点有三个过滤器
**FilterSecurityInterceptor:**是一个方法级的权限过滤器,基本位于过滤器链的最底层
-
super.beforeInvocation(filterInvocation):表示查看之前的filter 是否通过
-
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse()):表示真正调用后台的服务
**ExceptionTranslationFilter:**是一个异常过滤器,用来处理在认证授权过程中抛出的异常
**UsernamePasswordAuthenticationFilter:**对/login的POST请求做拦截,校验表单中用户名,密码。
过滤器是如何加载
- 使用 Spring Security 配置过滤器
- DelegatingFilterProxy
UserDetailsService 接口
当我们没有进行配置时,账号和密码是由Spring Security定义生成的。而在实际项目中账号和密码应该都是从数据库中查询出来的。所以我们要通过自定义逻辑控制认证逻辑。
- 自定义逻辑
- 创建类继承 UsernamePasswordAuthenticationFilter 类,重写三个方法
- 并且实现 UserDetailsService 接口,编写查询数据过程,返回 User对象
- 这个User 对象是安全框架提供的对象
PasswordEncoder 接口
- 数据加密接口,用于返回 User 对象里的密码加密
Web 权限方案
- 设置登录的用户名和密码
通过配置文件
-
使用 yml 配置文件
spring: security: user: name: kanan password: 123456
通过配置类
- 创建一个配置类继承 WebSecurityConfigurerAdapter 这个类 重写它的 configure 方法
- 在类上添加 @Configuration 注解,表示这个类是一个配置类
- 当我们使用 BCryptPasswordEncoder 类(实现了 PasswordEncoder 接口)加密时需要将类注入到容器中
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//创建一个加密类
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String password = passwordEncoder.encode("123456");
auth.inMemoryAuthentication().withUser("kanan").password(password).roles("admin");
}
@Bean
PasswordEncoder password(){
return new BCryptPasswordEncoder();
}
}
通过自定义实现类
- 创建配置类,设置使用哪个userDetailsService 实现类
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(password());
}
@Bean
PasswordEncoder password(){
return new BCryptPasswordEncoder();
}
}
- 编写实现类,返回 User 对象,User 对象有用户名密码和操作权限
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
return new User("kanan",
new BCryptPasswordEncoder().encode("123456"),auths);
}
}
实现数据库认证完成用户登入
整合 MyBatis Plus 完成数据库操作
- 引入依赖
<!--mybatis plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
- 创建数据库
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户名',
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户密码',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of users
-- ----------------------------
INSERT INTO `users` VALUES (1, 'kanan', '123456');
INSERT INTO `users` VALUES (2, 'wanan', '654321');
SET FOREIGN_KEY_CHECKS = 1;
- 配置数据源
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql:///security_test?serverTimezone=UTC&rewriteBatchedStatements=true
username: root
password: 123456
- 创建映射表的实体类
@Data
@TableName("users")
public class User {
/**
* 用户ID
*/
@TableId(type = IdType.AUTO) //自增策略
private Integer id;
/**
* 用户名
*/
private String username;
/**
* 用户密码
*/
private String password;
}
- 整合 mp 创建 Mapper 接口继承 BaseMapper
- 在启动类上添加一个 @MapperScan 扫描mapper类的注解
- 由于没有什么业务逻辑,则service 层可以省略不写
@Mapper //因为启动类已经添加注解,所以这里可以不加mapper注解,主要防止主动注入时爆红
public interface UserMapper extends BaseMapper<User> {
}
- 编写自定义的实现类
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//先通过username 查询数据库是否有这个用户
QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
userQueryWrapper.eq("username",username);
User user = this.userMapper.selectOne(userQueryWrapper);
if (user == null){//数据库没有当前用户名,认证失败
throw new UsernameNotFoundException("用户名不存在");
}
//权限设置
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
//从查询数据库返回的user对象,得到用户名和密码
return new User(user.getUsername(),
new BCryptPasswordEncoder().encode(user.getPassword()),auths);
}
}
自定义用户登入界面
- 在配置类中重写 configure 方法的重载方法
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
/**
* 自定义登录页面设置
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() //定义自己编写的登录页面
.loginPage("/login.html") //登录页面设置
.loginProcessingUrl("/user/login") //登录访问路径
.defaultSuccessUrl("/test/index").permitAll() //登录成功后跳转页面
.and().authorizeRequests()
.antMatchers("/","/test/hello","/user/login").permitAll() //设置哪些路径不被保护
.anyRequest().authenticated()
.and().csrf().disable(); //关闭csrf防护
}
}
关于关闭csrf防护,在后面会慢慢说明
- 在资源文件下springboot指定的静态资源目录下,创建 login.html 登录页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/user/login" method="post">
<div>
<lable>用户名:</lable>
<input type="text" name="username">
</div>
<div>
<lable>密 码:</lable>
<input type="password" name="password">
</div>
<div>
<input type="submit" value="登录">
</div>
</form>
</body>
</html>
- 编写一个 index页面的 controller
@RequestMapping("index")
public void index(){
return "Hello Index";
}
- 登录成功后
基于角色和权限访问控制
hasAuthority 方法
如果当前的主体具有指定的权限,有返回 true,没有则返回 false
- 在配置类设置当前访问地址有哪些权限
//表示当前登录用户具有admins权限时才可以访问这个路径
.antMatchers("/test/index").hasAuthority("admins")
- 当没有给权限时,我们再次登录
type=Forbidden:表示禁止访问,就是没有权限
- 在 UserDetailsService,把返回 User对象设置权限
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admins");
- 再次访问成功看到 HelloIndex
hasAnyAuthority 方法
如果当前的主体具有在一些指定权限中是否有一个权限,有返回 true,没有则返回 false
.antMatchers("/test/index").hasAnyAuthority("admins","manage")
- Test 我们给的权限是 admins,则满足条件可以正常访问
hasRole 方法
如果用户具备给定的角色就允许访问,否则出现403
如果当前主体具有指定的角色,则返回true
- 设置访问角色
.antMatchers("/test/index").hasRole("sale")
- 添加访问角色
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admins,ROLE_sale");
-
添加角色时用逗号隔开,注意角色需要添加 ROLE_ 前缀
- 原因:源码展示
private static String hasRole(String role) { Assert.notNull(role, "role cannot be null"); //判断是否为null //在设置访问角色时如果前缀是 ROLE_ 开始,则返回false,并提示,不要自己添加前缀,因为是它自己添加的(在下面retrun中可以看出结果) Assert.isTrue(!role.startsWith("ROLE_"), () -> { return "role should not start with 'ROLE_' since it is automatically inserted. Got '" + role + "'"; }); return "hasRole('ROLE_" + role + "')"; }
- 所以即使我们设置的权限没有 ROLE_ 前缀,而在添加权限时我们要加上这个前缀
hasAnyRole 方法
表示用户具备任何一个条件都可以访问
- 设置角色
.antMatchers("/test/index").hasAnyRole("sale","develop")
自定义 403 界面
自定义 403 没有权限访问的页面
- 在静态资源文件夹中创建一个准备好的页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>没有访问权限</h1>
</body>
</html>
- 设置访问配置类
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
/**
* 自定义登录页面设置
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling().accessDeniedPage("/unAuthority.html");
}
}
- 当我们再次访问没有权限时,会跳转到刚才设置的界面,表示成功
注解的使用
@Secured
判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀 ROLE_
- 使用前提,在启动类上或者配置类上添加下面注解
@EnableGlobalMethodSecurity(securedEnabled = true)
@SpringBootApplication
public class SecurityDemo1Application {
public static void main(String[] args) {
SpringApplication.run(SecurityDemo1Application.class, args);
}
}
- 在 controller的方法上面使用注解,设置角色
@Secured({"ROLE_sale","ROLE_manage"})
@GetMapping("delete")
public String delete(){
return "Hello Delete";
}
@PreAuthorize
注解适合进入方法前的权限验证,可以将登录用户的 roles/permissions 参数传到方法中
@PreAuthorize("hasAnyRole('admins')")
@GetMapping("delete")
public String delete(){
return "Hello Delete";
}
@PostAuthorize
表示方法执行后再进行校验
- 使用前提,开启注解使用(添加在配置类或启动类上)
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
@PostAuthorize("hasAnyRole('admins')") //方法执行之后校验
@GetMapping("delete")
public String delete(){
System.out.println("Hello ROLE_admins");
return "Hello Delete";
}
此时如果用户不是这个角色,那么访问后会跳转到403界面,都是还是会执行方法中的内容
Hello ROLE_admins
@PostFilter
权限验证之后对数据进行过滤
@PostFilter("filterObject.username == 'onana'")//只拿到 username为 onana 的数据,过滤掉其他数据
@PreAuthorize("hasAnyAuthority('admins')")
@GetMapping("getAll")
@ResponseBody
public List<Users> getAllUser(){
List<Users> users = new ArrayList<>();
users.add(new Users(null,"onana","123"));
users.add(new Users(null,"lsisi","123"));
return users;
}
- 结果
[
{
"id": null,
"username": "onana",
"password": "123"
}
]
@PreFilter
权限验证之前对数据进行过滤
用户注销
- 在配置类中设置退出的配置
http.logout()
.logoutUrl("/logout") //设置退出的请求地址
.logoutSuccessUrl("/test/hello").permitAll(); //退出完成后跳转页面
- 添加一个退出的请求路径
<a href="/logout">退出</a>
- 登录成功后跳转到 成功界面,然后可以正常访问有权限的地址,点击退出后再进行访问则需要登陆
自动登录(记住我)
**实现原理:**首先用户登入成功后,会生成一个 token(加密串),这个 token,一边相应给浏览器,放到 cookie中,一边使用 token 和用户信息 存储到数据库中,而以后再次访问时,浏览器获取 cookie信息,拿着cookie信息到数据库进行比对,如果查询到对应信息,则认证成功,实现自动登录
- 建表
DROP TABLE IF EXISTS `persistent_logins`;
CREATE TABLE `persistent_logins` (
`username` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`series` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`token` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`series`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
- 配置类
注入数据源,配置操作数据库对象
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
//注入数据源
@Autowired
private DataSource dataSource;
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//自动创建表
//jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
}
- 在配置类的 configure方法中配置自动登录
http.formLogin()
.and().rememberMe().tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(60) //设置有效时长(s)
.userDetailsService(userDetailsService)
- 在登录页面中添加一个复选框
<div>
<lable>记住我:</lable>
<input type="checkbox" name="remember-me"> <!--这里的name="remember-me" 是必须的 -->
</div>
当我们登入成功后,可以看到 security为我们自动封装的数据已经储存到数据库中了
CSRF
csrf指的是:跨站请求伪造(Cross-site request forgery),也被称为one-click attack 或者 session riding 通常缩写为 CSRF 或者 XSRF,是一种挟制用户在当前已经登录的 Web 应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF利用的是网站对用户网页浏览器的信任。
跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了 web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是不是用户自愿发出的
- 在 spring security 中 默认是开启的。只需要在表单提交中添加一个隐藏对象,就可以实现
<!-- 使用模板引擎 -->
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
**实现原理:**在session中存放一个 token,保护 post,put,delete 请求,判断提交的请求中 token与session 中的是否一致,相同则放行
微服务权限方案
等待更新