Springboot3 + SpringSecurity + JWT + OpenApi3で二重トークンを実現
現在、最新の Spring Security + JWT によるネットワーク全体へのデュアル Token の導入事例です。収集するのは正しいことです。ぜひ読んで参考にしてください。このプロジェクトは作者が個人的に作成したもので、みんなの学習やプロジェクトの実戦に使用できます。作成するのは簡単ではありません。転載する場合は出典を示してください。
このプロジェクトでは最新バージョンの Sprin Boot3 を使用し、市場で最も主流の JWT 認証方式を採用してデュアル トークン リフレッシュを実現します。
リマインダー: SpringBoot3 バージョンは JDK11 または JDK19 を使用する必要があります
SpringBoot3の新機能
Spring Boot3 は非常に重要なバージョンであり、新たな開発の旅に直面することになります。Sprin Boot 3.0 には、過去 12 か月間の 151 人の個人からの 5700 以上のコミットが含まれています。これは、4 年半前にリリースされたバージョン 2.0 以来のメジャー リビジョンであり、Spring Framework 6.0 と GraaIVM をサポートする最初の Spring Boot GA リリースです。
Spring Boot 3.0 の新しいバージョンの主なハイライトは次のとおりです。
- 最小要件は Java 17 であり、Java 19 と互換性があります
- Spring Native の代わりに GraalVM を使用したネイティブ イメージの生成のサポート
- マイクロメーターとマイクロメーター トレースによるアプリケーションの可観測性の向上
- EE 9 ベースラインによる Jakarta EE 10 のサポート
デュアル トークン リフレッシュを使用する理由は何ですか?
**シナリオの仮定: **シャオ ジンは木曜日に仕事で釣りをしていて、APP でドラマをフォローしようとしていたところです。彼はすでにその役にはまっていて、そこから抜け出すことができませんでした。この時点でトークンの有効期限が切れた場合、シャオはジンはログイン インターフェイスに戻る必要がありました。再度ログインすると、シャオ ジンのドラマを追う完全な体験が中断されます。この設計ではシャオ ジンに良い体験がもたらされないため、問題を解決するにはデュアル トークンを使用する必要があります。
**使用方法:** Xiaojin が初めて APP にログインすると、APP は Xiaojin に 2 つのトークン、1 つの accessToken と 1 つのfreshToken を返します。accessToken の有効期限は比較的短く、refreshToken の時間は比較的長いです。accessToken の有効期限が切れると、refreshToken を使用して accessToken を再度取得するため、Xiaojin は気付かれることなくログイン状態を維持することができ、Xiaojin は自分が常にログインしていると誤解します。また、refreshToken は使用されるたびに更新され、更新ごとの RefreshToken は異なります。
**利点の説明: **シャオ ジンは、釣り中にボスに発見されない限り、ドラマを追いかける完全な体験をすることができます。accessToken の存在により、ログインの正常な検証が保証されます。accessToken の有効期限は比較的短いため、アカウントのセキュリティも保証されます。RefreshToken の存在により、Xiaojin はトークンの有効性を維持するために短期間に繰り返しログインする必要がなくなります。また、アクティブなユーザーのログイン状態を再ログインせずに継続できるようになります。繰り返しの更新により、一部のエラーが発生することも防止されます。悪意のある人は、refreshToken を取得した後にユーザー アカウントに対して不正な操作を実行します。
百聞は一見にしかず:
プロジェクトの準備
プロジェクトは Spring Boot 3 + Spring Security + JWT + MyBatis-Plus + Lombok で構築されています。
データベースを作成する
user 表
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: 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
プロジェクトの実現
エンティティ、コントローラー、サービス、マッパーなど、プロジェクトに必要な一連のコードを準備します。
システムの役割 役割
ロール (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;
}
}
ユーザーは UserDetails を実装します
親切なヒント:
Spring Security のソース コードは設計されているため、ユーザー名とパスワードの属性はユーザー名とパスワードとして定義されているため、ここで見るチュートリアルのほとんどはソース コードの方法に従い、ユーザー名は慣習的にユーザー名とパスワードとして定義されます。がパスワードとして定義されます。
実際、このルールに従う必要はありません。システムへのログインに電子メールを使用します。つまり、セキュリティのユーザー名として電子メールを使用します。その後、ユーザーが入力した電子メールをユーザー名として保存する必要があります。これにより、私のシステムの実際のユーザー名には別の単語が付けられるため、非常に不快に感じます。
ログインフィールドにユーザー名とパスワードを設定する必要があることを回避するにはどうすればよいですか?
システムにログインしているユーザー名とパスワードの属性が username とpassword ではない場合にのみ、ゲッター メソッドを書き換えます。書き換えると、下の赤いボックス内のプロンプトが表示されます。
ユーザー名とパスワードのゲッター メソッドをオーバーライドする
@Override
public String getUsername() {
return email;
}
@Override
public String getPassword() {
return password;
}
セキュリティ構成ファイル
WebSecurityConfigurerAdapter はSpring Security で非推奨となり削除されたことに注意してください。
新しい設定ファイルは以下に採用されます
@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 の依存関係
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.1.0</version>
</dependency>
OpenApiConfig 構成
OpenApi3はインターフェースドキュメントを生成します。主な構成は次のとおりです
- API グループ (グループ化)
- ベアラー認証
- 顧客 (カスタムリクエストヘッダーなど)
@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();
}
}
セキュリティインターフェイスの権限付与方法
hasRole と hasAuthority の違いは何ですか?
hasAuthority によって渡される ID は文字列とまったく同じである必要があり、hasRole によって渡される ID には prefix が必要です。同時に、2
ROLE_
種類の文字列を渡すことができますROLE_
。もう 1 つはプレフィックスなしです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 と fresh_token を返します。
プロジェクトのソースコードに素早くアクセス
プロジェクトのソースコードを無料で入手するには、以下の WeChat 公開アカウント [Jingcha Jilu] で JWT に返信してください