MaxKey 3 年之前
父节点
当前提交
0f912df258

+ 43 - 8
maxkey-authentications/maxkey-authentication-core/src/main/java/org/maxkey/authn/jwt/AuthJwt.java

@@ -25,14 +25,30 @@ import org.maxkey.authn.SignPrincipal;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.GrantedAuthority;
 
+import com.fasterxml.jackson.annotation.JsonProperty;
+
 public class AuthJwt implements Serializable {
 	
 	private static final long serialVersionUID = -914373258878811144L;
 	
+	public static final String ACCESS_TOKEN 	= "access_token";
+	
+	public static final String REFRESH_TOKEN 	= "refresh_token";
+	
+	public static final String EXPIRES_IN 		= "expired";
+	
 	private String ticket;
+	
+	private String type = "Bearer";
+	
 	private String token;
+	
+	@JsonProperty(REFRESH_TOKEN)
 	private String refreshToken;
-	private String type = "Bearer";
+	
+	@JsonProperty(EXPIRES_IN)
+	private int expiresIn;
+
 	private String remeberMe;
 	private String id;
 	private String name;
@@ -44,27 +60,36 @@ public class AuthJwt implements Serializable {
 	private int    passwordSetType;
 	private List<String> authorities;
 	  
-	  
-	public AuthJwt(String token, String id, String username, String displayName, String email, String instId,
-			String instName, List<String> authorities) {
+	public AuthJwt(String ticket, String type, String token, String refreshToken, int expiresIn, String remeberMe,
+			String id, String name, String username, String displayName, String email, String instId, String instName,
+			int passwordSetType, List<String> authorities) {
+		super();
+		this.ticket = ticket;
+		this.type = type;
 		this.token = token;
+		this.refreshToken = refreshToken;
+		this.expiresIn = expiresIn;
+		this.remeberMe = remeberMe;
 		this.id = id;
-		this.name = username;
+		this.name = name;
 		this.username = username;
 		this.displayName = displayName;
 		this.email = email;
 		this.instId = instId;
 		this.instName = instName;
+		this.passwordSetType = passwordSetType;
 		this.authorities = authorities;
 	}
-	
-	public AuthJwt(String token,String refreshToken, Authentication  authentication) {
+
+
+	public AuthJwt(String token, Authentication  authentication,int expiresIn,String refreshToken) {
 		SignPrincipal principal = ((SignPrincipal)authentication.getPrincipal());
 		
 		this.token = token;
+		this.expiresIn = expiresIn;
 		this.refreshToken = refreshToken;
-		this.ticket = principal.getSession().getId();
 		
+		this.ticket = principal.getSession().getId();
 		this.id = principal.getUserInfo().getId();
 		this.username = principal.getUserInfo().getUsername();
 		this.name = this.username;
@@ -175,6 +200,16 @@ public class AuthJwt implements Serializable {
 	public void setRefreshToken(String refreshToken) {
 		this.refreshToken = refreshToken;
 	}
+	
+	public int getExpiresIn() {
+		return expiresIn;
+	}
+
+
+	public void setExpiresIn(int expiresIn) {
+		this.expiresIn = expiresIn;
+	}
+
 
 	@Override
 	public String toString() {

+ 10 - 7
maxkey-authentications/maxkey-authentication-core/src/main/java/org/maxkey/authn/jwt/AuthJwtService.java

@@ -7,6 +7,7 @@ import org.joda.time.DateTime;
 import org.maxkey.authn.SignPrincipal;
 import org.maxkey.crypto.jwt.HMAC512Service;
 import org.maxkey.entity.UserInfo;
+import org.maxkey.util.StringUtils;
 import org.maxkey.web.WebContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -33,7 +34,7 @@ public class AuthJwtService {
 		DateTime currentDateTime = DateTime.now();
 		String subject = principal.getUsername();
 		Date expirationTime = currentDateTime.plusSeconds(expires).toDate();
-		_logger.debug("jwt subject : {} , expiration Time : {}" , subject,expirationTime);
+		_logger.trace("jwt subject : {} , expiration Time : {}" , subject,expirationTime);
 		
 		 JWTClaimsSet jwtClaims =new  JWTClaimsSet.Builder()
 				.issuer(issuer)
@@ -102,12 +103,14 @@ public class AuthJwtService {
 	 */
 	public boolean validateJwtToken(String authToken) {
 		try {
-			JWTClaimsSet claims = resolve(authToken);
-			boolean isExpiration = claims.getExpirationTime().after(DateTime.now().toDate());
-			boolean isVerify = hmac512Service.verify(authToken);
-			_logger.debug("JWT Verify {} , now {} , ExpirationTime {} , isExpiration : {}" , 
-							isVerify,DateTime.now().toDate(),claims.getExpirationTime(),isExpiration);
-			return isVerify && isExpiration;
+			if(StringUtils.isNotBlank(authToken)) {
+				JWTClaimsSet claims = resolve(authToken);
+				boolean isExpiration = claims.getExpirationTime().after(DateTime.now().toDate());
+				boolean isVerify = hmac512Service.verify(authToken);
+				_logger.trace("JWT Verify {} , now {} , ExpirationTime {} , isExpiration : {}" , 
+								isVerify,DateTime.now().toDate(),claims.getExpirationTime(),isExpiration);
+				return isVerify && isExpiration;
+			}
 		} catch (ParseException e) {
 			_logger.error("authToken {}",authToken);
 			_logger.error("ParseException ",e);

+ 14 - 4
maxkey-authentications/maxkey-authentication-core/src/main/java/org/maxkey/authn/jwt/AuthTokenService.java

@@ -66,13 +66,22 @@ public class AuthTokenService  extends AuthJwtService{
 	public AuthJwt genAuthJwt(Authentication authentication) {
 		if(authentication != null) {
 			String refreshToken = refreshTokenService.genRefreshToken(authentication);
-			return new AuthJwt(genJwt(authentication),refreshToken, authentication);
+			String accessToken = genJwt(authentication);
+			AuthJwt authJwt = new AuthJwt(
+						accessToken,
+						authentication,
+						authJwkConfig.getExpires(),
+						refreshToken);
+			return authJwt;
 		}
 		return null;
 	}
 	
 	public String genJwt(Authentication authentication) {
-		return genJwt( authentication,authJwkConfig.getIssuer(),authJwkConfig.getExpires());
+		return genJwt(
+					authentication,
+					authJwkConfig.getIssuer(),
+					authJwkConfig.getExpires());
 	}
 
 	
@@ -100,8 +109,9 @@ public class AuthTokenService  extends AuthJwtService{
 				congress, 
 				new AuthJwt(
 						genJwt(authentication), 
-						refreshToken,
-						authentication)
+						authentication,
+						authJwkConfig.getExpires(),
+						refreshToken)
 			);
 		return congress;
 	}

+ 23 - 8
maxkey-authentications/maxkey-authentication-core/src/main/java/org/maxkey/authn/web/AuthorizationUtils.java

@@ -37,18 +37,23 @@ import org.springframework.security.core.Authentication;
 public class AuthorizationUtils {
 	private static final Logger _logger = LoggerFactory.getLogger(AuthorizationUtils.class);
 	
-	public static final String Authorization_Cookie = "congress";
+	public static final class BEARERTYPE{
+		
+		public static final String CONGRESS 		= "congress";
+		
+		public static final String AUTHORIZATION 	= "Authorization";
+	}
 	
 	public static  void authenticateWithCookie(
 			HttpServletRequest request,
 			AuthTokenService authTokenService,
 			SessionManager sessionManager
 			) throws ParseException{
-		Cookie authCookie = WebContext.getCookie(request, Authorization_Cookie);
+		Cookie authCookie = WebContext.getCookie(request, BEARERTYPE.CONGRESS);
 		if(authCookie != null ) {
 	    	String  authorization =  authCookie.getValue();
-	    	doJwtAuthenticate(authorization,authTokenService,sessionManager);
-	    	_logger.debug("congress automatic authenticated .");
+	    	_logger.trace("Try congress authenticate .");
+	    	doJwtAuthenticate(BEARERTYPE.CONGRESS,authorization,authTokenService,sessionManager);
 		}
 	}
 	
@@ -59,13 +64,14 @@ public class AuthorizationUtils {
 			) throws ParseException{
 		String  authorization = AuthorizationHeaderUtils.resolveBearer(request);
 		if(authorization != null ) {
-			doJwtAuthenticate(authorization,authTokenService,sessionManager);
-			_logger.debug("Authorization automatic authenticated .");
+			_logger.trace("Try Authorization authenticate .");
+			doJwtAuthenticate(BEARERTYPE.AUTHORIZATION,authorization,authTokenService,sessionManager);
 		}
 		 
 	}
 	
 	public static void doJwtAuthenticate(
+			String  bearerType,
 			String  authorization,
 			AuthTokenService authTokenService,
 			SessionManager sessionManager) throws ParseException {
@@ -75,12 +81,17 @@ public class AuthorizationUtils {
 				Session session = sessionManager.get(sessionId);
 				if(session != null) {
 					setAuthentication(session.getAuthentication());
+					_logger.debug("{} Automatic authenticated .",bearerType);
 				}else {
-					setAuthentication(null);
+					//time out
+					_logger.debug("Session timeout .");
+					clearAuthentication();
 				}
 			}
 		}else {
-			setAuthentication(null);
+			//token invalidate
+			_logger.debug("Token invalidate .");
+			clearAuthentication();
 		}
 	}
 
@@ -100,6 +111,10 @@ public class AuthorizationUtils {
     	WebContext.setAttribute(WebConstants.AUTHENTICATION, authentication);
     }
 
+    public static void clearAuthentication() {
+    	WebContext.removeAttribute(WebConstants.AUTHENTICATION);
+    }
+    
     public static  boolean isAuthenticated() {
     	return getAuthentication() != null;
     }

+ 2 - 1
maxkey-authentications/maxkey-authentication-core/src/main/java/org/maxkey/authn/web/LoginRefreshPoint.java

@@ -33,6 +33,7 @@ public class LoginRefreshPoint {
  	@RequestMapping(value={"/token/refresh"}, produces = {MediaType.APPLICATION_JSON_VALUE})
 	public ResponseEntity<?> refresh(
 					@RequestHeader(name = "refresh_token", required = true) String refreshToken) {
+ 		_logger.debug("try to refresh token " );
  		_logger.trace("refresh token {} " , refreshToken);
  		try {
 	 		if(refreshTokenService.validateJwtToken(refreshToken)) {
@@ -47,7 +48,7 @@ public class LoginRefreshPoint {
 		 			_logger.debug("Session is timeout , sessionId [{}]" , sessionId);
 		 		}
 	 		}else {
-	 			_logger.trace("refresh token is not validate .");
+	 			_logger.debug("refresh token is not validate .");
 	 		}
  		}catch(Exception e) {
  			_logger.error("Refresh Exception !",e);

+ 6 - 2
maxkey-protocols/maxkey-protocol-oauth-2.0/src/main/java/org/maxkey/authz/oauth2/provider/approval/endpoint/OAuth20AccessConfirmationEndpoint.java

@@ -22,6 +22,7 @@ import java.util.LinkedHashMap;
 import java.util.Map;
 
 import org.maxkey.authn.annotation.CurrentUser;
+import org.maxkey.authn.jwt.AuthTokenService;
 import org.maxkey.authn.web.AuthorizationUtils;
 import org.maxkey.authz.oauth2.common.OAuth2Constants;
 import org.maxkey.authz.oauth2.provider.AuthorizationRequest;
@@ -81,6 +82,9 @@ public class OAuth20AccessConfirmationEndpoint {
     @Autowired 
     protected ApplicationConfig applicationConfig;
     
+    @Autowired
+	AuthTokenService authTokenService;
+    
     /**
      * getAccessConfirmation.
      * @param model  Map
@@ -95,7 +99,7 @@ public class OAuth20AccessConfirmationEndpoint {
 	        AuthorizationRequest clientAuth = 
 	        		(AuthorizationRequest) momentaryService.get(currentUser.getSessionId(), "authorizationRequest");
 	        ClientDetails client = clientDetailsService.loadClientByClientId(clientAuth.getClientId(),true);
-	        model.put("oauth_approval", WebContext.genId());
+	        model.put("oauth_approval", authTokenService.genRandomJwt());
 	        model.put("auth_request", clientAuth);
 	        model.put("client", client);
 	        model.put("oauth_version", "oauth 2.0");
@@ -136,7 +140,7 @@ public class OAuth20AccessConfirmationEndpoint {
     			@PathVariable("oauth_approval") String oauth_approval,
     			@CurrentUser UserInfo currentUser) {
     	Map<String, Object> model = new HashMap<String, Object>();
-    	if(StringUtils.isNotBlank(oauth_approval)) {
+    	if(authTokenService.validateJwtToken(oauth_approval)) {
 	    	try {
 		        AuthorizationRequest clientAuth = 
 		        		(AuthorizationRequest) momentaryService.get(currentUser.getSessionId(), "authorizationRequest");

+ 13 - 9
maxkey-web-frontend/maxkey-web-app/src/app/core/net/default.interceptor.ts

@@ -45,6 +45,7 @@ export class DefaultInterceptor implements HttpInterceptor {
   private refreshTokenEnabled = environment.api.refreshTokenEnabled;
   private refreshTokenType: 're-request' | 'auth-refresh' = environment.api.refreshTokenType;
   private refreshToking = false;
+  private notified = false;
   private refreshToken$: BehaviorSubject<any> = new BehaviorSubject<any>(null);
 
   constructor(private injector: Injector) {
@@ -70,7 +71,10 @@ export class DefaultInterceptor implements HttpInterceptor {
   }
 
   private goTo(url: string): void {
-    setTimeout(() => this.injector.get(Router).navigateByUrl(url));
+    setTimeout(() => {
+      this.injector.get(Router).navigateByUrl(url);
+      this.notified = false;
+    });
   }
 
   private checkStatus(ev: HttpResponseBase): void {
@@ -87,7 +91,7 @@ export class DefaultInterceptor implements HttpInterceptor {
    */
   private refreshTokenRequest(): Observable<any> {
     const model = this.tokenSrv.get();
-    return this.http.post(`/auth/token/refresh`, null, null, { headers: { refresh_token: model?.['refreshToken'] || '' } });
+    return this.http.post(`/auth/token/refresh`, null, null, { headers: { refresh_token: model?.['refresh_token'] || '' } });
   }
 
   // #region 刷新Token方式一:使用 401 重新刷新 Token
@@ -117,7 +121,7 @@ export class DefaultInterceptor implements HttpInterceptor {
         console.log(res.data);
         // 通知后续请求继续执行
         this.refreshToking = false;
-        this.refreshToken$.next(res.data.refreshToken);
+        this.refreshToken$.next(res.data.refresh_token);
         this.cookieService.set(CONSTS.CONGRESS, res.data.token);
         // 重新保存新 token
         this.tokenSrv.set(res.data);
@@ -181,8 +185,11 @@ export class DefaultInterceptor implements HttpInterceptor {
   // #endregion
 
   private toLogin(): void {
-    this.notification.error(`未登录或登录已过期,请重新登录。`, ``);
-    this.goTo(this.tokenSrv.login_url!);
+    if (!this.notified) {
+      this.notified = true;
+      this.notification.error(`未登录或登录已过期,请重新登录。`, ``);
+      this.goTo(this.tokenSrv.login_url!);
+    }
   }
 
   private handleData(ev: HttpResponseBase, req: HttpRequest<any>, next: HttpHandler): Observable<any> {
@@ -227,10 +234,7 @@ export class DefaultInterceptor implements HttpInterceptor {
         break;
       default:
         if (ev instanceof HttpErrorResponse) {
-          console.warn(
-            '未可知错误,大部分是由于后端不支持跨域CORS或无效配置引起,请参考 https://ng-alain.com/docs/server 解决跨域问题',
-            ev
-          );
+          console.warn('未可知错误,大部分是由于后端不支持跨域CORS或无效配置引起.', ev);
         }
         break;
     }

+ 8 - 1
maxkey-web-frontend/maxkey-web-app/src/app/layout/basic/basic.component.ts

@@ -78,7 +78,14 @@ import { LayoutDefaultOptions } from '../../theme/layout-default';
         <router-outlet></router-outlet>
       </ng-template>
     </layout-default>
-
+    <global-footer style="border-top: 1px solid #e5e5e5; min-height: 120px; text-shadow: 0 1px 0 #fff;margin:0;">
+      <div style="margin-top: 30px">
+        MaxKey v3.5.0 GA<br />
+        Copyright
+        <i nz-icon nzType="copyright"></i> 2022 <a href="//www.maxkey.top" target="_blank">http://www.maxkey.top</a><br />
+        Licensed under the Apache License, Version 2.0
+      </div>
+    </global-footer>
     <setting-drawer *ngIf="showSettingDrawer"></setting-drawer>
     <theme-btn></theme-btn>
   `

+ 1 - 1
maxkey-web-frontend/maxkey-web-app/src/app/service/authentication.service.ts

@@ -73,7 +73,7 @@ export class AuthenticationService {
     }
 
     this.cookieService.set(CONSTS.CONGRESS, authJwt.token);
-    this.cookieService.set(CONSTS.CONGRESS, authJwt.ticket, { domain: subHostName });
+    this.cookieService.set(CONSTS.ONLINE_TICKET, authJwt.ticket, { domain: subHostName });
     if (authJwt.remeberMe) {
       localStorage.setItem(CONSTS.REMEMBER, authJwt.remeberMe);
     }

+ 1 - 0
maxkey-web-frontend/maxkey-web-app/src/app/shared/consts.ts

@@ -1,5 +1,6 @@
 export const CONSTS = {
     CONGRESS: 'congress',
+    ONLINE_TICKET: 'online_ticket',
     REDIRECT_URI: 'redirect_uri',
     REMEMBER: 'remember_me'
 };

+ 30 - 10
maxkey-web-frontend/maxkey-web-mgt-app/src/app/core/net/default.interceptor.ts

@@ -13,9 +13,12 @@ import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
 import { ALAIN_I18N_TOKEN, _HttpClient } from '@delon/theme';
 import { environment } from '@env/environment';
 import { NzNotificationService } from 'ng-zorro-antd/notification';
+import { CookieService } from 'ngx-cookie-service';
 import { BehaviorSubject, Observable, of, throwError } from 'rxjs';
 import { catchError, filter, mergeMap, switchMap, take } from 'rxjs/operators';
 
+import { CONSTS } from '../../shared/consts';
+
 const CODEMESSAGE: { [key: number]: string } = {
   200: '服务器成功返回请求的数据。',
   201: '新建或修改数据成功。',
@@ -42,6 +45,7 @@ export class DefaultInterceptor implements HttpInterceptor {
   private refreshTokenEnabled = environment.api.refreshTokenEnabled;
   private refreshTokenType: 're-request' | 'auth-refresh' = environment.api.refreshTokenType;
   private refreshToking = false;
+  private notified = false;
   private refreshToken$: BehaviorSubject<any> = new BehaviorSubject<any>(null);
 
   constructor(private injector: Injector) {
@@ -54,6 +58,10 @@ export class DefaultInterceptor implements HttpInterceptor {
     return this.injector.get(NzNotificationService);
   }
 
+  private get cookieService(): CookieService {
+    return this.injector.get(CookieService);
+  }
+
   private get tokenSrv(): ITokenService {
     return this.injector.get(DA_SERVICE_TOKEN);
   }
@@ -63,7 +71,10 @@ export class DefaultInterceptor implements HttpInterceptor {
   }
 
   private goTo(url: string): void {
-    setTimeout(() => this.injector.get(Router).navigateByUrl(url));
+    setTimeout(() => {
+      this.injector.get(Router).navigateByUrl(url);
+      this.notified = false;
+    });
   }
 
   private checkStatus(ev: HttpResponseBase): void {
@@ -91,6 +102,7 @@ export class DefaultInterceptor implements HttpInterceptor {
       this.toLogin();
       return throwError(ev);
     }
+
     // 2、如果 `refreshToking` 为 `true` 表示已经在请求刷新 Token 中,后续所有请求转入等待状态,直至结果返回后再重新发起请求
     if (this.refreshToking) {
       return this.refreshToken$.pipe(
@@ -99,17 +111,20 @@ export class DefaultInterceptor implements HttpInterceptor {
         switchMap(() => next.handle(this.reAttachToken(req)))
       );
     }
+
     // 3、尝试调用刷新 Token
     this.refreshToking = true;
     this.refreshToken$.next(null);
 
     return this.refreshTokenRequest().pipe(
       switchMap(res => {
+        console.log(res.data);
         // 通知后续请求继续执行
         this.refreshToking = false;
-        this.refreshToken$.next(res);
+        this.refreshToken$.next(res.data.refresh_token);
+        this.cookieService.set(CONSTS.CONGRESS, res.data.token);
         // 重新保存新 token
-        this.tokenSrv.set(res);
+        this.tokenSrv.set(res.data);
         // 重新发起请求
         return next.handle(this.reAttachToken(req));
       }),
@@ -127,11 +142,14 @@ export class DefaultInterceptor implements HttpInterceptor {
    * > 由于已经发起的请求,不会再走一遍 `@delon/auth` 因此需要结合业务情况重新附加新的 Token
    */
   private reAttachToken(req: HttpRequest<any>): HttpRequest<any> {
+    //console.log('reAttachToken');
     // 以下示例是以 NG-ALAIN 默认使用 `SimpleInterceptor`
     const token = this.tokenSrv.get()?.token;
     return req.clone({
       setHeaders: {
-        Authorization: `Bearer ${token}`
+        Authorization: `Bearer ${token}`,
+        hostname: window.location.hostname,
+        AuthServer: 'MaxKey'
       }
     });
   }
@@ -167,8 +185,11 @@ export class DefaultInterceptor implements HttpInterceptor {
   // #endregion
 
   private toLogin(): void {
-    this.notification.error(`未登录或登录已过期,请重新登录。`, ``);
-    this.goTo(this.tokenSrv.login_url!);
+    if (!this.notified) {
+      this.notified = true;
+      this.notification.error(`未登录或登录已过期,请重新登录。`, ``);
+      this.goTo(this.tokenSrv.login_url!);
+    }
   }
 
   private handleData(ev: HttpResponseBase, req: HttpRequest<any>, next: HttpHandler): Observable<any> {
@@ -213,10 +234,7 @@ export class DefaultInterceptor implements HttpInterceptor {
         break;
       default:
         if (ev instanceof HttpErrorResponse) {
-          console.warn(
-            '未可知错误,大部分是由于后端不支持跨域CORS或无效配置引起,请参考 https://ng-alain.com/docs/server 解决跨域问题',
-            ev
-          );
+          console.warn('未可知错误,大部分是由于后端不支持跨域CORS或无效配置引起.', ev);
         }
         break;
     }
@@ -237,6 +255,8 @@ export class DefaultInterceptor implements HttpInterceptor {
     if (jwtAuthn !== null) {
       res['Authorization'] = `Bearer ${jwtAuthn.token}`;
     }
+    res['hostname'] = window.location.hostname;
+    res['AuthServer'] = 'MaxKey';
     return res;
   }
 

+ 1 - 1
maxkey-webs/maxkey-web-maxkey/src/main/resources/application-http.properties

@@ -55,7 +55,7 @@ maxkey.app.issuer                               =CN=ConSec,CN=COM,CN=SH
 maxkey.session.timeout                          =${SERVER_SESSION_TIMEOUT:1800}
 
 maxkey.auth.jwt.issuer                          =${maxkey.server.uri}
-maxkey.auth.jwt.expires                         =60
+maxkey.auth.jwt.expires                         =600
 maxkey.auth.jwt.secret                          =7heM-14BtxjyKPuH3ITIm7q2-ps5MuBirWCsrrdbzzSAOuSPrbQYiaJ54AeA0uH2XdkYy3hHAkTFIsieGkyqxOJZ_dQzrCbaYISH9rhUZAKYx8tUY0wkE4ArOC6LqHDJarR6UIcMsARakK9U4dhoOPO1cj74XytemI-w6ACYfzRUn_Rn4e-CQMcnD1C56oNEukwalf06xVgXl41h6K8IBEzLVod58y_VfvFn-NGWpNG0fy_Qxng6dg8Dgva2DobvzMN2eejHGLGB-x809MvC4zbG7CKNVlcrzMYDt2Gt2sOVDrt2l9YqJNfgaLFjrOEVw5cuXemGkX1MvHj6TAsbLg
 maxkey.auth.jwt.refresh.secret                  =7heM-14BtxjyKPuH3ITIm7q2-ps5MuBirWCsrrdbzzSAOuSPrbQYiaJ54AeA0uH2XdkYy3hHAkTFIsieGkyqxOJZ_dQzrCbaYISH9rhUZAKYx8tUY0wkE4ArOC6LqHDJarR6UIcMsARakK9U4dhoOPO1cj74XytemI-w6ACYfzRUn_Rn4e-CQMcnD1C56oNEukwalf06xVgXl41h6K8IBEzLVod58y_VfvFn-NGWpNG0fy_Qxng6dg8Dgva2DobvzMN2eejHGLGB-x809MvC4zbG7CKNVlcrzMYDt2Gt2sOVDrt2l9YqJNfgaLFjrOEVw5cuXemGkX1MvHj6TAsbLg
 ############################################################################