当前位置:   article > 正文

Springboot3 + SpringSecurity + JWT + OpenApi3 实现认证授权_定义一个角色(role)枚举,详细代码参考文章结尾处的项目源码

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

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 后对用户账号进行不良操作。

一图胜千言:

项目准备

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

创建数据库

user 表

token 表

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

创建 Spring Boot 项目

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

引入依赖

  1. xml复制代码<dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-security</artifactId>
  4. <version>3.0.4</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>io.jsonwebtoken</groupId>
  8. <artifactId>jjwt-api</artifactId>
  9. <version>0.11.5</version>
  10. </dependency>
  11. <dependency>
  12. <groupId>io.jsonwebtoken</groupId>
  13. <artifactId>jjwt-impl</artifactId>
  14. <version>0.11.5</version>
  15. </dependency>
  16. <dependency>
  17. <groupId>io.jsonwebtoken</groupId>
  18. <artifactId>jjwt-jackson</artifactId>
  19. <version>0.11.5</version>
  20. </dependency>

编写配置文件

  1. yml复制代码server:
  2. port: 8417
  3. spring:
  4. application:
  5. name: Spring Boot 3 + Spring Security + JWT + OpenAPI3
  6. datasource:
  7. url: jdbc:mysql://localhost:3306/w_admin
  8. username: root
  9. password: jcjl417
  10. mybatis-plus:
  11. configuration:
  12. log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  13. global-config:
  14. db-config:
  15. table-prefix: t_
  16. id-type: auto
  17. type-aliases-package: com.record.security.entity
  18. mapper-locations: classpath:mapper/*.xml
  19. application:
  20. security:
  21. jwt:
  22. secret-key: VUhJT0pJT0hVWUlHRFVGVFdPSVJISVVHWUZHVkRVR0RISVVIREJZI1VJSEZTVUdZR0ZTVVk=
  23. expiration: 86400000 # 1
  24. refresh-token:
  25. expiration: 604800000 # 7
  26. springdoc:
  27. swagger-ui:
  28. path: /docs.html
  29. tags-sorter: alpha
  30. operations-sorter: alpha
  31. api-docs:
  32. path: /v3/api-docs

项目实现

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

系统角色 Role

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

  1. java复制代码public enum Role {
  2. // 用户
  3. USER(Collections.emptySet()),
  4. // 一线人员
  5. CHASER( ... ),
  6. // 部门主管
  7. SUPERVISOR( ... ),
  8. // 系统管理员
  9. ADMIN( ... ),
  10. ;
  11. @Getter
  12. private final Set<Permission> permissions;
  13. public List<SimpleGrantedAuthority> getAuthorities() {
  14. var authorities = getPermissions()
  15. .stream()
  16. .map(permission -> new SimpleGrantedAuthority(permission.getPermission()))
  17. .collect(Collectors.toList());
  18. authorities.add(new SimpleGrantedAuthority("ROLE_" + this.name()));
  19. return authorities;
  20. }
  21. }

User 实现 UserDetails

温馨提示:

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

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

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

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

重写 username 和 password 的 getter方法

  1. java复制代码@Override
  2. public String getUsername() {
  3. return email;
  4. }
  5. @Override
  6. public String getPassword() {
  7. return password;
  8. }

Security 配置文件

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

下面将采用新的配置文件

  1. java复制代码@Configuration
  2. @EnableWebSecurity
  3. @RequiredArgsConstructor
  4. @EnableMethodSecurity
  5. public class SecurityConfiguration {
  6. private final JwtAuthenticationFilter jwtAuthFilter;
  7. private final AuthenticationProvider authenticationProvider;
  8. private final LogoutHandler logoutHandler;
  9. private final RestAuthorizationEntryPoint restAuthorizationEntryPoint;
  10. private final RestfulAccessDeniedHandler restfulAccessDeniedHandler;
  11. @Bean
  12. public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
  13. http.csrf()
  14. .disable()
  15. .authorizeHttpRequests()
  16. .requestMatchers(
  17. "/api/v1/auth/**",
  18. "/api/v1/test/**",
  19. "/v2/api-docs",
  20. "/v3/api-docs",
  21. "/v3/api-docs/**",
  22. "/swagger-resources",
  23. "/swagger-resources/**",
  24. "/configuration/ui",
  25. "/configuration/security",
  26. "/swagger-ui/**",
  27. "/doc.html",
  28. "/webjars/**",
  29. "/swagger-ui.html",
  30. "/favicon.ico"
  31. ).permitAll()
  32. .requestMatchers("/api/v1/supervisor/**").hasAnyRole(SUPERVISOR.name(), ADMIN.name())
  33. .requestMatchers(GET, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_READ.name(), ADMIN_READ.name())
  34. .requestMatchers(POST, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_CREATE.name(), ADMIN_CREATE.name())
  35. .requestMatchers(PUT, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_UPDATE.name(), ADMIN_UPDATE.name())
  36. .requestMatchers(DELETE, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_DELETE.name(), ADMIN_DELETE.name())
  37. .requestMatchers("/api/v1/chaser/**").hasRole(CHASER.name())
  38. .requestMatchers(GET, "/api/v1/chaser/**").hasAuthority(CHASER_READ.name())
  39. .requestMatchers(POST, "/api/v1/chaser/**").hasAuthority(CHASER_CREATE.name())
  40. .requestMatchers(PUT, "/api/v1/chaser/**").hasAuthority(CHASER_UPDATE.name())
  41. .requestMatchers(DELETE, "/api/v1/chaser/**").hasAuthority(CHASER_DELETE.name())
  42. .anyRequest()
  43. .authenticated()
  44. .and()
  45. .sessionManagement()
  46. .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
  47. .and()
  48. .authenticationProvider(authenticationProvider)
  49. //添加jwt 登录授权过滤器
  50. .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
  51. .logout()
  52. .logoutUrl("/api/v1/auth/logout")
  53. .addLogoutHandler(logoutHandler)
  54. .logoutSuccessHandler((request, response, authentication) -> SecurityContextHolder.clearContext())
  55. ;
  56. //添加自定义未授权和未登录结果返回
  57. http.exceptionHandling()
  58. .accessDeniedHandler(restfulAccessDeniedHandler)
  59. .authenticationEntryPoint(restAuthorizationEntryPoint);
  60. return http.build();
  61. }
  62. }

OpenApi 配置文件

OpenApi 依赖

  1. xml复制代码<dependency>
  2. <groupId>org.springdoc</groupId>
  3. <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
  4. <version>2.1.0</version>
  5. </dependency>

OpenApiConfig 配置

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

  • Api Group(分组)
  • Bearer Authorization(认证)
  • Customer(自定义请求头等)
  1. java复制代码@Configuration
  2. public class OpenApiConfig {
  3. @Bean
  4. public OpenAPI customOpenAPI(){
  5. return new OpenAPI()
  6. .info(info())
  7. .externalDocs(externalDocs())
  8. .components(components())
  9. .addSecurityItem(securityRequirement())
  10. ;
  11. }
  12. private Info info(){
  13. return new Info()
  14. .title("京茶吉鹿的 Demo")
  15. .version("v0.0.1")
  16. .description("Spring Boot 3 + Spring Security + JWT + OpenAPI3")
  17. .license(new License()
  18. .name("Apache 2.0") // The Apache License, Version 2.0
  19. .url("https://www.apache.org/licenses/LICENSE-2.0.html"))
  20. .contact(new Contact()
  21. .name("京茶吉鹿")
  22. .url("http://localost:8417")
  23. .email("jc.top@qq.com"))
  24. .termsOfService("http://localhost:8417")
  25. ;
  26. }
  27. private ExternalDocumentation externalDocs() {
  28. return new ExternalDocumentation()
  29. .description("京茶吉鹿的开放文档")
  30. .url("http://localhost:8417/docs");
  31. }
  32. private Components components(){
  33. return new Components()
  34. .addSecuritySchemes("Bearer Authorization",
  35. new SecurityScheme()
  36. .name("Bearer 认证")
  37. .type(SecurityScheme.Type.HTTP)
  38. .scheme("bearer")
  39. .bearerFormat("JWT")
  40. .in(SecurityScheme.In.HEADER)
  41. )
  42. .addSecuritySchemes("Basic Authorization",
  43. new SecurityScheme()
  44. .name("Basic 认证")
  45. .type(SecurityScheme.Type.HTTP)
  46. .scheme("basic")
  47. )
  48. ;
  49. }
  50. private SecurityRequirement securityRequirement() {
  51. return new SecurityRequirement()
  52. .addList("Bearer Authorization");
  53. }
  54. private List<SecurityRequirement> security(Components components) {
  55. return components.getSecuritySchemes()
  56. .keySet()
  57. .stream()
  58. .map(k -> new SecurityRequirement().addList(k))
  59. .collect(Collectors.toList());
  60. }
  61. /**
  62. * 通用接口
  63. * @return
  64. */
  65. @Bean
  66. public GroupedOpenApi publicApi(){
  67. return GroupedOpenApi.builder()
  68. .group("身份认证")
  69. .pathsToMatch("/api/v1/auth/**")
  70. // 为指定组设置请求头
  71. // .addOperationCustomizer(operationCustomizer())
  72. .build();
  73. }
  74. /**
  75. * 一线人员
  76. * @return
  77. */
  78. @Bean
  79. public GroupedOpenApi chaserApi(){
  80. return GroupedOpenApi.builder()
  81. .group("一线人员")
  82. .pathsToMatch("/api/v1/chaser/**",
  83. "/api/v1/experience/search/**",
  84. "/api/v1/log/**",
  85. "/api/v1/contact/**",
  86. "/api/v1/admin/user/update")
  87. .pathsToExclude("/api/v1/experience/search/id")
  88. .build();
  89. }
  90. /**
  91. * 部门主管
  92. * @return
  93. */
  94. @Bean
  95. public GroupedOpenApi supervisorApi(){
  96. return GroupedOpenApi.builder()
  97. .group("部门主管")
  98. .pathsToMatch("/api/v1/supervisor/**",
  99. "/api/v1/experience/**",
  100. "/api/v1/schedule/**",
  101. "/api/v1/contact/**",
  102. "/api/v1/admin/user/update")
  103. .build();
  104. }
  105. /**
  106. * 系统管理员
  107. * @return
  108. */
  109. @Bean
  110. public GroupedOpenApi adminApi(){
  111. return GroupedOpenApi.builder()
  112. .group("系统管理员")
  113. .pathsToMatch("/api/v1/admin/**")
  114. // .addOpenApiCustomiser(openApi -> openApi.info(new Info().title("京茶吉鹿接口—Admin")))
  115. .build();
  116. }
  117. }

