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

打印 上一主题 下一主题

主题 676|帖子 676|积分 2028

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 导入依赖
  1. <dependencies>
  2.    
  3.     <dependency>
  4.         <groupId>org.springframework.boot</groupId>
  5.         <artifactId>spring-boot-starter-web</artifactId>
  6.     </dependency>
  7.    
  8.     <dependency>
  9.         <groupId>org.projectlombok</groupId>
  10.         <artifactId>lombok</artifactId>
  11.         <optional>true</optional>
  12.     </dependency>
  13.    
  14.     <dependency>
  15.         <groupId>mysql</groupId>
  16.         <artifactId>mysql-connector-java</artifactId>
  17.         <scope>runtime</scope>
  18.     </dependency>
  19.    
  20.     <dependency>
  21.         <groupId>com.baomidou</groupId>
  22.         <artifactId>mybatis-plus-boot-starter</artifactId>
  23.         <version>3.3.1.tmp</version>
  24.     </dependency>
  25.    
  26.     <dependency>
  27.         <groupId>io.springfox</groupId>
  28.         <artifactId>springfox-swagger2</artifactId>
  29.         <version>2.7.0</version>
  30.     </dependency>
  31.    
  32.     <dependency>
  33.         <groupId>com.github.xiaoymin</groupId>
  34.         <artifactId>swagger-bootstrap-ui</artifactId>
  35.         <version>1.9.6</version>
  36.     </dependency>
  37.    
  38.     <dependency>
  39.         <groupId>org.springframework.boot</groupId>
  40.         <artifactId>spring-boot-starter-security</artifactId>
  41.     </dependency>
  42.    
  43.     <dependency>
  44.         <groupId>io.jsonwebtoken</groupId>
  45.         <artifactId>jjwt</artifactId>
  46.         <version>0.9.1</version>
  47.     </dependency>
  48.    
  49.     <dependency>
  50.         <groupId>org.apache.commons</groupId>
  51.         <artifactId>commons-pool2</artifactId>
  52.     </dependency>
  53. </dependencies>
复制代码
构建数据库表:user
  1. create table user(
  2.         id int primary key auto_increment,
  3.         username varchar not null,
  4.         password varchar not null,
  5.         info varchar(200),
  6.         enabled tinyint(1) default 1
  7. )
  8. insert into user values(default,"admin","$2a$10$Himwt.wu3MPOLnNQ9YUH8O2quxgi7bMuomiNeFsVKRay87.qG5dgy","管理员 info ...",default)
