Springboot3 + SpringSecurity + JWT + OpenApi3 实现认证授权

news/2024/10/21 6:45:29/

Springboot3 + SpringSecurity + JWT + OpenApi3 实现双token

目前全网最新的 Spring Security + JWT 实现双 Token 的案例!收藏就对了,欢迎各位看友学习参考。此项目由作者个人创作,可以供大家学习和项目实战使用,创作不易,转载请注明出处!

该项目使用目前最新的 Sprin Boot3 版本,采用目前市面上最主流的 JWT 认证方式,实现双token刷新。

温馨提示:SpringBoot3 版本必须要使用 JDK11 或 JDK19

SpringBoot3 新特性

Spring Boot3 是一个非常重要的版本,将会面临一个新的发展征程!Sprin Boot 3.0 包含了 12 个月以来,151 个人的 5700+ 次 commit 的贡献。这是自 4 年半前发布的 2.0 版本以来的第一次重大修订,这也是第一个支持 Spring Framework 6.0 和 GraaIVM 的 Spring Boot GA 版本。

Spring Boot 3.0 新版本的主要亮点:

  1. 最低要求为 Java 17 ,兼容 Java 19
  2. 支持用 GraalVM 生成原生镜像,代替了 Spring Native
  3. 通过 Micrometer 和 Micrometer 追踪提高应用可观察性
  4. 支持具有 EE 9 baseline 的 Jakarta EE 10

为什么采用双 Token刷新?

**场景假设:**星期四小金上班的时候摸鱼,准备在某APP 上面追剧,已经深深的陷入了角色中无法自拔,此时如果 Token 过期了 ,小金就不得不重新返回登录界面,重新进行登录,那么这样小金的一次完整的追剧体验就被打断了,这种设计带给小金的体验并不好,于是就需要使用双 Token 来解决。

**如何使用:**在小金首次登陆 APP 时,APP 会返回两个 Token 给小金,一个 accessToken,一个 refreshToken,其中 accessToken 的过期时间比较短,refreshToken 的时间比较长。当 accessToken 失效后,会通过 refreshToken 去重新获取 accessToken,这样一来就可以在不被察觉的情况下仍然使小金保持登录状态,让小金误以为自己一直是登录的状态。并且每次使用refreshToken 后会刷新,每一次刷新后的 refreshToken 都是不相同的。

**优势说明:**小金能够有一次完整的追剧体验,除非摸鱼时被老板发现了。accessToken 的存在,保证了登录的正常验证,因为 accessToken 的过期时间比较短,所以也可以保证账号的安全性。refreshToken 的存在,保证了小金无需在短时间内反复的登录来保持 Token 的有效性,同时也保证了活跃用户的登录状态可以一直延续而不需要重新登录,反复刷新也防止了某些不怀好意的人获取 refreshToken 后对用户账号进行不良操作。

一图胜千言:

image-20230604084837740

项目准备

项目采用 Spring Boot 3 + Spring Security + JWT + MyBatis-Plus + Lombok 进行搭建。

创建数据库

user 表

image-20230603220205094

token 表

在实际中应该把 token 信息保存到 redis

image-20230603220333914

创建 Spring Boot 项目

创建一个 Spring Boot 3 项目,一定要选择 Java 17 或者 Java 19

引入依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId><version>3.0.4</version>
</dependency><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>
</dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.11.5</version>
</dependency>

编写配置文件

