Springboot3 + SpringSecurity + JWT + OpenApi3 implémente l'authentification et l'autorisation

Springboot3 + SpringSecurity + JWT + OpenApi3 réalisent un double jeton

À l'heure actuelle, le dernier cas d'implémentation Spring Security + JWT de double jeton dans l'ensemble du réseau! Il est juste de le collecter, et vous êtes invités à le lire et à vous y référer. Ce projet est créé par l'auteur personnellement, il peut être utilisé pour l'étude et le projet de combat réel de chacun, il n'est pas facile à créer, veuillez indiquer la source pour la réimpression !

Le projet utilise la dernière version de Sprin Boot3 et adopte la méthode d'authentification JWT la plus courante du marché pour obtenir une actualisation à double jeton.

Rappel : la version SpringBoot3 doit utiliser JDK11 ou JDK19

Nouvelles fonctionnalités de SpringBoot3

Spring Boot3 est une version très importante et fera face à un nouveau parcours de développement ! Sprin Boot 3.0 comprend plus de 5700 commits de 151 personnes au cours des 12 derniers mois. Il s'agit de la première révision majeure depuis la version 2.0, publiée il y a 4,5 ans, et de la première version de Spring Boot GA à prendre en charge Spring Framework 6.0 et GraaIVM.

Les principaux points forts de la nouvelle version de Spring Boot 3.0 :

  1. La configuration minimale requise est Java 17, compatible avec Java 19
  2. Prise en charge de la génération d'images natives avec GraalVM au lieu de Spring Native
  3. Améliorez l'observabilité des applications avec le traçage au micromètre et au micromètre
  4. Prise en charge de Jakarta EE 10 avec la ligne de base EE 9

Pourquoi utiliser l'actualisation à double jeton ?

** Hypothèse de scénario : ** Xiao Jin pêchait au travail jeudi et était sur le point de suivre un drame sur une APP, et était déjà profondément immergé dans le rôle. Si le jeton expirait à ce moment-là, Xiao Jin devait retourner à l'interface de connexion. Si vous vous reconnectez, l'expérience complète de Xiao Jin en matière de chasse aux drames sera interrompue. Cette conception n'apporte pas à Xiao Jin une bonne expérience, vous devez donc utiliser deux jetons pour le résoudre.

**Comment utiliser :** Lorsque Xiaojin se connecte à l'APP pour la première fois, l'APP renverra deux Tokens à Xiaojin, un accessToken et un refreshToken. Le délai d'expiration d'accessToken est relativement court, et le temps de refreshToken est relativement longue. Lorsque l'accessToken expire, le refreshToken sera utilisé pour obtenir à nouveau l'accessToken, de sorte que Xiaojin puisse toujours conserver le statut de connexion sans se faire remarquer, faisant croire à tort à Xiaojin qu'il est toujours connecté. Et le refreshToken sera actualisé à chaque fois qu'il est utilisé, et le refreshToken après chaque actualisation est différent.

**Description de l'avantage : **Xiao Jin peut avoir une expérience complète de la chasse aux drames, à moins qu'il ne soit découvert par le patron pendant qu'il pêche. L'existence d'accessToken assure la vérification normale de la connexion, car le délai d'expiration d'accessToken est relativement court, il peut donc également garantir la sécurité du compte. L'existence de refreshToken garantit que Xiaojin n'a pas besoin de se connecter à plusieurs reprises dans un court laps de temps pour maintenir la validité du jeton. Il garantit également que l'état de connexion des utilisateurs actifs peut continuer sans se reconnecter. L'actualisation répétée empêche également certains malveillant Certaines personnes effectuent de mauvaises opérations sur le compte d'utilisateur après avoir obtenu le refreshToken.

Une image vaut mieux que mille mots:

image-20230604084837740

préparation de projet

Le projet est construit avec Spring Boot 3 + Spring Security + JWT + MyBatis-Plus + Lombok.

créer une base de données

tableau des utilisateurs

image-20230603220205094

tableau des jetons

En pratique, les informations de jeton doivent être enregistrées dans redis

image-20230603220333914

Créer un projet Spring Boot

Pour créer un projet Spring Boot 3, assurez-vous de choisir Java 17 ou Java 19

Introduire des dépendances

<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>

Écrire un fichier de configuration

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

réalisation de projet

Préparez une série de codes requis pour le projet, tels que l'entité, le contrôleur, le service, le mappeur, etc.

Rôle système Rôle

Définir une énumération de rôle (Role), le code détaillé fait référence au code source du projet à la fin de l'article

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

L'utilisateur implémente UserDetails

Bons conseils :

Étant donné que le code source de Spring Security est conçu, les attributs de nom d'utilisateur et de mot de passe sont définis comme nom d'utilisateur et mot de passe, de sorte que la plupart des didacticiels que nous voyons suivront la voie dans le code source, et le nom d'utilisateur est généralement défini comme nom d'utilisateur et le mot de passe est défini comme mot de passe.

En fait, nous n'avons pas à suivre cette règle . J'utilise l'e-mail pour me connecter à mon système, c'est-à-dire que j'utilise l'e-mail (e-mail) comme nom d'utilisateur (nom d'utilisateur) dans la sécurité, puis je dois stocker l'e-mail saisi par l'utilisateur. comme nom d'utilisateur. Cela me mettrait très mal à l'aise, car le vrai nom d'utilisateur dans mon système sera nommé avec un autre mot.

Comment éviter que les champs de connexion doivent être définis sur nom d'utilisateur et mot de passe ?

Réécrivez la méthode getter, uniquement si les attributs username et password connectés à votre système ne sont pas username et password , vous verrez l'invite dans la case rouge ci-dessous si vous réécrivez.

202306032035283

Remplacer les méthodes de récupération de nom d'utilisateur et de mot de passe

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

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

Fichier de configuration de sécurité

Il convient de noter que WebSecurityConfigurerAdapter a été obsolète et supprimé dans Spring Security

Le nouveau fichier de configuration sera adopté ci-dessous

@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();
  }
}

Fichier de configuration OpenApi

Dépendances OpenApi

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

Configuration d'OpenApiConfig

OpenApi3 génère des documents d'interface, la configuration principale est la suivante

  • Groupe API (regroupement)
  • Autorisation du porteur
  • Client (en-tête de demande personnalisé, etc.)
@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("[email protected]"))
                .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();
    }
}

image-20230603224928028

Méthode d'autonomisation de l'interface de sécurité

Quelle est la différence entre hasRole et hasAuthority ?

L'identité qui peut être transmise par hasAuthority doit être exactement la même que la chaîne, et l'identité qui peut être transmise par hasRole doit avoir un préfixe . ROLE_En même temps, deux types de chaînes peuvent être transmises, l'une avec un préfixe ROLE_, et l'autre est sans préfixe ROLE_.

via le fichier de configuration

Spécifiez les autorisations de chemin d'accès dans le fichier de configuration

.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())

par annotation

@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";
    }
}

test

Une fois que nous nous sommes connectés et authentifiés avec succès, le système nous renverra access_token et refresh_token.

image-20230604082145598

Accès rapide au code source du projet

Répondez JWT dans le compte public WeChat [Jingcha Jilu] ci-dessous pour obtenir gratuitement le code source du projet

Je suppose que tu aimes

Origine blog.csdn.net/weixin_52372879/article/details/131059739
conseillé
Classement