大号在练葵花宝典 发表于 2023-3-5 14:11:52

Spring Security + JWT + Swagger2 登录验证一套流程小结

Spring Security + JWT + Swagger2 登录验证一套流程

主要是三个框架的集成配置,以及各个独立的配置(主要是 JWT + Security 的登录验证)。
流程:

[*]构建 Spring Boot 基本项目,准备数据库表 User —— 用于存放登录实体类信息。
[*]配置 Security 和 Swagger2 环境,确保没有什么问题。
[*]构建 RespBean——公共返回实体类,JwtTokenUtil——JWT token 工具类,User——登录实体类
[*]让 User 实现 UserDetails 接口,重写部分方法。
[*]配置 Security 实现重写 UserDetailsService 方法,以及 PasswordEncoder——密码凭证器 并加上 @Bean 注解。这两个主要用于设置 Security 的认证。
[*]构建 jwtAuthenticationTokenFilter 类——自定义 JWT Token 拦截器,并在 SecurityConfig 的授权方法中添加此拦截器。
[*]在 Swagger2Config 配置类中,配置有关 Security 的 Token 认证。
[*]启动项目查看代码是否准确。
1. 构建 Spring Boot 基本项目,准备数据库——User

项目子模块:authority-security,父模块已引入 Spring boot 依赖 2.3.0
1.1 导入依赖

<dependencies>
   
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

   
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>

   
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <scope>runtime</scope>
    </dependency>

   
    <dependency>
      <groupId>com.baomidou</groupId>
      <artifactId>mybatis-plus-boot-starter</artifactId>
      <version>3.3.1.tmp</version>
    </dependency>

   
    <dependency>
      <groupId>io.springfox</groupId>
      <artifactId>springfox-swagger2</artifactId>
      <version>2.7.0</version>
    </dependency>

   
    <dependency>
      <groupId>com.github.xiaoymin</groupId>
      <artifactId>swagger-bootstrap-ui</artifactId>
      <version>1.9.6</version>
    </dependency>

   
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

   
    <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt</artifactId>
      <version>0.9.1</version>
    </dependency>

   
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-pool2</artifactId>
    </dependency>
</dependencies>构建数据库表:user
create table user(
        id int primary key auto_increment,
        username varchar not null,
        password varchar not null,
        info varchar(200),
        enabled tinyint(1) default 1
)

insert into user values(default,"admin","$2a$10$Himwt.wu3MPOLnNQ9YUH8O2quxgi7bMuomiNeFsVKRay87.qG5dgy","管理员 info ...",default)username:admin;password:123
配置 application.yml 文件参数:
server:
port: 8082

spring:
datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/dbtest16?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: admin
    password: admin
    hikari:
      # 连接池名字
      pool-name: DateHikari
      # 最小空闲连接数
      minimum-idle: 5
      # 空闲连接存活最大事件,默认10分钟(600000)
      idle-timeout: 180000
      # 最大连接数:默认 10
      maximum-pool-size: 10
      # 从连接池返回的连接自动提交
      auto-commit: true
      # 连接最大存活时间,0 表示永久存活,默认 1800000(30 min)
      max-lifetime: 1800000
      # 连接超时事件 30 s
      connection-timeout: 30000
      # 测试连接是否可用的查询语句
      connection-test-query: SELECT 1