server:port: 8417
spring:application:name: Spring Boot 3 + Spring Security + JWT + OpenAPI3datasource:url: jdbc:mysql://localhost:3306/w_adminusername: rootpassword: jcjl417
mybatis-plus:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImplglobal-config:db-config:table-prefix: t_id-type: autotype-aliases-package: com.record.security.entitymapper-locations: classpath:mapper/*.xml
application:security:jwt:secret-key: VUhJT0pJT0hVWUlHRFVGVFdPSVJISVVHWUZHVkRVR0RISVVIREJZI1VJSEZTVUdZR0ZTVVk=expiration: 86400000 # 1天refresh-token:expiration: 604800000 # 7 天
springdoc:swagger-ui:path: /docs.htmltags-sorter: alphaoperations-sorter: alphaapi-docs:path: /v3/api-docs

项目实现

准备项目所需要的一系列代码,如 entity、controller 、service、mapper 等

系统角色 Role

定义一个角色(Role)枚举,详细代码参考文章结尾处的项目源码

public enum Role {// 用户USER(Collections.emptySet()),// 一线人员CHASER( ... ),// 部门主管SUPERVISOR( ... ),// 系统管理员ADMIN( ... ),;@Getterprivate final Set<Permission> permissions;public List<SimpleGrantedAuthority> getAuthorities() {var authorities = getPermissions().stream().map(permission -> new SimpleGrantedAuthority(permission.getPermission())).collect(Collectors.toList());authorities.add(new SimpleGrantedAuthority("ROLE_" + this.name()));return authorities;}
}

User 实现 UserDetails

温馨提示:

由于 Spring Security 源码设计的时候 ,将用户名和密码属性定义为 username 和 password,所以我们看到的大部分教程都会遵循源码中的方式,习惯性的将用户名定义为 username,密码定义为 password。

其实我们大可不必遵守这个规则,在我的系统中使用邮箱登录,也即是将邮箱(email)作为 Security 中的用户名(username),那么我必须要将用户输入的 email 作为 username 来存放,这会使我感到非常的不适,因为我的系统中正真的 username 将会 用另外一个单词来命名。

如何避免登录时的字段必须设置为 username 和 password 呢?

重写 getter方法, 只有你的系统中登录的用户名和密码属性不是 username 和 password 的情况下 ,你进行重写才会看到下面红色框中的提示。

202306032035283

重写 username 和 password 的 getter方法

@Override
public String getUsername() {return email;
}@Override
public String getPassword() {return password;
}

Security 配置文件

需要注意的是 WebSecurityConfigurerAdapter 在 Spring Security 中已经被弃用和移除

下面将采用新的配置文件

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableMethodSecurity
public class SecurityConfiguration {private final JwtAuthenticationFilter jwtAuthFilter;private final AuthenticationProvider authenticationProvider;private final LogoutHandler logoutHandler;private final RestAuthorizationEntryPoint restAuthorizationEntryPoint;private final RestfulAccessDeniedHandler restfulAccessDeniedHandler;@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.csrf().disable().authorizeHttpRequests().requestMatchers("/api/v1/auth/**","/api/v1/test/**","/v2/api-docs","/v3/api-docs","/v3/api-docs/**","/swagger-resources","/swagger-resources/**","/configuration/ui","/configuration/security","/swagger-ui/**","/doc.html","/webjars/**","/swagger-ui.html","/favicon.ico").permitAll().requestMatchers("/api/v1/supervisor/**").hasAnyRole(SUPERVISOR.name(), ADMIN.name()).requestMatchers(GET, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_READ.name(), ADMIN_READ.name()).requestMatchers(POST, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_CREATE.name(), ADMIN_CREATE.name()).requestMatchers(PUT, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_UPDATE.name(), ADMIN_UPDATE.name()).requestMatchers(DELETE, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_DELETE.name(), ADMIN_DELETE.name()).requestMatchers("/api/v1/chaser/**").hasRole(CHASER.name()).requestMatchers(GET, "/api/v1/chaser/**").hasAuthority(CHASER_READ.name()).requestMatchers(POST, "/api/v1/chaser/**").hasAuthority(CHASER_CREATE.name()).requestMatchers(PUT, "/api/v1/chaser/**").hasAuthority(CHASER_UPDATE.name()).requestMatchers(DELETE, "/api/v1/chaser/**").hasAuthority(CHASER_DELETE.name()).anyRequest().authenticated().and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authenticationProvider(authenticationProvider)//添加jwt 登录授权过滤器.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class).logout().logoutUrl("/api/v1/auth/logout").addLogoutHandler(logoutHandler).logoutSuccessHandler((request, response, authentication) -> SecurityContextHolder.clearContext());//添加自定义未授权和未登录结果返回http.exceptionHandling().accessDeniedHandler(restfulAccessDeniedHandler).authenticationEntryPoint(restAuthorizationEntryPoint);return http.build();}
}

OpenApi 配置文件

OpenApi 依赖

<dependency><groupId>org.springdoc</groupId><artifactId>springdoc-openapi-starter-webmvc-ui</artifactId><version>2.1.0</version>
</dependency>

OpenApiConfig 配置

OpenApi3 生成接口文档,主要配置如下

  • Api Group(分组)
  • Bearer Authorization(认证)
  • Customer(自定义请求头等)
@Configuration
public class OpenApiConfig {@Beanpublic OpenAPI customOpenAPI(){return new OpenAPI().info(info()).externalDocs(externalDocs()).components(components()).addSecurityItem(securityRequirement());}private Info info(){return new Info().title("京茶吉鹿的 Demo").version("v0.0.1").description("Spring Boot 3 + Spring Security + JWT + OpenAPI3").license(new License().name("Apache 2.0") // The Apache License, Version 2.0.url("https://www.apache.org/licenses/LICENSE-2.0.html")).contact(new Contact().name("京茶吉鹿").url("http://localost:8417").email("jc.top@qq.com")).termsOfService("http://localhost:8417");}private ExternalDocumentation externalDocs() {return new ExternalDocumentation().description("京茶吉鹿的开放文档").url("http://localhost:8417/docs");}private Components components(){return new Components().addSecuritySchemes("Bearer Authorization",new SecurityScheme().name("Bearer 认证").type(SecurityScheme.Type.HTTP).scheme("bearer").bearerFormat("JWT").in(SecurityScheme.In.HEADER)).addSecuritySchemes("Basic Authorization",new SecurityScheme().name("Basic 认证").type(SecurityScheme.Type.HTTP).scheme("basic"));}private SecurityRequirement securityRequirement() {return new SecurityRequirement().addList("Bearer Authorization");}private List<SecurityRequirement> security(Components components) {return components.getSecuritySchemes().keySet().stream().map(k -> new SecurityRequirement().addList(k)).collect(Collectors.toList());}/*** 通用接口* @return*/@Beanpublic GroupedOpenApi publicApi(){return GroupedOpenApi.builder().group("身份认证").pathsToMatch("/api/v1/auth/**")// 为指定组设置请求头// .addOperationCustomizer(operationCustomizer()).build();}/*** 一线人员* @return*/@Beanpublic GroupedOpenApi chaserApi(){return GroupedOpenApi.builder().group("一线人员").pathsToMatch("/api/v1/chaser/**","/api/v1/experience/search/**","/api/v1/log/**","/api/v1/contact/**","/api/v1/admin/user/update").pathsToExclude("/api/v1/experience/search/id").build();}/*** 部门主管* @return*/@Beanpublic GroupedOpenApi supervisorApi(){return GroupedOpenApi.builder().group("部门主管").pathsToMatch("/api/v1/supervisor/**","/api/v1/experience/**","/api/v1/schedule/**","/api/v1/contact/**","/api/v1/admin/user/update").build();}/*** 系统管理员* @return*/@Beanpublic GroupedOpenApi adminApi(){return GroupedOpenApi.builder().group("系统管理员").pathsToMatch("/api/v1/admin/**")// .addOpenApiCustomiser(openApi -> openApi.info(new Info().title("京茶吉鹿接口—Admin"))).build();}
}

