SpringBoot框架结构速查
SpringBoot框架结构速查
用户登录模块
1.系统资源分类
- 公共资源(游客资源)
- 不需要登录就可以访问,系统不需要知道你是谁
- 所有人看到的数据是一样的
- 认证资源(登录后资源)
- 需要登录后才可以访问,系统需要知道你是谁
- 不同用户看到的数据是不同的
- 拦截器
拦截器是什么
拦截器是控制器的请求门卫,所有的HTTP请求先经过拦截器在进入控制器
在springboot中不需要额外添加依赖,拦截器和控制器都包含在Spring web的基础框架中了
如何创建拦截器
自定义拦截器实现拦截器父接口的HandlerInterceptor,父接口提供了三个默认方法被子类
- 之前说子类必须实现父类的所有抽象方法
- 在jdk1.8后子类提供了一种新的方法,运行子类可选重写,让接口更加灵活、
- 抽象方法和默认方法的区别
- 抽象方法没有方法体,要求子类必须重写
- 默认方法有方法体,子类可以不重写,默认
- 重写preHandle方法(在进入控制器之前拦截)
- http请求的潜质拦截方法
- 在潜质拦截方法中可以获取请求报文
- 这个方法一般必须重写
- 该方法的返回值是boolean类型,true表示放行,false表示拦截请求
- 重写postHandle方法(在进入控制器之后拦截)
- http请求的后拦截方法
- 在后拦截方法中可以获取请求报文
- 这个方法一般必须重写
- 该方法的返回值是void类型,没有返回值
- 重写afterCompletion方法(在进入控制器之后拦截)
- 视图指的是前端页面由后端渲染
- 这个拦截仅用于前后端不分离开发
- 前后端分离开发中,用不到这个拦截方法
蓝图放在下面1
2
3
4
5
6public class MyInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}
}
如何注册拦截器
- 拦截器创建后并不会立刻生效,需要进行注册,然后配置拦截路径的规则
- 创建webConfig配置文件类,实现WebMvcConfigurer接口
- 重写addInterceptors方法,添加拦截器
- 该方法提供了一个registry对象,代表拦截器注册表对象
- 在这个方法中需要拦截指定的路径,需要排除拦截的路径
- 该路径支持正则表达式写法
蓝图见下图:1
2
3
4
5
6
7
public class WebConfig implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**").excludePathPatterns("/login");
}
}多个拦截器如何指定执行顺序
- 如果定义了多个拦截器,并同时进行注册,这时在拦截器注册表中会形成一个拦截器链
- 可使用@Order注解指定拦截器的执行顺序,数字越小,执行顺序越靠前
1
2
3
4
5
6
7//看这里,数字越小,执行顺序越靠前
public class MyInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}
}
拦截器中如何放回HTTP响应
- 拦截器放回false表示拦截请求,此时客户端无法获取任何响应内容(报文中没有响应内容)
- 因此需要在返回false之前先设置响应报文中携带的内容
方式一:使用原始的response对象设置响应报文 - 由于拦截器需要返回ture或者false表示拦截或者是放行,因此框架没有提供直接返回响应的方法
- 需要我们手动将放回的对象转为JSON字符串,然后设置到响应报文中,框架对于控制器和全局异常处理器赋予这种能力
- 这个过程需要我们自己实现,比较麻烦
案例蓝图如下:方式二:抛出一个异常个全局异常处理器捕捉,走异常处理器返回响应1
2
3
4
5
6
7
8
9
10
11public class MyInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//设置响应报文
response.setContentType("text/html;charset=utf-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
//设置响应体
response.getWriter().write("{\"code\":401,\"msg\":\"未登录\"}");
return false;
}
} - 在拦截器中抛出一个异常,全局异常处理器会捕获到这个异常,然后走异常处理器返回响应
- 后面不需要加return语句
- 因为方法抛出异常之后,方法就会结束执行,不会继续执行后续的代码
并不是抛出一个异常后,就一定能全局异常处理器上,可以使用@ExceptionHandler注解指定异常处理器
1 | public class MyInterceptor implements HandlerInterceptor { |
1 |
|
全局异常处理器
- 全局异常处理器是一个特殊的控制器,专门用于处理系统中发生的异常
- 可以使用@ExceptionHandler注解指定异常处理器
- 异常处理器的方法参数可以是异常对象,也可以是异常的类
- 异常处理器的方法返回值可以是任意类型,但是建议返回Result对象,
- 这样可以统一封装响应结果,方便前端处理
1
2
3
4
5
6
7
public class GlobalExceptionHandler {
public Result handleException(Exception e) {
return new Result<>(false, e.getMessage());
}
}JWT令牌
- JWT令牌是一种基于JSON的TOKEN,用于在客户端和服务器之间传递安全的认证信息。
- 它通常由三部分组成:
- 头部(Header):包含令牌的类型(如JWT)和签名算法(如HS256)
- typ:JWT令牌的类型,固定值为JWT
- alg:签名算法,如HS256
- 载荷(Payload):包含实际的认证信息,如用户ID、用户名、角色等
- 包含签发者,签发时间,过期时间,主题,受众,自定义声明等
- 签名(Signature):用于验证令牌的完整性和真实性,
- 令牌的签名是根据载荷和头部信息,使用指定的签名算法生成的,
- 服务器在收到令牌后,会验证签名是否正确,来判断令牌的合法性
- 头部(Header):包含令牌的类型(如JWT)和签名算法(如HS256)
- JWT令牌的使用流程:
- 用户登录成功后,服务器生成一个JWT令牌,并将其返回给客户端
- 客户端在后续的请求中,将JWT令牌放在请求头中,发送给服务器
- 服务器在收到请求后,验证JWT令牌的合法性,如果合法,则允许访问受保护的资源,否则拒绝访问
在使用JWT令牌的时候需要引入以下依赖:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<!-- pom.xml -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
使用JWT令牌的蓝图:(可以复用)1
2
3
4//生成JWT令牌
String token = JwtUtils.generateToken(user);
//验证JWT令牌
boolean isValid = JwtUtils.validateToken(token);
完整文件配置示意:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62package com.xhayane.springbootlogin.util;
import com.xhayane.springbootlogin.entity.UserDetails;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JwtUtil {
private String secretKey;
private Long expiration;
/**
* 生成 Token
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userDetails.getUserId());
claims.put("username", userDetails.getUsername());
claims.put("role", userDetails.getRole());
return Jwts.builder()
.setClaims(claims) // 设置载荷
.setSubject(userDetails.getUsername()) // 设置主题
.setIssuedAt(new Date()) // 设置签发时间
.setExpiration(new Date(System.currentTimeMillis() + expiration)) // 设置过期时间
.signWith(SignatureAlgorithm.HS256, secretKey) // 设置签名算法和密钥
.compact(); // 生成 Token
}
/**
* 从 Token 中获取用户名
*/
public String getUsernameFromToken(String token) {
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
/**
* 验证 Token 是否过期
*/
public boolean isTokenExpired(String token) {
Date expiration = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody()
.getExpiration();
return expiration.before(new Date());
}
}
如果要使用上述代码,可以客制化更改部分内容
- 在这个蓝图中,有一个UserDetails类型,需要定义出来,也可根据实际运用情况使用
- 工具类的成员secretkey 用于加密token令牌的密钥
- 只要密钥不泄露,就算是令牌被劫持,对方也无法破解
- 工具类的成员expiration 用于设置token令牌的过期时间,单位是毫秒
- 可以在application.properties文件中配置这两个值
结合配置文件去配置令牌:1
2
3
4jwt:
secret: 8L4Aq9aG2pR5sY7kF3jH8cB1vX0zN7mR6tU8iP2oE5wQ7yS4dF6gH1jK4lL2
#512位密钥,用于加密token令牌的密钥
expiration: 600000
SpringSecurity安全框架
这孩子涉及认证,授权,和安全相关的功能
注意先认证,后授权,否则未认证的用户,连授权的资格都没有
相关能力
当你在项目中集成了这个框架后什么都不做的情况下,框架默认赋予项目以下能力
- 项目中的所有资源和接口都会受到该框架的保护,必须登录才会访问
- 他会自动设置一道拦截器,用于对所有请求进行拦截和校验
- 如果请求中没有JWT令牌,或者令牌无效,会拒绝访问
- 默认提供名为User的默认用户名
- 启动后提供的默认密码会在项目重启后自动打印在控制台,是一串随机密码
依赖配置
- 引入SpringSecurity的依赖
1
2
3
4
5
6
7
8
9<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency></dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>配置SpringSecurity框架的默认账号和密码
你不配置就是默认的User账号,密码是随机密码,在启动的时候可以在控制台查看,下面这个是配置多用户角色配置文件实例对应的config文件写法为:1
2
3
4
5
6
7
8security:
user:
- name: User
password: 123456
roles: USER #权限名
- name: Admin
password: 123456
roles: ADMIN #权限名如果要自定义用户角色,需要在config文件中配置,否则默认是USER角色1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 关键:开启 @PreAuthorize 注解权限控制
public class SecurityConfig {
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.formLogin(form -> form.permitAll()) // 登录页放行
.logout(logout -> logout.permitAll());
return http.build();
}
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
在上述config文件的基础上,添加角色的配置:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public UserDetailsService userDetailsService() {
UserDetails user1 = User
.withUsername("admin")
.password("{noop}123456")
.roles("ADMIN","USER")
.build();
UserDetails user2 = User
.withUsername("zhanfei")
.password("{noop}123456")
.roles("ADMIN")
.build();
UserDetails user3 = User
.withUsername("guanyu")
.password("{noop}123456")
.roles("USER")
.build();
//创建用户管理器,注册这些用户
UserDetailsService userDetailsService = new InMemoryUserDetailsManager(user1, user2, user3);
//返回用户管理器来给框架
return userDetailsService;
}
在控制器中使用@PreAuthorize注解进行权限控制
- @PreAuthorize 注解用于在方法级别进行权限控制
- 它可以指定在访问方法前需要满足的权限条件
- 如果权限条件不满足,会拒绝访问
/test/t1必须具备USER角色才能访问
/test/t2必须具备ADMIN角色才能访问
对应的控制器写法为:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class TestController {
// @GetMapping("/")
// @GetMapping("/test")
public Object test1(){
return "访问成功test1";
}
public Object test2(){
return "test2";
}
}
