赞
踩
目前全网最新的 Spring Security + JWT 实现双 Token 的案例!收藏就对了,欢迎各位看友学习参考。此项目由作者个人创作,可以供大家学习和项目实战使用,创作不易,转载请注明出处!
该项目使用目前最新的 Sprin Boot3 版本,采用目前市面上最主流的 JWT 认证方式,实现双token刷新。
温馨提示:SpringBoot3 版本必须要使用 JDK11 或 JDK19
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 新版本的主要亮点:
**场景假设:**星期四小金上班的时候摸鱼,准备在某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 3 项目,一定要选择 Java 17 或者 Java 19
引入依赖
- xml复制代码<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>

编写配置文件
- yml复制代码server:
- port: 8417
- spring:
- application:
- name: Spring Boot 3 + Spring Security + JWT + OpenAPI3
- datasource:
- url: jdbc:mysql://localhost:3306/w_admin
- username: root
- password: jcjl417
- mybatis-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/*.xml
- application:
- 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)枚举,详细代码参考文章结尾处的项目源码
- java复制代码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;
- }
- }

温馨提示:
由于 Spring Security 源码设计的时候 ,将用户名和密码属性定义为 username 和 password,所以我们看到的大部分教程都会遵循源码中的方式,习惯性的将用户名定义为 username,密码定义为 password。
其实我们大可不必遵守这个规则,在我的系统中使用邮箱登录,也即是将邮箱(email)作为 Security 中的用户名(username),那么我必须要将用户输入的 email 作为 username 来存放,这会使我感到非常的不适,因为我的系统中正真的 username 将会 用另外一个单词来命名。
如何避免登录时的字段必须设置为 username 和 password 呢?
重写 getter方法, 只有你的系统中登录的用户名和密码属性不是 username 和 password 的情况下 ,你进行重写才会看到下面红色框中的提示。
重写 username 和 password 的 getter方法
- java复制代码@Override
- public String getUsername() {
- return email;
- }
-
- @Override
- public String getPassword() {
- return password;
- }
需要注意的是 WebSecurityConfigurerAdapter 在 Spring Security 中已经被弃用和移除了
下面将采用新的配置文件
- java复制代码@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;
-
- @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 依赖
- xml复制代码<dependency>
- <groupId>org.springdoc</groupId>
- <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
- <version>2.1.0</version>
- </dependency>
OpenApiConfig 配置
OpenApi3 生成接口文档,主要配置如下
- java复制代码@Configuration
- public 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();
- }
- }

hasRole及hasAuthority的区别?
hasAuthority能通过的身份必须与字符串一模一样,而hasRole能通过的身前缀必须带有ROLE_,同时可以通过两种字符串,一是带有前缀ROLE_,二是不带前缀ROLE_。
在配置文件中指明访问路径的权限
- java复制代码.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())
-
- java复制代码.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())
- java复制代码@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。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。