image-20230603224928028

Security 接口赋权的方式

hasRole及hasAuthority的区别?

hasAuthority能通过的身份必须与字符串一模一样,而hasRole能通过的身前缀必须带有ROLE_,同时可以通过两种字符串,一是带有前缀ROLE_,二是不带前缀ROLE_

通过配置文件

在配置文件中指明访问路径的权限

.requestMatchers("/api/v1/supervisor/**").hasAnyRole(SUPERVISOR.name(), ADMIN.name())
.requestMatchers(GET, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_READ.name(), ADMIN_READ.name())
.requestMatchers(POST, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_CREATE.name(), ADMIN_CREATE.name())
.requestMatchers(PUT, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_UPDATE.name(), ADMIN_UPDATE.name())
.requestMatchers(DELETE, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_DELETE.name(), ADMIN_DELETE.name())
.requestMatchers("/api/v1/chaser/**").hasRole(CHASER.name())
.requestMatchers(GET, "/api/v1/chaser/**").hasAuthority(CHASER_READ.name())
.requestMatchers(POST, "/api/v1/chaser/**").hasAuthority(CHASER_CREATE.name())
.requestMatchers(PUT, "/api/v1/chaser/**").hasAuthority(CHASER_UPDATE.name())
.requestMatchers(DELETE, "/api/v1/chaser/**").hasAuthority(CHASER_DELETE.name())

