Spring Security
一、 Spring Security 简介
1 概括
Spring Security 是一个高度自定义的 安全框架。利用 Spring IoC/DI和 AOP 功能,为系统提供了声明式安全访问控制功能,减少了为系统安全而编写大量重复代码的工作。
使用 Spring Secruity 的原因有很多,但大部分都是发现了 javaEE的 Servlet 规范或 EJB 规范中的安全功能缺乏典型企业应用场景。同时认识到他们在 WAR 或 EAR 级别无法移植。因此如果你更换服务器环境,还有大量工作去重新配置你的应用程序。使用 Spring Security解决了这些问题,也为你提供许多其他有用的、可定制的安全功能。
正如你可能知道的两个应用程序的两个主要区域是“ 认证”和“ 授权”(或者访问控制)。这两点也是 Spring Security 重要核心功能。“认证”,是建立一个他声明的主体的过程(一个“主体”一般是指用户,设备或一些可以在你的应用程序中执行动作的其他系统),通俗点说就是系统认为用户是否能登录。“授权”指确定一个主体是否允许在你的应用程序执行一个动作的过程。通俗点讲就是系统判断用户是否有权限去做某些事情。
2 历史
Spring Security 以“The Acegi Secutity System for Spring” 的名字始于 2003 年年底。其前身为 acegi 项目。起因是 Spring 开发者邮件列表中一个问题,有人提问是否考虑提供一个基于 Spring 的安全实现。限制于时间问题,开发出了一个简单的安全实现,但是并没有深入研究。几周后,Spring 社区中其他成员同样询问了安全问题,代码提供给了这些人。2004 年 1 月份已经有 20 人左右使用这个项目。随着更多人的加入,在 2004 年 3 月左右在 sourceforge 中建立了一个项目。在最开始并没有认证模块,所有的认证功能都是依赖容器完成的,而 acegi 则注重授权。但是随着更多人的使用,基于容器的认证就显现出了不足。acegi 中也加入了认证功能。大约 1 年后 acegi 成为 Spring子项目。
在 2006 年 5 月发布了 acegi 1.0.0 版本。2007 年底 acegi 更名为Spring Security。
二、第一个 Spring Security 项目
1 导入依赖
Spring Security 已经被 Spring 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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.bjsxt</groupId>
<artifactId>SpringSecurityDemo</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.1.6.RELEASE</version>
</parent>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
</project>
package com.bjsxt.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class LoginController {
@RequestMapping("/login")
public String login() {
System.out.println("执行login方法");
return "redirect:main.html";
}
}
package com.bjsxt;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MyApp {
public static void main(String[] args) {
SpringApplication.run(MyApp.class, args);
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/login" method="post">
用户名:<input type="text" name="username"/><br/>
密码:<input type="password" name="password"/><br/>
<input type="submit" value="OK">
</form>
</body>
</html>
2 访问页面
导入 spring-boot-starter-security 启动器后,Spring Security 已经生效,默认拦截全部请求,如果用户没有登录,跳转到内置登录页面。
在项目中新建 login.html 页面后在浏览器输入: http://localhost:8080/login.html 后会显示下面页面
默认的 username 为 user,password 打印在控制台中。当然了,同学们显示的肯定和我的不一样。
在浏览器中输入账号和密码后会显示 login.html 页面内容。
三、 UserDetailsService 详解
当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。而在实际项目中账号和密码都是从数据库中查询出来的。所以我们要通过自定义逻辑控制认证逻辑。
如果需要自定义逻辑时,只需要实现 UserDetailsService 接口即可。接口定义如下:
1 返回值
返回值 UserDetails 是一个接口,定义如下要想返回 UserDetails 的实例就只能返回接口的实现类。Spring
Security 中提供了如下的实例。对于我们只需要使用里面的 User 类即可。注意 User 的全限定路径是:
org.springframework.security.core.userdetails.User
此处经常和系统中自己开发的 User 类弄混。
在 User 类中提供了很多方法和属性。
其中构造方法有两个,调用其中任何一个都可以实例化
UserDetails 实现类 User 类的实例。而三个参数的构造方法实际上也是调用 7 个参数的构造方法。
username:用户名
password:密码
authorities:用户具有的权限。此处不允许为 null
此处的用户名应该是客户端传递过来的用户名。而密码应该是从数据库中查询出来的密码。Spring Security 会根据 User 中的 password和客户端传递过来的 password 进行比较。如果相同则表示认证通过,如果不相同表示认证失败。
authorities 里面的权限对于后面学习授权是很有必要的,包含的所有内容为此用户具有的权限,如有里面没有包含某个权限,而在做某个事情时必须包含某个权限则会出现 403。通常都是通过
AuthorityUtils.commaSeparatedStringToAuthorityList(“”) 来 创 建authorities 集合对象的。参数是一个字符串,多个权限使用逗号分隔。
2 方法参数
方法参数表示用户名。此值是客户端表单传递过来的数据。默认情况下必须叫 username,否则无法接收。
3 异常
UsernameNotFoundException 用 户 名 没 有 发 现 异 常 。 在loadUserByUsername 中是需要通过自己的逻辑从数据库中取值的。如果 通 过 用 户 名 没 有 查 询 到 对 应 的 数 据 , 应 该 抛 出
UsernameNotFoundException,系统就知道用户名没有查询到。
四、 PasswordEncoder 密码解析器详解
Spring Security 要求容器中必须有 PasswordEncoder 实例。所以当自定义登录逻辑时要求必须给容器注入 PaswordEncoder 的 bean 对象
1 接口介绍
encode():把参数按照特定的解析规则进行解析。
matches()验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹配,则返回 true;如果不匹配,则返回 false。
第一个参数表示需要被解析的密码。第二个参数表示存储的密码。
upgradeEncoding():如果解析的密码能够再次进行解析且达到更安全的结果则返回 true,否则返回 false。默认返回 false。
2 内置解析器介绍
在 Spring Security 中内置了很多解析器。
3 BCryptPasswordEncoder 简介
BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,平时多使用这个解析器。
BCryptPasswordEncoder 是对 bcrypt 强散列方法的具体实现。是基于 Hash 算法实现的单向加密(只能加密不能解密)。可以通过 strength 控制加密强度,默认 10.
4 代码演示
在 项 目 src/test/java 下 新 建 com.bjsxt.MyTest 测 试BCryptPasswordEncoder 用法。
package com.bjsxt;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.junit4.SpringRunner;
@SpringBootTest
@RunWith(SpringRunner.class)
public class MyTest {
@Test
public void test() {
PasswordEncoder pe = new BCryptPasswordEncoder();
String encode = pe.encode("123");
System.out.println(encode);
}
}
BUG:测试代码少了@Test注解
判断是否匹配
不匹配
BUG: 包名包结构还必须一样
五、 自定义登录逻辑
当 进 行 自 定 义 登 录 逻 辑 时 需 要 用 到 之 前 讲 解 的UserDetailsService 和 PasswordEncoder。
但是 Spring Security 要求:
当进行自定义登录逻辑时容器内必须有 PasswordEncoder 实例。所以不能直接 new 对象。
1 编写配置类
新建类 com.bjsxt.config.SecurityConfig 编写下面内容
package com.bjsxt.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder getPe() {
return new BCryptPasswordEncoder();
}
}
2 自定义逻辑
在 Spring Security 中实现 UserDetailService 就表示为用户详情服务。在这个类中编写用户认证逻辑。
package com.bjsxt.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired//注入密码解析器
private PasswordEncoder encoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询数据库判断用户名是否存在,如果不存在抛出UsernameNotFoundException
if (!username.equals("admin")) {
throw new UsernameNotFoundException("用户名不存在!!!");
}
//将查询出来的密码进行解析,或直接将password放到构造方法中,数据库中存在的是密码加密后的结果
String password = encoder.encode("123");
return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
3 查看效果
重启项目后,在浏览器中输入账号:admin,密码:123。后可以正确进入到 login.html 页面。
六、 自定义登录页面
虽然 Spring Security 给我们提供了登录页面,但是对于实际项目中,大多喜欢使用自己的登录页面。所以 Spring Security 中不仅仅提供了登录页面,还支持用户自定义登录页面。实现过程也比较简单,只需要修改配置类即可。
package com.bjsxt.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login.html");
}
@Bean
public PasswordEncoder getPe() {
return new BCryptPasswordEncoder();
}
}
重启项目 原因:所有的请求都需要拦截,必须登陆后才能访问,会跳到login.html,但是访问login.html也需要认证,如此陷入死循环
解决办法,对login.html进行放行
访问main.html回车 跳转到login.html
在业务层添加打印语句
登录打印语句并未打印
再配置两行代码
会执行代码,但是跳转到404
package com.bjsxt.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* 自定义登录页面
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//用户认证
http.formLogin()
.loginProcessingUrl("/login")//当发现请求是/login时认为是登录,需要执行UserDetailsServiceImpl
.successForwardUrl("/toMain")//此处是一个post请求得用过控制器跳转,直接请求会报405错误
.loginPage("/login.html");
//URL拦截
http.authorizeRequests()
.antMatchers("/login.html").permitAll()// /login.html 不需要拦截,放行
.anyRequest().authenticated();//所有的请求都需要被认证,登录后才能访问
//关闭CSRF防护
http.csrf().disable();
}
@Bean
public PasswordEncoder getPe() {
return new BCryptPasswordEncoder();
}
}
重启项目,成功登录
1 编写登录页面
别写登录页面,登录页面中的 action 不编写对应控制器也可以。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/login" method="post">
用户名:<input type="text" name="username"/><br/>
密码:<input type="password" name="password"/><br/>
<input type="submit" value="OK">
</form>
</body>
</html>
2 修改配置类
修改配置类中主要是设置哪个页面是登录页面。配置类需要继承WebSecurityConfigurerAdapte,并重写 configure 方法。
successForwardUrl()登录成功后跳转地址
loginPage() 登录页面
loginProcessingUrl 登录页面表单提交地址,此地址可以不真实存在。
antMatchers():匹配内容
permitAll():允许
package com.bjsxt.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//表单认证
http.formLogin()
.loginProcessingUrl("/login") //当发现是/login 时认为是登录需要执行UserDetailsServiceImpl
.successForwardUrl("/toMain") //此处是一个post请求 得通过控制器跳转,直接请求页面会报405错误
.loginPage