# MP 配置
mybatis-plus:
# 配置 Mapper 映射文件
mapper-locations: classpath*:/mapper/*Mapper.xml
# 实体类的别名包
type-aliases-package: com.cnda.pojo
configuration:
    # 自动驼峰命名
    map-underscore-to-camel-case: false

# MyBatis 的 SQL 打印是方法接口所在的包
logging:
level:
    com.cnda.mapper: debug

# JWT 配置
jwt:
# JWT 存储的请求头
tokenHeader: Authorization
# JWT 加密使用的密钥
secret: test-cnda-secret
# JWT 的有效时间 (60*60*24)
expiration: 604800
# JWT 负载中拿到开头 规定
tokenHead: BearerUser 实体类代码:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Integer id;
    private String username;
    private String password;
    private String info;
    private Boolean enabled;
}2. 配置 Security 和 Swagger2 的配置

先配置好这两个确保没有什么问题,因为重点是 JWT,这两个配置比较简单,当搭配了 JWT 之后,Swagger2 也需要与两者集成一些配置,这个后面再说,现在只配置基本设置。
2.1 配置 SecurityConfig

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
      super.configure(auth);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
      web.ignoring().antMatchers(
                "/hello",
                    // 下面是对静态资源以及 swagger2 UI 的放行。
                    "/css/**",
                "/js/**",
                "/img/**",
                "/index.html",
                "favicon.ico",
                "/doc.html",
                "/webjars/**",
                "/swagger-resources/**",
                "/v2/api-docs/**",
                "/ws/**"
      );
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
      super.configure(http);
    }
}上面使用 WebSecurity 放行了 /hello 请求,在 LoginController 中。
@RestController
public class LoginController {

    @RequestMapping("/hello")
    public String hello(){
      return "Hello Word!";
    }
}这意味除了 localhost:8082/hello 会被放行,其他请求都会被 Security 拦截重定向到 /login(这个请求 Security 内部已经实现了包括相关页面)。
2.2 配置 Swagger2Config

@Configuration
@EnableSwagger2
public class Swagger2Config {
    @Bean
    public Docket docket(){
      return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo()) // 配置 apiInfo
                .select() // 选择那些路径和api会生成document
                .apis(RequestHandlerSelectors.basePackage("com.cnda.controller")) // // 对哪些 api进行监控,RequestHandlerSelectors.basePackage 基于包扫描
                .paths(PathSelectors.any()) // 对所有路径进行监控
                .build();

    }

    private ApiInfo apiInfo(){
      return new ApiInfoBuilder()
                .title("在线接口文档")
                .description("在线接口文档")
                .contact(new Contact("cnda","http://localhost:8082/doc.html","xxx@xxx.com"))
                .build();
    }
}运行效果:
https://picimg-blog.oss-cn-nanjing.aliyuncs.com/blog-img/image-20230305095856050.png
修改一下 Rustful 风格,并加了一个 /hello1 请求,不放行,打印内容相同。
https://picimg-blog.oss-cn-nanjing.aliyuncs.com/blog-img/image-20230305100904394.png
https://picimg-blog.oss-cn-nanjing.aliyuncs.com/blog-img/image-20230305100944517.png
可以看到 Security 和 Swagger2 基本配置完成。
3. 构建 JWT 工具类、公共响应对象

JWT 工具类主要用于生成 JWT,判断 JWT 是否有效,刷新 JWT 等方法。
公共响应对象——RespBean,返回的都已 JSON 格式返回。
3.1 JwtUtil

@Component
public class JwtUtil {
    // 准备两个存放在荷载的内容
    private static final String CLAIM_KEY_SUB = "sub";
    private static final String CLAIM_KEY_CREATE = "ibt";


    // 提取 application.yml 中 JWT 的参数:
    // 1. expiration Long
    @Value("${jwt.expiration}")
    private Long expiration;

    // 2. secret String
    @Value("${jwt.secret}")
    private String secret; // 密钥

    // 根据用户名构建 token
    public String foundJWT(UserDetails userDetails) {
      String username = userDetails.getUsername();
      Map<String, Object> claims = new HashMap<>();
      claims.put(CLAIM_KEY_SUB, username);
      claims.put(CLAIM_KEY_CREATE, new Date());
      return foundJWT(claims);
    }

    // 根据荷载 map 构建 token
    private String foundJWT(Map<String, Object> claims) {
      return Jwts.builder()
                .setClaims(claims)
                .setExpiration(getExpiration()) // 过期时间
                .signWith(SignatureAlgorithm.HS512, secret) // 设置签名算法和密钥
                .compact();
    }

    // 判断 token 是否有效
    public boolean validateToken(String token,UserDetails userDetails){
      // 从 token 中获取 username 与 userDetails 中的username 对比
      String username = getUsernameInToken(token);
      // 判断 username 是否一致以及 token 是否过期
      return username.equals(userDetails.getUsername()) && !isExpired(token);
    }


    // 判断 token 是否过期
    // true 过期 false 没过期
    private boolean isExpired(String token) {
      Date expiration = getClaimsInToken(token).getExpiration();
      return expiration.before(new Date());
    }
    // 从 token 中提取荷载信息
    public Claims getClaimsInToken(String token){
      Claims claims = null;

      try {
            claims = Jwts.parser()
                  .setSigningKey(secret)
                  .parseClaimsJws(token)
                  .getBody();
      }catch (Exception e){
            e.printStackTrace();
      }
      return claims;
    }

    // 从 token 中提取用户名信息
    public String getUsernameInToken(String token){
      String username;
      try {
            username = getClaimsInToken(token).getSubject();
      }catch (Exception e){
            username = null;
      }
      return username;
    }
    // token 是否能刷新
    public boolean tokenCanRef(String token){
      return !isExpired(token); // 有效地 token 才能被刷新
    }
    // 刷新 token
    public String refToken(String token){
      Claims claimsInToken = getClaimsInToken(token);
      claimsInToken.put(CLAIM_KEY_CREATE,new Date());
      return foundJWT(claimsInToken);
    }

    // 设置过期时间
    private Date getExpiration() {
      return new Date(System.currentTimeMillis() + expiration * 1000);
    }
}3.2 RespBean 公共返回对象

@Data
@AllArgsConstructor
@NoArgsConstructor
public class RespBean {
    private long code;
    private String message;
    private Object obj;

    /**
   * 返回响应结果
   */
    private static RespBean result(long code, String message, Object obj) {
      return new RespBean(code, message, obj);
    }
    /*
       返回成功响应
      */
    public static RespBean success(String message) {
      return result(200, message, null);
    }

    /*
    返回成功响应以及数据体
   */
    public static RespBean success(String message, Object obj) {
      return result(200, message, obj);
    }

    /*
    返回错误响应
   */
    public static RespBean error(String message) {
      return result(500, message, null);
    }
}4. 让 User 实体类实现 UserDetails 的方法成为 Security 验证的用户核心主体

由于 Security 框架的性质,自定义授权和认证时,一般情况下会自定义 UserDetails。
@Data@AllArgsConstructor@NoArgsConstructorpublic class User implements UserDetails {    private Integer id;    private String username;    private String password;    private String info;    private Boolean enabled;    @Override    public Collection
页: [1]
查看完整版本: Spring Security + JWT + Swagger2 登录验证一套流程小结