通过注解

@RestController
@RequestMapping("/api/v1/admin")
@PreAuthorize("hasRole('ADMIN')")
@Tag(name = "系统管理员权限测试")
public class AdminController {@GetMapping@PreAuthorize("hasAuthority('admin:read')")public String get() {return "GET |==| AdminController";}@PostMapping@PreAuthorize("hasAuthority('admin:create')")public String post() {return "POST |==| AdminController";}
}

测试

我们登录认证成功后,系统会为我们返回 access_token 和 refresh_token。

image-20230604082145598

快速获取项目源码

下方微信公众号【京茶吉鹿】中回复 JWT 免费获取项目源代码


http://www.ppmy.cn/news/241344.html

相关文章

蓝桥 巧克力 贪心 排序 java

&#x1f351; 算法题解专栏 &#x1f351; 蓝桥 巧克力 输入 10 3 1 6 5 2 7 3 3 10 10输出 18&#x1f351; 思路 &#x1f364; 前边日期安排后影响后边的安排&#xff0c;但后边的安排不会影响前边的安排 &#x1f364; 从后往前步步贪心实现局部最优&#xff1a;在可选…

C语言,你觉得难吗?

C语言&#xff0c;众所周知&#xff0c;作为许多学校的编程入门课程&#xff0c;它并非易如反掌&#xff0c;甚至可称为最具挑战性的语言之一。学习C语言的难点不在于其语法&#xff0c;因为它的语法知识点并不繁多。其真正难以掌握之处在于如何运用这些简单的指令设计出复杂的…

企业课(理论)

数据链路层 IP地址&#xff1a;32bit 十进制、二进制表示 Mac地址&#xff1a;48bit 十六进制 &#xff08;0-9&#xff0c;a-f&#xff09; Mac地址&#xff1a; 单播Mac地址&#xff1a;一对一 48bit第八bit为0 组播Mac地址&#xff1a;一对多 48bit第八b…

iOS app上架截屏尺寸 5.5英寸:1242x2208 6.5英寸:1242x2688

5.5英寸和6.5英寸的iphone的截屏必须上传 5.5英寸&#xff1a;1242x2208 6.5英寸&#xff1a;1242x2688

not in 查不出数据

当not in (idList) 里面 id存在空时&#xff0c;not in null 等同于 !null, 我们知道数据库判断空是is null &#xff0c;如果用!null就查不到数据了

缓存一致性

《缓存更新的套路》 -陈皓 &#xff08;不考虑二步失败&#xff0c;只考虑并发状况&#xff09; 先指出了”先删除缓存&#xff0c;再更新DB“策略在并发时存在的一致性问题 说明cache aside缓存模式&#xff0c;其更新为“先更新DB&#xff0c;再让缓存失效”模式&#xff08…

令人惊艳的高效算法盘点(附示例)

令人惊艳的高效算法盘点&#xff08;附示例&#xff09; 在计算机科学领域&#xff0c;算法是解决问题的基石。有些算法&#xff0c;因为其高效性和惊人表现&#xff0c;令人瞩目。本文将为你介绍一些令人惊艳的高效算法&#xff0c;让我们一起来领略这些算法的魅力吧&#xf…

matlab作业 阳光的快乐老爹,霍思燕6岁儿子近照曝光,调皮起来超阳光,完美继承老爹容颜!...

霍思燕6岁儿子近照曝光&#xff0c;调皮起来超阳光&#xff0c;完美继承老爹容颜&#xff01; 明星带娃一起参加真人秀是最近几年比较常见的节目形式&#xff0c;也让我们近距离的关注到一批可爱呆萌的星二代。因为父母都是演艺圈的&#xff0c;所以大部分星二代长得都是非常好…