复制代码
username:admin;password:123
配置 application.yml 文件参数:
  1. server:
  2.   port: 8082
  3. spring:
  4.   datasource:
  5.     driver-class-name: com.mysql.cj.jdbc.Driver
  6.     url: jdbc:mysql://localhost:3306/dbtest16?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
  7.     username: admin
  8.     password: admin
  9.     hikari:
  10.       # 连接池名字
  11.       pool-name: DateHikari
  12.       # 最小空闲连接数
  13.       minimum-idle: 5
  14.       # 空闲连接存活最大事件,默认10分钟(600000)
  15.       idle-timeout: 180000
  16.       # 最大连接数:默认 10
  17.       maximum-pool-size: 10
  18.       # 从连接池返回的连接自动提交
  19.       auto-commit: true
  20.       # 连接最大存活时间,0 表示永久存活,默认 1800000(30 min)
  21.       max-lifetime: 1800000
  22.       # 连接超时事件 30 s
  23.       connection-timeout: 30000
  24.       # 测试连接是否可用的查询语句
  25.       connection-test-query: SELECT 1
  26. # MP 配置
  27. mybatis-plus:
  28.   # 配置 Mapper 映射文件
  29.   mapper-locations: classpath*:/mapper/*Mapper.xml
  30.   # 实体类的别名包
  31.   type-aliases-package: com.cnda.pojo
  32.   configuration:
  33.     # 自动驼峰命名
  34.     map-underscore-to-camel-case: false
  35. # MyBatis 的 SQL 打印是方法接口所在的包
  36. logging:
  37.   level:
  38.     com.cnda.mapper: debug
  39. # JWT 配置
  40. jwt:
  41.   # JWT 存储的请求头
  42.   tokenHeader: Authorization
  43.   # JWT 加密使用的密钥
  44.   secret: test-cnda-secret
  45.   # JWT 的有效时间 (60*60*24)
  46.   expiration: 604800
  47.   # JWT 负载中拿到开头 规定
  48.   tokenHead: Bearer
复制代码
User 实体类代码:
  1. @Data
  2. @AllArgsConstructor
  3. @NoArgsConstructor
  4. public class User {
  5.     private Integer id;
  6.     private String username;
  7.     private String password;
  8.     private String info;
  9.     private Boolean enabled;
  10. }
复制代码
2. 配置 Security 和 Swagger2 的配置

先配置好这两个确保没有什么问题,因为重点是 JWT,这两个配置比较简单,当搭配了 JWT 之后,Swagger2 也需要与两者集成一些配置,这个后面再说,现在只配置基本设置。
2.1 配置 SecurityConfig
  1. @EnableWebSecurity
  2. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  3.     @Override
  4.     protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  5.         super.configure(auth);
  6.     }
  7.     @Override
  8.     public void configure(WebSecurity web) throws Exception {
  9.         web.ignoring().antMatchers(
  10.                 "/hello",
  11.                     // 下面是对静态资源以及 swagger2 UI 的放行。
  12.                     "/css/**",
  13.                 "/js/**",
  14.                 "/img/**",
  15.                 "/index.html",
  16.                 "favicon.ico",
  17.                 "/doc.html",
  18.                 "/webjars/**",
  19.                 "/swagger-resources/**",
  20.                 "/v2/api-docs/**",
  21.                 "/ws/**"
  22.         );
  23.     }
  24.     @Override
  25.     protected void configure(HttpSecurity http) throws Exception {
  26.         super.configure(http);
  27.     }
  28. }
复制代码
上面使用 WebSecurity 放行了 /hello 请求,在 LoginController 中。
  1. @RestController
  2. public class LoginController {
  3.     @RequestMapping("/hello")
  4.     public String hello(){
  5.         return "Hello Word!";
  6.     }
  7. }
复制代码
这意味除了 localhost:8082/hello 会被放行,其他请求都会被 Security 拦截重定向到 /login(这个请求 Security 内部已经实现了包括相关页面)。
2.2 配置 Swagger2Config
  1. @Configuration
  2. @EnableSwagger2
  3. public class Swagger2Config {
  4.     @Bean
  5.     public Docket docket(){
  6.         return new Docket(DocumentationType.SWAGGER_2)
  7.                 .apiInfo(apiInfo()) // 配置 apiInfo
  8.                 .select() // 选择那些路径和api会生成document
  9.                 .apis(RequestHandlerSelectors.basePackage("com.cnda.controller")) // // 对哪些 api进行监控,RequestHandlerSelectors.basePackage 基于包扫描
  10.                 .paths(PathSelectors.any()) // 对所有路径进行监控
  11.                 .build();
  12.     }
  13.     private ApiInfo apiInfo(){
  14.         return new ApiInfoBuilder()
  15.                 .title("在线接口文档")
  16.                 .description("在线接口文档")
  17.                 .contact(new Contact("cnda","http://localhost:8082/doc.html","xxx@xxx.com"))
  18.                 .build();
  19.     }
  20. }
复制代码
运行效果:

修改一下 Rustful 风格,并加了一个 /hello1 请求,不放行,打印内容相同。


可以看到 Security 和 Swagger2 基本配置完成。
3. 构建 JWT 工具类、公共响应对象

JWT 工具类主要用于生成 JWT,判断 JWT 是否有效,刷新 JWT 等方法。
公共响应对象——RespBean,返回的都已 JSON 格式返回。
3.1 JwtUtil
  1. @Component
  2. public class JwtUtil {
  3.     // 准备两个存放在荷载的内容
  4.     private static final String CLAIM_KEY_SUB = "sub";
  5.     private static final String CLAIM_KEY_CREATE = "ibt";
  6.     // 提取 application.yml 中 JWT 的参数:
  7.     // 1. expiration Long
  8.     @Value("${jwt.expiration}")
  9.     private Long expiration;
  10.     // 2. secret String
  11.     @Value("${jwt.secret}")
  12.     private String secret; // 密钥
  13.     // 根据用户名构建 token
  14.     public String foundJWT(UserDetails userDetails) {
  15.         String username = userDetails.getUsername();
  16.         Map<String, Object> claims = new HashMap<>();
  17.         claims.put(CLAIM_KEY_SUB, username);
  18.         claims.put(CLAIM_KEY_CREATE, new Date());
  19.         return foundJWT(claims);
  20.     }
  21.     // 根据荷载 map 构建 token
  22.     private String foundJWT(Map<String, Object> claims) {
  23.         return Jwts.builder()
  24.                 .setClaims(claims)
  25.                 .setExpiration(getExpiration()) // 过期时间
  26.                 .signWith(SignatureAlgorithm.HS512, secret) // 设置签名算法和密钥
  27.                 .compact();
  28.     }
  29.     // 判断 token 是否有效
  30.     public boolean validateToken(String token,UserDetails userDetails){
  31.         // 从 token 中获取 username 与 userDetails 中的username 对比
  32.         String username = getUsernameInToken(token);
  33.         // 判断 username 是否一致以及 token 是否过期
  34.         return username.equals(userDetails.getUsername()) && !isExpired(token);
  35.     }
  36.     // 判断 token 是否过期
  37.     // true 过期 false 没过期
  38.     private boolean isExpired(String token) {
  39.         Date expiration = getClaimsInToken(token).getExpiration();
  40.         return expiration.before(new Date());
  41.     }
  42.     // 从 token 中提取荷载信息
  43.     public Claims getClaimsInToken(String token){
  44.         Claims claims = null;
  45.         try {
  46.             claims = Jwts.parser()
  47.                     .setSigningKey(secret)
  48.                     .parseClaimsJws(token)
  49.                     .getBody();
  50.         }catch (Exception e){
  51.             e.printStackTrace();
  52.         }
  53.         return claims;
  54.     }
  55.     // 从 token 中提取用户名信息
  56.     public String getUsernameInToken(String token){
  57.         String username;
  58.         try {
  59.             username = getClaimsInToken(token).getSubject();
  60.         }catch (Exception e){
  61.             username = null;
  62.         }
  63.         return username;
  64.     }
  65.     // token 是否能刷新
  66.     public boolean tokenCanRef(String token){
  67.         return !isExpired(token); // 有效地 token 才能被刷新
  68.     }
  69.     // 刷新 token
  70.     public String refToken(String token){
  71.         Claims claimsInToken = getClaimsInToken(token);
  72.         claimsInToken.put(CLAIM_KEY_CREATE,new Date());
  73.         return foundJWT(claimsInToken);
  74.     }
  75.     // 设置过期时间
  76.     private Date getExpiration() {
  77.         return new Date(System.currentTimeMillis() + expiration * 1000);
  78.     }
  79. }
复制代码
3.2 RespBean 公共返回对象
  1. @Data
  2. @AllArgsConstructor
  3. @NoArgsConstructor
  4. public class RespBean {
  5.     private long code;
  6.     private String message;
  7.     private Object obj;
  8.     /**
  9.      * 返回响应结果
  10.      */
  11.     private static RespBean result(long code, String message, Object obj) {
  12.         return new RespBean(code, message, obj);
  13.     }
  14.     /*
  15.        返回成功响应
  16.         */
  17.     public static RespBean success(String message) {
  18.         return result(200, message, null);
  19.     }
  20.     /*
  21.     返回成功响应以及数据体
  22.      */
  23.     public static RespBean success(String message, Object obj) {
  24.         return result(200, message, obj);
  25.     }
  26.     /*
  27.     返回错误响应
  28.      */
  29.     public static RespBean error(String message) {
  30.         return result(500, message, null);
  31.     }
  32. }
复制代码
4. 让 User 实体类实现 UserDetails 的方法成为 Security 验证的用户核心主体

由于 Security 框架的性质,自定义授权和认证时,一般情况下会自定义 UserDetails。
[code]@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

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

大号在练葵花宝典

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表