Security 接口赋权的方式

hasRole及hasAuthority的区别?

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

通过配置文件

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

  1. java复制代码.requestMatchers("/api/v1/supervisor/**").hasAnyRole(SUPERVISOR.name(), ADMIN.name())
  2. .requestMatchers(GET, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_READ.name(), ADMIN_READ.name())
  3. .requestMatchers(POST, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_CREATE.name(), ADMIN_CREATE.name())
  4. .requestMatchers(PUT, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_UPDATE.name(), ADMIN_UPDATE.name())
  5. .requestMatchers(DELETE, "/api/v1/supervisor/**").hasAnyAuthority(SUPERVISOR_DELETE.name(), ADMIN_DELETE.name())
  1. java复制代码.requestMatchers("/api/v1/chaser/**").hasRole(CHASER.name())
  2. .requestMatchers(GET, "/api/v1/chaser/**").hasAuthority(CHASER_READ.name())
  3. .requestMatchers(POST, "/api/v1/chaser/**").hasAuthority(CHASER_CREATE.name())
  4. .requestMatchers(PUT, "/api/v1/chaser/**").hasAuthority(CHASER_UPDATE.name())
  5. .requestMatchers(DELETE, "/api/v1/chaser/**").hasAuthority(CHASER_DELETE.name())

通过注解

  1. java复制代码@RestController
  2. @RequestMapping("/api/v1/admin")
  3. @PreAuthorize("hasRole('ADMIN')")
  4. @Tag(name = "系统管理员权限测试")
  5. public class AdminController {
  6. @GetMapping
  7. @PreAuthorize("hasAuthority('admin:read')")
  8. public String get() {
  9. return "GET |==| AdminController";
  10. }
  11. @PostMapping
  12. @PreAuthorize("hasAuthority('admin:create')")
  13. public String post() {
  14. return "POST |==| AdminController";
  15. }
  16. }

测试

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/388660
推荐阅读
  

闽ICP备14008679号