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

3476 篇文章 105 订阅

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

引入依赖

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

定义一个角色(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;
  }
}

User 实现 UserDetails

温馨提示:

由于 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;
}

Security 配置文件

需要注意的是 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 配置文件

OpenApi 依赖

xml复制代码<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(自定义请求头等)
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();
    }
}

Security 接口赋权的方式

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。

  • 3
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
好的,下面是Spring Boot 3 + Spring Security 6 + JWT项目配置步骤: 1. 添加Spring SecurityJWT的依赖 在pom.xml文件中添加以下依赖: ``` <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> ``` 2. 配置JWT 在application.yml或者application.properties文件中添加JWT的配置信息: ``` jwt: secret: yourSecretKey expiration: 604800000 # 7 days ``` 3. 配置Spring Security 创建一个继承自WebSecurityConfigurerAdapter的配置类,并添加以下代码: ```java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomUserDetailsService customUserDetailsService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/api/auth/**") .permitAll() .anyRequest() .authenticated(); http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public JwtAuthenticationFilter jwtAuthenticationFilter() { return new JwtAuthenticationFilter(); } } ``` 其中,CustomUserDetailsService是自定义的用户认证服务,JwtAuthenticationFilter是自定义的JWT认证过滤器。 4. 编写JWT认证过滤器 创建一个继承自OncePerRequestFilter的JwtAuthenticationFilter,并添加以下代码: ```java public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private JwtTokenProvider jwtTokenProvider; @Autowired private CustomUserDetailsService customUserDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { String token = jwtTokenProvider.resolveToken(request); if (token != null && jwtTokenProvider.validateToken(token)) { Authentication authentication = jwtTokenProvider.getAuthentication(token); SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (JwtAuthenticationException ex) { SecurityContextHolder.clearContext(); response.sendError(ex.getHttpStatus().value(), ex.getMessage()); return; } filterChain.doFilter(request, response); } } ``` 其中,JwtTokenProvider是自定义的JWT Token提供器。在这个过滤器中,我们通过JWT Token提供器解析请求中的Token,并将用户认证信息存储在SecurityContextHolder中。 5. 编写JWT Token提供器 创建一个JwtTokenProvider类,并添加以下代码: ```java @Service public class JwtTokenProvider { @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration}") private Long expiration; public String createToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); return Jwts.builder() .setClaims(claims) .setSubject(userDetails.getUsername()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + expiration)) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } public boolean validateToken(String token) { try { Jwts.parser().setSigningKey(secret).parseClaimsJws(token); return true; } catch (JwtException | IllegalArgumentException ex) { throw new JwtAuthenticationException("Expired or invalid JWT token", HttpStatus.INTERNAL_SERVER_ERROR); } } public Authentication getAuthentication(String token) { UserDetails userDetails = customUserDetailsService.loadUserByUsername(getUsername(token)); return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); } public String getUsername(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody() .getSubject(); } public String resolveToken(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } } ``` 其中,UserDetails是Spring Security提供的用户认证信息对象,CustomUserDetailsService是自定义的用户认证服务。 在这个类中,我们使用JJWT库来创建和解析JWT Token,并在getAuthentication方法中从Token中获取用户认证信息,并将其封装成Spring Security的Authentication对象。 以上就是Spring Boot 3 + Spring Security 6 + JWT项目配置步骤。希望能够帮到您!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值