diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 8d89debb3..64946fce7 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -2,6 +2,18 @@ 更新日志文档,版本顺序从新到旧,最新版本在最前(上)面。 +# 0.20.6 + +## 新特性 + +- 优化SecurityProperties结构 +- token申请同时返回refreshToken +- 添加token刷新接口 + +## 问题修复 + +- Console的登录页申请令牌接口适配 + # 0.20.5 # 新特性 diff --git a/api/src/main/java/run/ikaros/api/infra/exception/security/InvalidTokenException.java b/api/src/main/java/run/ikaros/api/infra/exception/security/InvalidTokenException.java new file mode 100644 index 000000000..6689292e5 --- /dev/null +++ b/api/src/main/java/run/ikaros/api/infra/exception/security/InvalidTokenException.java @@ -0,0 +1,13 @@ +package run.ikaros.api.infra.exception.security; + +import org.springframework.security.core.AuthenticationException; + +public class InvalidTokenException extends AuthenticationException { + public InvalidTokenException(String msg) { + super(msg); + } + + public InvalidTokenException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/api/src/main/java/run/ikaros/api/infra/utils/StringUtils.java b/api/src/main/java/run/ikaros/api/infra/utils/StringUtils.java index 5e89b33d6..ea1a14283 100644 --- a/api/src/main/java/run/ikaros/api/infra/utils/StringUtils.java +++ b/api/src/main/java/run/ikaros/api/infra/utils/StringUtils.java @@ -1,6 +1,11 @@ package run.ikaros.api.infra.utils; +import java.security.SecureRandom; + public class StringUtils { + private static final String CHARACTERS + = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_-+=<>?"; + private static final SecureRandom RANDOM = new SecureRandom(); /** * upper str first char. @@ -45,4 +50,23 @@ public static String addLikeChar(String str) { return "%" + str + "%"; } + + /** + * 生成指定长度的字符串. + * + * @param length 字符串长度 + * @return 随机生成的字符串 + */ + public static String generateRandomStr(int length) { + if (length <= 0) { + throw new IllegalArgumentException("字符串长度必须大于 0"); + } + + StringBuilder sb = new StringBuilder(length); + for (int i = 0; i < length; i++) { + sb.append(CHARACTERS.charAt(RANDOM.nextInt(CHARACTERS.length()))); + } + return sb.toString(); + } + } diff --git a/console/packages/api-client/README.md b/console/packages/api-client/README.md index 208dfb68f..ea802b05d 100644 --- a/console/packages/api-client/README.md +++ b/console/packages/api-client/README.md @@ -58,7 +58,7 @@ npm publish 选择当前目录下的更改进行`git add .` ```bash -git commit -am "build: gen new api-client v20.1.0" +git commit -am "build: gen new api-client v20.6.0" ``` 合成版(powershell),升级 package.json 版本,并启动服务端后,在 api-client 路径下: diff --git a/console/packages/api-client/package.json b/console/packages/api-client/package.json index a7b0eee36..9db18d55c 100644 --- a/console/packages/api-client/package.json +++ b/console/packages/api-client/package.json @@ -1,6 +1,6 @@ { "name": "@runikaros/api-client", - "version": "20.1.0", + "version": "20.6.0", "description": "Project ikaros console api-client package", "type": "module", "scripts": { diff --git a/console/packages/api-client/src/.openapi-generator/FILES b/console/packages/api-client/src/.openapi-generator/FILES index 168e2e071..9d4864196 100644 --- a/console/packages/api-client/src/.openapi-generator/FILES +++ b/console/packages/api-client/src/.openapi-generator/FILES @@ -48,6 +48,7 @@ models/episode-resource.ts models/episode.ts models/index.ts models/jwt-apply-param.ts +models/jwt-apply-response.ts models/link.ts models/paging-wrap.ts models/plugin-load-location-file-system.ts diff --git a/console/packages/api-client/src/api/v1alpha1-indices-api.ts b/console/packages/api-client/src/api/v1alpha1-indices-api.ts index 9fc0ce612..ae459a164 100644 --- a/console/packages/api-client/src/api/v1alpha1-indices-api.ts +++ b/console/packages/api-client/src/api/v1alpha1-indices-api.ts @@ -97,16 +97,16 @@ export const V1alpha1IndicesApiAxiosParamCreator = function ( * Search subjects with fuzzy query * @param {string} keyword * @param {number} [limit] - * @param {string} [highlightPostTag] * @param {string} [highlightPreTag] + * @param {string} [highlightPostTag] * @param {*} [options] Override http request option. * @throws {RequiredError} */ searchSubject: async ( keyword: string, limit?: number, - highlightPostTag?: string, highlightPreTag?: string, + highlightPostTag?: string, options: AxiosRequestConfig = {} ): Promise => { // verify required parameter 'keyword' is not null or undefined @@ -139,10 +139,6 @@ export const V1alpha1IndicesApiAxiosParamCreator = function ( localVarQueryParameter["limit"] = limit; } - if (highlightPostTag !== undefined) { - localVarQueryParameter["highlightPostTag"] = highlightPostTag; - } - if (keyword !== undefined) { localVarQueryParameter["keyword"] = keyword; } @@ -151,6 +147,10 @@ export const V1alpha1IndicesApiAxiosParamCreator = function ( localVarQueryParameter["highlightPreTag"] = highlightPreTag; } + if (highlightPostTag !== undefined) { + localVarQueryParameter["highlightPostTag"] = highlightPostTag; + } + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; @@ -199,16 +199,16 @@ export const V1alpha1IndicesApiFp = function (configuration?: Configuration) { * Search subjects with fuzzy query * @param {string} keyword * @param {number} [limit] - * @param {string} [highlightPostTag] * @param {string} [highlightPreTag] + * @param {string} [highlightPostTag] * @param {*} [options] Override http request option. * @throws {RequiredError} */ async searchSubject( keyword: string, limit?: number, - highlightPostTag?: string, highlightPreTag?: string, + highlightPostTag?: string, options?: AxiosRequestConfig ): Promise< (axios?: AxiosInstance, basePath?: string) => AxiosPromise @@ -216,8 +216,8 @@ export const V1alpha1IndicesApiFp = function (configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.searchSubject( keyword, limit, - highlightPostTag, highlightPreTag, + highlightPostTag, options ); return createRequestFunction( @@ -265,8 +265,8 @@ export const V1alpha1IndicesApiFactory = function ( .searchSubject( requestParameters.keyword, requestParameters.limit, - requestParameters.highlightPostTag, requestParameters.highlightPreTag, + requestParameters.highlightPostTag, options ) .then((request) => request(axios, basePath)); @@ -299,14 +299,14 @@ export interface V1alpha1IndicesApiSearchSubjectRequest { * @type {string} * @memberof V1alpha1IndicesApiSearchSubject */ - readonly highlightPostTag?: string; + readonly highlightPreTag?: string; /** * * @type {string} * @memberof V1alpha1IndicesApiSearchSubject */ - readonly highlightPreTag?: string; + readonly highlightPostTag?: string; } /** @@ -343,8 +343,8 @@ export class V1alpha1IndicesApi extends BaseAPI { .searchSubject( requestParameters.keyword, requestParameters.limit, - requestParameters.highlightPostTag, requestParameters.highlightPreTag, + requestParameters.highlightPostTag, options ) .then((request) => request(this.axios, this.basePath)); diff --git a/console/packages/api-client/src/api/v1alpha1-security-api.ts b/console/packages/api-client/src/api/v1alpha1-security-api.ts index 6c7035620..3f8a8ba60 100644 --- a/console/packages/api-client/src/api/v1alpha1-security-api.ts +++ b/console/packages/api-client/src/api/v1alpha1-security-api.ts @@ -39,6 +39,8 @@ import { } from "../base"; // @ts-ignore import { JwtApplyParam } from "../models"; +// @ts-ignore +import { JwtApplyResponse } from "../models"; /** * V1alpha1SecurityApi - axios parameter creator * @export @@ -97,6 +99,61 @@ export const V1alpha1SecurityApiAxiosParamCreator = function ( configuration ); + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * Refresh access token with refresh token. + * @param {string} [body] Refresh token. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + refreshToken: async ( + body?: string, + options: AxiosRequestConfig = {} + ): Promise => { + const localVarPath = `/api/v1alpha1/security/auth/token/jwt/refresh`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { + method: "PUT", + ...baseOptions, + ...options, + }; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication BasicAuth required + // http basic authentication required + setBasicAuthToObject(localVarRequestOptions, configuration); + + // authentication BearerAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration); + + localVarHeaderParameter["Content-Type"] = "application/json"; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = + baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = { + ...localVarHeaderParameter, + ...headersFromBaseOptions, + ...options.headers, + }; + localVarRequestOptions.data = serializeDataIfNeeded( + body, + localVarRequestOptions, + configuration + ); + return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, @@ -123,7 +180,10 @@ export const V1alpha1SecurityApiFp = function (configuration?: Configuration) { jwtApplyParam?: JwtApplyParam, options?: AxiosRequestConfig ): Promise< - (axios?: AxiosInstance, basePath?: string) => AxiosPromise + ( + axios?: AxiosInstance, + basePath?: string + ) => AxiosPromise > { const localVarAxiosArgs = await localVarAxiosParamCreator.applyJwtToken( jwtApplyParam, @@ -136,6 +196,29 @@ export const V1alpha1SecurityApiFp = function (configuration?: Configuration) { configuration ); }, + /** + * Refresh access token with refresh token. + * @param {string} [body] Refresh token. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async refreshToken( + body?: string, + options?: AxiosRequestConfig + ): Promise< + (axios?: AxiosInstance, basePath?: string) => AxiosPromise + > { + const localVarAxiosArgs = await localVarAxiosParamCreator.refreshToken( + body, + options + ); + return createRequestFunction( + localVarAxiosArgs, + globalAxios, + BASE_PATH, + configuration + ); + }, }; }; @@ -159,11 +242,25 @@ export const V1alpha1SecurityApiFactory = function ( applyJwtToken( requestParameters: V1alpha1SecurityApiApplyJwtTokenRequest = {}, options?: AxiosRequestConfig - ): AxiosPromise { + ): AxiosPromise { return localVarFp .applyJwtToken(requestParameters.jwtApplyParam, options) .then((request) => request(axios, basePath)); }, + /** + * Refresh access token with refresh token. + * @param {V1alpha1SecurityApiRefreshTokenRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + refreshToken( + requestParameters: V1alpha1SecurityApiRefreshTokenRequest = {}, + options?: AxiosRequestConfig + ): AxiosPromise { + return localVarFp + .refreshToken(requestParameters.body, options) + .then((request) => request(axios, basePath)); + }, }; }; @@ -181,6 +278,20 @@ export interface V1alpha1SecurityApiApplyJwtTokenRequest { readonly jwtApplyParam?: JwtApplyParam; } +/** + * Request parameters for refreshToken operation in V1alpha1SecurityApi. + * @export + * @interface V1alpha1SecurityApiRefreshTokenRequest + */ +export interface V1alpha1SecurityApiRefreshTokenRequest { + /** + * Refresh token. + * @type {string} + * @memberof V1alpha1SecurityApiRefreshToken + */ + readonly body?: string; +} + /** * V1alpha1SecurityApi - object-oriented interface * @export @@ -203,4 +314,20 @@ export class V1alpha1SecurityApi extends BaseAPI { .applyJwtToken(requestParameters.jwtApplyParam, options) .then((request) => request(this.axios, this.basePath)); } + + /** + * Refresh access token with refresh token. + * @param {V1alpha1SecurityApiRefreshTokenRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof V1alpha1SecurityApi + */ + public refreshToken( + requestParameters: V1alpha1SecurityApiRefreshTokenRequest = {}, + options?: AxiosRequestConfig + ) { + return V1alpha1SecurityApiFp(this.configuration) + .refreshToken(requestParameters.body, options) + .then((request) => request(this.axios, this.basePath)); + } } diff --git a/console/packages/api-client/src/api/v1alpha1-subject-api.ts b/console/packages/api-client/src/api/v1alpha1-subject-api.ts index f6056d059..a38627e43 100644 --- a/console/packages/api-client/src/api/v1alpha1-subject-api.ts +++ b/console/packages/api-client/src/api/v1alpha1-subject-api.ts @@ -166,8 +166,10 @@ export const V1alpha1SubjectApiAxiosParamCreator = function ( * @param {string} [name] 经过Basic64编码的名称,名称字段模糊查询。 * @param {string} [nameCn] 经过Basic64编码的中文名称,中文名称字段模糊查询。 * @param {boolean} [nsfw] Not Safe/Suitable For Work. default is false. - * @param {'ANIME' | 'COMIC' | 'GAME' | 'MUSIC' | 'NOVEL' | 'REAL' | 'OTHER'} [type] 条目类型E + * @param {'ANIME' | 'COMIC' | 'GAME' | 'MUSIC' | 'NOVEL' | 'REAL' | 'OTHER'} [type] 条目类型 + * @param {string} [time] 时间范围,格式范围类型: 2000.9-2010.8 或者 单个类型2020.8 * @param {boolean} [airTimeDesc] 是否根据放送时间倒序,新番在列表前面。默认为 true. + * @param {boolean} [updateTimeDesc] 是否根据更新时间倒序,默认为 true. * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -178,7 +180,9 @@ export const V1alpha1SubjectApiAxiosParamCreator = function ( nameCn?: string, nsfw?: boolean, type?: "ANIME" | "COMIC" | "GAME" | "MUSIC" | "NOVEL" | "REAL" | "OTHER", + time?: string, airTimeDesc?: boolean, + updateTimeDesc?: boolean, options: AxiosRequestConfig = {} ): Promise => { const localVarPath = `/api/v1alpha1/subjects/condition`; @@ -229,10 +233,18 @@ export const V1alpha1SubjectApiAxiosParamCreator = function ( localVarQueryParameter["type"] = type; } + if (time !== undefined) { + localVarQueryParameter["time"] = time; + } + if (airTimeDesc !== undefined) { localVarQueryParameter["airTimeDesc"] = airTimeDesc; } + if (updateTimeDesc !== undefined) { + localVarQueryParameter["updateTimeDesc"] = updateTimeDesc; + } + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; @@ -475,8 +487,10 @@ export const V1alpha1SubjectApiFp = function (configuration?: Configuration) { * @param {string} [name] 经过Basic64编码的名称,名称字段模糊查询。 * @param {string} [nameCn] 经过Basic64编码的中文名称,中文名称字段模糊查询。 * @param {boolean} [nsfw] Not Safe/Suitable For Work. default is false. - * @param {'ANIME' | 'COMIC' | 'GAME' | 'MUSIC' | 'NOVEL' | 'REAL' | 'OTHER'} [type] 条目类型E + * @param {'ANIME' | 'COMIC' | 'GAME' | 'MUSIC' | 'NOVEL' | 'REAL' | 'OTHER'} [type] 条目类型 + * @param {string} [time] 时间范围,格式范围类型: 2000.9-2010.8 或者 单个类型2020.8 * @param {boolean} [airTimeDesc] 是否根据放送时间倒序,新番在列表前面。默认为 true. + * @param {boolean} [updateTimeDesc] 是否根据更新时间倒序,默认为 true. * @param {*} [options] Override http request option. * @throws {RequiredError} */ @@ -487,7 +501,9 @@ export const V1alpha1SubjectApiFp = function (configuration?: Configuration) { nameCn?: string, nsfw?: boolean, type?: "ANIME" | "COMIC" | "GAME" | "MUSIC" | "NOVEL" | "REAL" | "OTHER", + time?: string, airTimeDesc?: boolean, + updateTimeDesc?: boolean, options?: AxiosRequestConfig ): Promise< (axios?: AxiosInstance, basePath?: string) => AxiosPromise @@ -500,7 +516,9 @@ export const V1alpha1SubjectApiFp = function (configuration?: Configuration) { nameCn, nsfw, type, + time, airTimeDesc, + updateTimeDesc, options ); return createRequestFunction( @@ -641,7 +659,9 @@ export const V1alpha1SubjectApiFactory = function ( requestParameters.nameCn, requestParameters.nsfw, requestParameters.type, + requestParameters.time, requestParameters.airTimeDesc, + requestParameters.updateTimeDesc, options ) .then((request) => request(axios, basePath)); @@ -765,7 +785,7 @@ export interface V1alpha1SubjectApiListSubjectsByConditionRequest { readonly nsfw?: boolean; /** - * 条目类型E + * 条目类型 * @type {'ANIME' | 'COMIC' | 'GAME' | 'MUSIC' | 'NOVEL' | 'REAL' | 'OTHER'} * @memberof V1alpha1SubjectApiListSubjectsByCondition */ @@ -778,12 +798,26 @@ export interface V1alpha1SubjectApiListSubjectsByConditionRequest { | "REAL" | "OTHER"; + /** + * 时间范围,格式范围类型: 2000.9-2010.8 或者 单个类型2020.8 + * @type {string} + * @memberof V1alpha1SubjectApiListSubjectsByCondition + */ + readonly time?: string; + /** * 是否根据放送时间倒序,新番在列表前面。默认为 true. * @type {boolean} * @memberof V1alpha1SubjectApiListSubjectsByCondition */ readonly airTimeDesc?: boolean; + + /** + * 是否根据更新时间倒序,默认为 true. + * @type {boolean} + * @memberof V1alpha1SubjectApiListSubjectsByCondition + */ + readonly updateTimeDesc?: boolean; } /** @@ -893,7 +927,9 @@ export class V1alpha1SubjectApi extends BaseAPI { requestParameters.nameCn, requestParameters.nsfw, requestParameters.type, + requestParameters.time, requestParameters.airTimeDesc, + requestParameters.updateTimeDesc, options ) .then((request) => request(this.axios, this.basePath)); diff --git a/console/packages/api-client/src/models/index.ts b/console/packages/api-client/src/models/index.ts index 7f32fa357..21c10bae6 100644 --- a/console/packages/api-client/src/models/index.ts +++ b/console/packages/api-client/src/models/index.ts @@ -13,6 +13,7 @@ export * from "./episode-collection"; export * from "./episode-record"; export * from "./episode-resource"; export * from "./jwt-apply-param"; +export * from "./jwt-apply-response"; export * from "./link"; export * from "./paging-wrap"; export * from "./plugin"; diff --git a/console/packages/api-client/src/models/jwt-apply-response.ts b/console/packages/api-client/src/models/jwt-apply-response.ts new file mode 100644 index 000000000..b8bb82bd0 --- /dev/null +++ b/console/packages/api-client/src/models/jwt-apply-response.ts @@ -0,0 +1,39 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Ikaros Open API Documentation + * Documentation for Ikaros Open API + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/** + * + * @export + * @interface JwtApplyResponse + */ +export interface JwtApplyResponse { + /** + * 用户名 + * @type {string} + * @memberof JwtApplyResponse + */ + username?: string; + /** + * Access Token + * @type {string} + * @memberof JwtApplyResponse + */ + accessToken?: string; + /** + * Refresh Token + * @type {string} + * @memberof JwtApplyResponse + */ + refreshToken?: string; +} diff --git a/console/packages/api-client/src/models/paging-wrap.ts b/console/packages/api-client/src/models/paging-wrap.ts index 26cd71fd5..df967c879 100644 --- a/console/packages/api-client/src/models/paging-wrap.ts +++ b/console/packages/api-client/src/models/paging-wrap.ts @@ -43,17 +43,17 @@ export interface PagingWrap { */ items: Array; /** - * Indicates whether current page is the first page. + * Indicates whether current page is the last page. * @type {boolean} * @memberof PagingWrap */ - firstPage: boolean; + lastPage: boolean; /** - * Indicates whether current page is the last page. + * Indicates whether current page is the first page. * @type {boolean} * @memberof PagingWrap */ - lastPage: boolean; + firstPage: boolean; /** * Indicates whether current page has previous page. * @type {boolean} diff --git a/console/packages/api-client/src/models/subject-hint.ts b/console/packages/api-client/src/models/subject-hint.ts index c4e907242..266a9eba1 100644 --- a/console/packages/api-client/src/models/subject-hint.ts +++ b/console/packages/api-client/src/models/subject-hint.ts @@ -48,6 +48,12 @@ export interface SubjectHint { * @memberof SubjectHint */ summary?: string; + /** + * + * @type {string} + * @memberof SubjectHint + */ + cover?: string; /** * * @type {boolean} diff --git a/console/src/stores/user.ts b/console/src/stores/user.ts index d4516c47c..04fa88ad2 100644 --- a/console/src/stores/user.ts +++ b/console/src/stores/user.ts @@ -9,6 +9,7 @@ interface UserStoreState { currentUser?: User; isAnonymous: boolean; jwtToken?: string; + refreshToken?:string, } export const useUserStore = defineStore('user', { @@ -17,6 +18,7 @@ export const useUserStore = defineStore('user', { currentUser: undefined, isAnonymous: true, jwtToken: undefined, + refreshToken: undefined, }), actions: { async fetchCurrentUser() { @@ -49,10 +51,12 @@ export const useUserStore = defineStore('user', { }, }); if (status === 200) { - this.jwtToken = data; + this.jwtToken = data.accessToken; + this.refreshToken = data.refreshToken; this.isAnonymous = false; } else { this.jwtToken = undefined; + this.refreshToken = undefined; this.isAnonymous = true; } } catch (e) { @@ -62,6 +66,7 @@ export const useUserStore = defineStore('user', { }, jwtTokenLogout() { this.jwtToken = undefined; + this.refreshToken = undefined; this.isAnonymous = true; this.currentUser = undefined; }, diff --git a/gradle.properties b/gradle.properties index 64b6ac7e7..0661e5b10 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=0.20.5 +version=0.20.6 diff --git a/server/src/main/java/run/ikaros/server/security/SecurityEndpoint.java b/server/src/main/java/run/ikaros/server/security/SecurityEndpoint.java index 2235cebfc..37805499a 100644 --- a/server/src/main/java/run/ikaros/server/security/SecurityEndpoint.java +++ b/server/src/main/java/run/ikaros/server/security/SecurityEndpoint.java @@ -20,6 +20,7 @@ import run.ikaros.api.infra.exception.user.UserNotFoundException; import run.ikaros.server.endpoint.CoreEndpoint; import run.ikaros.server.security.authentication.jwt.JwtApplyParam; +import run.ikaros.server.security.authentication.jwt.JwtApplyResponse; import run.ikaros.server.security.authentication.jwt.JwtAuthenticationProvider; import run.ikaros.server.security.authentication.jwt.JwtReactiveAuthenticationManager; @@ -53,8 +54,16 @@ public RouterFunction endpoint() { .implementation(JwtApplyParam.class) .description("Apply JWT token params")) .response(responseBuilder() - .implementation(String.class) - .description("Token")) + .implementation(JwtApplyResponse.class) + .description("Jwt token response.")) + ) + .PUT("/security/auth/token/jwt/refresh", this::refreshToken, + builder -> builder.operationId("RefreshToken") + .tag(tag).description("Refresh access token with refresh token.") + .requestBody(requestBodyBuilder().implementation(String.class) + .description("Refresh token.")) + .response(responseBuilder().implementation(String.class) + .description("New access token.")) ) .build(); } @@ -71,10 +80,15 @@ private Mono applyJwtToken(ServerRequest request) { .map(UserDetails::getUsername) .map(String::valueOf) .flatMap(userDetailsService::findByUsername) - .map(UserDetails::getUsername) - .map(jwtAuthenticationProvider::generateToken) + .flatMap(jwtAuthenticationProvider::generateJwtResp) .flatMap(token -> ServerResponse.ok().bodyValue(token)) .onErrorResume(UserNotFoundException.class, e -> Mono.error(new UserAuthenticationException(e.getLocalizedMessage(), e))); } + + private Mono refreshToken(ServerRequest request) { + return request.bodyToMono(String.class) + .flatMap(jwtAuthenticationProvider::refreshToken) + .flatMap(accessToken -> ServerResponse.ok().bodyValue(accessToken)); + } } diff --git a/server/src/main/java/run/ikaros/server/security/SecurityProperties.java b/server/src/main/java/run/ikaros/server/security/SecurityProperties.java index d4348c540..63771e2be 100644 --- a/server/src/main/java/run/ikaros/server/security/SecurityProperties.java +++ b/server/src/main/java/run/ikaros/server/security/SecurityProperties.java @@ -10,14 +10,13 @@ public class SecurityProperties { private final Initializer initializer = new Initializer(); - private String jwtSecretKey = """ - 平和な日常ってやつは、一瞬にして壊されるんだ! - 自分がどうしたいか、自分がどうなりたいか、それが一番大事なんだ! - 私はあなたのエンジェロイド。あなたの命令をなんでも聞きます。 - どんなに辛いことがあっても、笑顔で乗り越えるのが俺の流儀だ! - 君と出会えたこと、それが私の奇跡です。 - """; - private Long jwtExpirationTime = (long) (3 * 24 * 60 * 60 * 1000); // 3day + private final Expiry expiry = new Expiry(); + + @Data + public static class Expiry { + private Integer accessTokenDay = 3; + private Integer refreshTokenMonth = 3; + } @Data public static class Initializer { diff --git a/server/src/main/java/run/ikaros/server/security/authentication/jwt/JwtApplyResponse.java b/server/src/main/java/run/ikaros/server/security/authentication/jwt/JwtApplyResponse.java new file mode 100644 index 000000000..1b92e6ae1 --- /dev/null +++ b/server/src/main/java/run/ikaros/server/security/authentication/jwt/JwtApplyResponse.java @@ -0,0 +1,24 @@ +package run.ikaros.server.security.authentication.jwt; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +public class JwtApplyResponse { + @Schema(description = "用户名") + private String username; + @Schema(description = "Access Token") + private String accessToken; + @Schema(description = "Refresh Token") + private String refreshToken; +} + + diff --git a/server/src/main/java/run/ikaros/server/security/authentication/jwt/JwtAuthenticationProvider.java b/server/src/main/java/run/ikaros/server/security/authentication/jwt/JwtAuthenticationProvider.java index f234f3e37..d919721c0 100644 --- a/server/src/main/java/run/ikaros/server/security/authentication/jwt/JwtAuthenticationProvider.java +++ b/server/src/main/java/run/ikaros/server/security/authentication/jwt/JwtAuthenticationProvider.java @@ -7,7 +7,11 @@ import java.util.Base64; import java.util.Date; import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; +import run.ikaros.api.infra.exception.security.InvalidTokenException; +import run.ikaros.api.infra.utils.StringUtils; import run.ikaros.server.security.SecurityProperties; @Slf4j @@ -15,7 +19,8 @@ public class JwtAuthenticationProvider { private final SecurityProperties securityProperties; private final String secretKey; - private final long expirationTime; // 1 d + private Long accessTokenExpiry = 0L; + private Long refreshTokenExpiry = 0L; /** * Construct instance. @@ -23,17 +28,48 @@ public class JwtAuthenticationProvider { public JwtAuthenticationProvider(SecurityProperties securityProperties) { this.securityProperties = securityProperties; secretKey = Base64.getEncoder().encodeToString( - securityProperties.getJwtSecretKey().getBytes(StandardCharsets.UTF_8)); - expirationTime = securityProperties.getJwtExpirationTime(); + StringUtils.generateRandomStr(512).getBytes(StandardCharsets.UTF_8)); + } + + /** + * generateToken and convert to {@link JwtApplyResponse}. + */ + public Mono generateJwtResp(UserDetails userDetails) { + String username = userDetails.getUsername(); + SecurityProperties.Expiry expiry = securityProperties.getExpiry(); + Integer accessTokenDay = expiry.getAccessTokenDay(); + Integer refreshTokenMonth = expiry.getRefreshTokenMonth(); + int dayOfMs = 24 * 60 * 60 * 1000; + accessTokenExpiry = (long) (accessTokenDay * dayOfMs); + refreshTokenExpiry = (long) refreshTokenMonth * dayOfMs * 30; + String accessToken = generateToken(username, accessTokenExpiry); + String refreshToken = generateToken(username, refreshTokenExpiry); + return Mono.just(JwtApplyResponse.builder() + .username(username) + .accessToken(accessToken) + .refreshToken(refreshToken) + .build()); + } + + /** + * 刷新token. + * + * @return 新的accessToken + */ + public Mono refreshToken(String refreshToken) { + if (!validateToken(refreshToken)) { + return Mono.error(new InvalidTokenException("Invalid token")); + } + return Mono.just(generateToken(extractUsername(refreshToken), accessTokenExpiry)); } /** * generateToken. */ - public String generateToken(String username) { + private String generateToken(String username, long milliseconds) { return Jwts.builder() .setSubject(username) - .setExpiration(new Date(System.currentTimeMillis() + expirationTime)) + .setExpiration(new Date(System.currentTimeMillis() + milliseconds)) .signWith(SignatureAlgorithm.HS512, secretKey) .compact(); } diff --git a/server/src/main/java/run/ikaros/server/security/authorization/RequestAuthorizationManager.java b/server/src/main/java/run/ikaros/server/security/authorization/RequestAuthorizationManager.java index 5e673d616..945b427eb 100644 --- a/server/src/main/java/run/ikaros/server/security/authorization/RequestAuthorizationManager.java +++ b/server/src/main/java/run/ikaros/server/security/authorization/RequestAuthorizationManager.java @@ -26,13 +26,10 @@ public Mono check(Mono authentication, final ServerHttpRequest request = object.getExchange().getRequest(); final String path = request.getURI().getPath(); final HttpMethod method = request.getMethod(); - boolean urlStartWithApiStatic = path - .startsWith("/api/" + CORE_VERSION + "/static/"); - if (urlStartWithApiStatic) { - return authentication.map(auth -> new AuthorizationDecision(true)); - } - - if (path.equals("/api/" + CORE_VERSION + "/security/auth/token/jwt/apply")) { + if (path.startsWith("/api/" + CORE_VERSION + "/static/") + || path.equals("/api/" + CORE_VERSION + "/security/auth/token/jwt/apply") + || path.equals("/api/" + CORE_VERSION + "/security/auth/token/jwt/refresh") + ) { return authentication.map(auth -> new AuthorizationDecision(true)); } diff --git a/server/src/main/resources/application.yaml b/server/src/main/resources/application.yaml index bb8d3f4de..d8ba15f75 100644 --- a/server/src/main/resources/application.yaml +++ b/server/src/main/resources/application.yaml @@ -13,8 +13,9 @@ ikaros: timeout: 10000 security: - # 3 day for token expire - jwt-expiration-time: 259200000 + expiry: + access-token-day: 3 + refresh-token-month: 3 plugin: runtime-mode: deployment