Spring Security
一. 权限控制
任何一个管理系统都必须要实现权限控制,它是任何一个系统的基础建设部分,以实现对公司不同岗位、不同角色员工的数据隔离。
在早期的权限控制使用的 ACL(Access Control List) 机制,它实现起来简单,对于使用者来说很麻烦,Linux操作系统就是这种机制,因为该操作系统能够使用的人很少。
现代的权限管理使用的是 RBAC(Role Based Access Control),基于角色的访问权限控制。就是给用户授予不同的角色,角色赋予不同的权限。它开发难度变大,但是使用简单。
二. 权限框架
在spring security诞生之前,业界使用的 shiro 这个权限框架,它可以不依赖与web容器和spring的IOC容器。Spring Security是必须要依赖web容器和spring的IOC容器。Spring security它作为 spring的亲儿子,从出生就备受关注,市场份额逐渐替代shiro。
Spring Security的学习难度要远远高于 shiro,但是我们按照一个简单的方式来实现权限控制。
三. 权限框架解决的问题
3.1 认证(Authentication)
认证就是解决 “我是谁” 的问题,就是用户是否可以访问该系统。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XsK2ZG8S-1677461821703)(images/authentication.jpg)]
3.2 授权(Authorization)
“授权” 表示用户进入系统只能,“能干什么”。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7U7UfFa1-1677461821704)(images/authorization.jpg)]
四. Springboot整合Security
第一步,导入如下的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
4.1 spring security的核心配置
/**
* 1.@EnableWebSecurity 开启 spring secuirty的自定义配置
*/
@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 关于 认证 和 授权的配置,都在该代码中实现
@Override
protected void configure(HttpSecurity http) throws Exception {
/**
* 1. authorizeRequests() 方法的目的是为了配置哪些资源需要认证才能访问,哪些资源不需要认证就可以访问;
* 2. authorizeRequests() 这个方法一旦被调用了,就会覆盖spring security内部的登录表单。
*/
http.authorizeRequests() //
// .antMatchers("/greet").permitAll() //permitAll() 可以不登录直接访问
// /user/** 它可以匹配如下的路径: /user /user/add /user/add/a/b/c
// authenticated() 表示对应的资源必须要认证了才能访问
.antMatchers("/user/**", "/greet").authenticated() // ant 是spring中的一个路径匹配法则,“*” 表示匹配一个单词,“**” 是递归路径
.antMatchers("/login").permitAll()
.and() // and() 方法表示的意思是,要做另外一个块的配置
// .formLogin().disable() // 在前后端分离情况下,需要禁用spring security自带的登录功能
.csrf().disable(); // csrf() 跨站伪造攻击,禁用掉;如果需要发送非 GET 请求,需要将 crsf() 禁用
}
}
五. JWT
5.1 前后端分离的Session
下图为标准的session机制
在前后端分离的时候
5.2 JWT
JWT(Json Web Token),他就是一个字符串,它的出现是为了解决前后端分离登录的处理的。它分为3段,每一段之间是通过 “.” 来分割的,前两段其实就是一个 base64 的字符串,说白就是明文。它具有自验证的功能。
5.2.1 JWT的使用
第一步,需要引入如下的依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
第二步,jwt的生成
@Test
public void generateJWT() {
// 密钥,需要绝对保密的一个字符串
String key = "Qltw8l63IzprU54xLHTK3bzkY40iCLbA26Jo4IZmARXQALz0dBrclPDwkRyOUiDoasOioGtBAPfN0NWmrLC1Og==";
/**
* 将用于加密的字符串生成一个 Key 类型的对象
*/
Key k = new SecretKeySpec(Base64.getDecoder().decode(key), "HmacSHA512");
// 生成一个 JWT的字符串
String jwt = Jwts.builder()
.setSubject("admin") // 用户名信息,要放到的 payload
.setIssuedAt(new Date()) // jwt的颁发日期
.setExpiration(new Date(System.currentTimeMillis() + 2000)) // jwt的有效期
.signWith(k, SignatureAlgorithm.HS512) // 生成jwt的算法
.compact(); // 返回一个 jwt的字符串
//
System.out.println(jwt);
}
第三步,jwt的验证
@Test
public void verifyJWT() {
// 一个jwt的字符串
String jwt = "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImlhdCI6MTY3Njk2Njk1NywiZXhwIjoxNjc2OTY2OTU5fQ.8Ub25SFQbP7K54VcoSD2Y_S9DXg6tXMisKgLuAk0Udcrl7hAZSvhFhIBfWt2I5DfssmH3lhV1wnukiqN2eKfMw";
// 这是密钥,因为验证 JWT的时候,需要用当时生成的 jwt时候的密钥来进行验证
String key = "Qltw8l63IzprU54xLHTK3bzkY40iCLbA26Jo4IZmARXQALz0dBrclPDwkRyOUiDoasOioGtBAPfN0NWmrLC1Og==";
// 将密钥封装成一个 Key类型的对象
Key k = new SecretKeySpec(Base64.getDecoder().decode(key), "HmacSHA512");
/**
* 如下的代码就是用来验证jwt的
*/
try {
Claims claims = Jwts.parserBuilder().setSigningKey(k).build().parseClaimsJws(jwt).getBody();
System.out.println("验证成功");
}catch (Exception ex) {
/**
* 1. 如果抛出的是这个异常:SignatureException,jwt被篡改
* 2. 如果抛出的是这个异常:MalformedJwtException,jwt被篡改了,格式不对。
* 3. 如果抛出的是这个异常:ExpiredJwtException,jwt过期,需要重新登录。
*/
System.out.println(ex.getClass());
}
}
第四步,jwt加密字符串的生成
@Test
public void generateKey() {
Key key = Keys.secretKeyFor(SignatureAlgorithm.HS512);
String k = new String(Base64.getEncoder().encode(key.getEncoded()));
System.out.println(k);
}
补充
- 属性注入,在项目中非架构配置需要单独提取一个 properties 配置文件,例如如下的配置
jwt.jwt-token=Qltw8l63IzprU54xLHTK3bzkY40iCLbA26Jo4IZmARXQALz0dBrclPDwkRyOUiDoasOioGtBAPfN0NWmrLC1Og==
jwt.expire-time=2000000
/**
* 第一种方式
* 1.@PropertySource 作用是读取外部 properties 资源的
* 2.关于配置,application.yml 是框架层面的配置,系统中业务数据不要往 application.yml 中配置
*/
@PropertySource("classpath:app.properties")
@Component // 只有在容器中才能使用 @Value读取 配置文件中的内容
@Data
public class AppProperties {
@Value("${jwt.jwt-token}")
private String jwtToken;
@Value("${jwt.expire-time}")
private String expireTime;
}
/**
* 1.获取配置文件的信息,这个方式用的比较多;
* 2. @PropertySource 用来指定外部的 properties 配置文件;
* 3. @ConfigurationProperties 用来指定将哪个前缀下的属性注入到该 Bean的属性中
*/
@PropertySource("classpath:app.properties")
@Component
@ConfigurationProperties(prefix = "jwt") // prefix: 前缀 sufix: 后缀
@Data
public class AppProperties {
private String jwtToken; // 配置文件中有两种写法:jwt-token 或者 jwtToken
private Long expireTime; // 配置文件中有两种写法:expire-time 或者 expireTime
}
prefix = “jwt”) // prefix: 前缀 sufix: 后缀
@Data
public class AppProperties {private String jwtToken; // 配置文件中有两种写法:jwt-token 或者 jwtToken
private Long expireTime; // 配置文件中有两种写法:expire-time 或者 expireTime
}