写点什么

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

作者:京茶吉鹿
  • 2023-06-06
    四川
  • 本文字数:7208 字

    阅读完需:约 24 分钟

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

目前全网最新的 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


引入依赖


<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: 8417spring:    application:      name: Spring Boot 3 + Spring Security + JWT + OpenAPI3    datasource:        url: jdbc:mysql://localhost:3306/w_admin        username: root        password: jcjl417mybatis-plus:    configuration:      log-impl: org.apache.ibatis.logging.stdout.StdOutImpl    global-config:        db-config:            table-prefix: t_            id-type: auto    type-aliases-package: com.record.security.entity    mapper-locations: classpath:mapper/*.xmlapplication:    security:        jwt:            secret-key: VUhJT0pJT0hVWUlHRFVGVFdPSVJISVVHWUZHVkRVR0RISVVIREJZI1VJSEZTVUdZR0ZTVVk=            expiration: 86400000 # 1天            refresh-token:                expiration: 604800000 # 7 天springdoc:    swagger-ui:        path: /docs.html        tags-sorter: alpha        operations-sorter: alpha    api-docs:        path: /v3/api-docs
复制代码

项目实现

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

系统角色 Role

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


public enum Role {
// 用户 USER(Collections.emptySet()), // 一线人员 CHASER( ... ), // 部门主管 SUPERVISOR( ... ), // 系统管理员 ADMIN( ... ), ;
@Getter private 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 的情况下 ,你进行重写才会看到下面红色框中的提示。


重写 username 和 password 的 getter 方法


@Overridepublic String getUsername() {    return email;}
@Overridepublic String getPassword() { return password;}
复制代码

Security 配置文件

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

下面将采用新的配置文件


@Configuration@EnableWebSecurity@RequiredArgsConstructor@EnableMethodSecuritypublic class SecurityConfiguration {
private final JwtAuthenticationFilter jwtAuthFilter; private final AuthenticationProvider authenticationProvider; private final LogoutHandler logoutHandler; private final RestAuthorizationEntryPoint restAuthorizationEntryPoint; private final RestfulAccessDeniedHandler restfulAccessDeniedHandler;
@Bean public 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(自定义请求头等)


@Configurationpublic class OpenApiConfig {
@Bean public 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 */ @Bean public GroupedOpenApi publicApi(){ return GroupedOpenApi.builder() .group("身份认证") .pathsToMatch("/api/v1/auth/**") // 为指定组设置请求头 // .addOperationCustomizer(operationCustomizer()) .build(); } /** * 一线人员 * @return */ @Bean public 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 */ @Bean public 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 */ @Bean public GroupedOpenApi adminApi(){ return GroupedOpenApi.builder() .group("系统管理员") .pathsToMatch("/api/v1/admin/**") // .addOpenApiCustomiser(openApi -> openApi.info(new Info().title("京茶吉鹿接口—Admin"))) .build(); }}
复制代码


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。


项目源代码获取

微信公众号【京茶吉鹿】内回复 JWT 获取项目源码。

发布于: 刚刚阅读数: 3
用户头像

京茶吉鹿

关注

励志写最优美的代码! 2022-05-30 加入

关注微信公众号【京茶吉鹿】,给您分享实用的文章,推荐优质的项目!

评论

发布
暂无评论
Springboot3 + SpringSecurity + JWT + OpenApi3 实现认证授权_spring security_京茶吉鹿_InfoQ写作社区