SpringBoot框架结构速查

用户登录模块

1.系统资源分类

- 公共资源(游客资源)
    - 不需要登录就可以访问,系统不需要知道你是谁
    - 所有人看到的数据是一样的
- 认证资源(登录后资源)
    - 需要登录后才可以访问,系统需要知道你是谁
    - 不同用户看到的数据是不同的
  1. 拦截器
    拦截器是什么
    拦截器是控制器的请求门卫,所有的HTTP请求先经过拦截器在进入控制器
    在springboot中不需要额外添加依赖,拦截器和控制器都包含在Spring web的基础框架中了

如何创建拦截器

自定义拦截器实现拦截器父接口的HandlerInterceptor,父接口提供了三个默认方法被子类

  • 之前说子类必须实现父类的所有抽象方法
  • 在jdk1.8后子类提供了一种新的方法,运行子类可选重写,让接口更加灵活、
  • 抽象方法和默认方法的区别
    • 抽象方法没有方法体,要求子类必须重写
    • 默认方法有方法体,子类可以不重写,默认
  • 重写preHandle方法(在进入控制器之前拦截)
    • http请求的潜质拦截方法
    • 在潜质拦截方法中可以获取请求报文
    • 这个方法一般必须重写
    • 该方法的返回值是boolean类型,true表示放行,false表示拦截请求
  • 重写postHandle方法(在进入控制器之后拦截)
    • http请求的后拦截方法
    • 在后拦截方法中可以获取请求报文
    • 这个方法一般必须重写
    • 该方法的返回值是void类型,没有返回值
  • 重写afterCompletion方法(在进入控制器之后拦截)
    • 视图指的是前端页面由后端渲染
    • 这个拦截仅用于前后端不分离开发
    • 前后端分离开发中,用不到这个拦截方法
      蓝图放在下面
      1
      2
      3
      4
      5
      6
      public class MyInterceptor implements HandlerInterceptor {
      @Override
      public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
      return true;
      }
      }

如何注册拦截器

  • 拦截器创建后并不会立刻生效,需要进行注册,然后配置拦截路径的规则
  • 创建webConfig配置文件类,实现WebMvcConfigurer接口
  • 重写addInterceptors方法,添加拦截器
  • 该方法提供了一个registry对象,代表拦截器注册表对象
  • 在这个方法中需要拦截指定的路径,需要排除拦截的路径
  • 该路径支持正则表达式写法
    蓝图见下图:
    1
    2
    3
    4
    5
    6
    7
    @Configuration
    public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**").excludePathPatterns("/login");
    }
    }

    多个拦截器如何指定执行顺序

  • 如果定义了多个拦截器,并同时进行注册,这时在拦截器注册表中会形成一个拦截器链
  • 可使用@Order注解指定拦截器的执行顺序,数字越小,执行顺序越靠前
    1
    2
    3
    4
    5
    6
    7
    @Order(1)//看这里,数字越小,执行顺序越靠前
    public class MyInterceptor implements HandlerInterceptor {
    @Override
    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
    11
    public class MyInterceptor implements HandlerInterceptor {  
    @Override
    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
2
3
4
5
6
7
public class MyInterceptor implements HandlerInterceptor {  
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
throw new Exception("未登录");

}
}
1
2
3
4
@ExceptionHandler(Exception.class)
public Result handleException(Exception e) {
return new Result<>(false, e.getMessage());
}

全局异常处理器

  • 全局异常处理器是一个特殊的控制器,专门用于处理系统中发生的异常
  • 可以使用@ExceptionHandler注解指定异常处理器
  • 异常处理器的方法参数可以是异常对象,也可以是异常的类
  • 异常处理器的方法返回值可以是任意类型,但是建议返回Result对象,
  • 这样可以统一封装响应结果,方便前端处理
    1
    2
    3
    4
    5
    6
    7
    @ControllerAdvice
    public class GlobalExceptionHandler {
    @ExceptionHandler(Exception.class)
    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):用于验证令牌的完整性和真实性,
    • 令牌的签名是根据载荷和头部信息,使用指定的签名算法生成的,
    • 服务器在收到令牌后,会验证签名是否正确,来判断令牌的合法性
  • JWT令牌的使用流程:
  1. 用户登录成功后,服务器生成一个JWT令牌,并将其返回给客户端
  2. 客户端在后续的请求中,将JWT令牌放在请求头中,发送给服务器
  3. 服务器在收到请求后,验证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
62
package 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;

@Component
public class JwtUtil {

@Value("${jwt.secret}")
private String secretKey;

@Value("${jwt.expiration}")
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
4
jwt:
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账号,密码是随机密码,在启动的时候可以在控制台查看,下面这个是配置多用户角色配置文件实例
    1
    2
    3
    4
    5
    6
    7
    8
    security:
    user:
    - name: User
    password: 123456
    roles: USER #权限名
    - name: Admin
    password: 123456
    roles: ADMIN #权限名
    对应的config文件写法为:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    @Configuration
    @EnableWebSecurity
    @EnableMethodSecurity // 关键:开启 @PreAuthorize 注解权限控制
    public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
    .authorizeHttpRequests(auth -> auth
    .anyRequest().authenticated()
    )
    .formLogin(form -> form.permitAll()) // 登录页放行
    .logout(logout -> logout.permitAll());

    return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
    }
    }
    如果要自定义用户角色,需要在config文件中配置,否则默认是USER角色
    在上述config文件的基础上,添加角色的配置:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    @Bean
    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
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/t1")
// @GetMapping("/")
// @GetMapping("/test")
@PreAuthorize("hasRole('USER')")
public Object test1(){
return "访问成功test1";
}
@GetMapping("/t2")
@PreAuthorize("hasRole('ADMIN')")
public Object test2(){
return "test2";
}
}