diff --git a/common/src/main/java/com/usthe/common/util/CommonConstants.java b/common/src/main/java/com/usthe/common/util/CommonConstants.java index 92b7337..4fd7b49 100644 --- a/common/src/main/java/com/usthe/common/util/CommonConstants.java +++ b/common/src/main/java/com/usthe/common/util/CommonConstants.java @@ -32,6 +32,11 @@ public interface CommonConstants { */ byte MONITOR_CONFLICT = 0x04; + /** + * 响应状态码: 登陆账户密码错误 + */ + byte MONITOR_LOGIN_FAILED = 0x05; + /** * 监控状态码: 未管理 */ diff --git a/manager/pom.xml b/manager/pom.xml index e3a9f3d..0a1024d 100644 --- a/manager/pom.xml +++ b/manager/pom.xml @@ -14,6 +14,7 @@ 8.0.16 1.26 + 1.0.5 @@ -92,6 +93,12 @@ org.springframework.boot spring-boot-starter-validation + + + com.usthe.sureness + spring-boot-starter-sureness + 1.0.0-beta.2 + diff --git a/manager/src/main/java/com/usthe/manager/controller/AccountController.java b/manager/src/main/java/com/usthe/manager/controller/AccountController.java new file mode 100644 index 0000000..03f842a --- /dev/null +++ b/manager/src/main/java/com/usthe/manager/controller/AccountController.java @@ -0,0 +1,118 @@ +package com.usthe.manager.controller; + +import com.usthe.common.entity.dto.Message; +import com.usthe.sureness.provider.SurenessAccount; +import com.usthe.sureness.provider.SurenessAccountProvider; +import com.usthe.sureness.provider.ducument.DocumentAccountProvider; +import com.usthe.sureness.subject.SubjectSum; +import com.usthe.sureness.util.JsonWebTokenUtil; +import com.usthe.sureness.util.Md5Util; +import com.usthe.sureness.util.SurenessContextHolder; +import io.swagger.annotations.Api; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static com.usthe.common.util.CommonConstants.MONITOR_LOGIN_FAILED; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +/** + * 认证注册TOKEN管理API + * @author tomsun28 + * @date 13:11 2019-05-26 + */ +@Api(tags = "认证注册TOKEN管理API") +@RestController() +@RequestMapping(value = "/account/auth", produces = {APPLICATION_JSON_VALUE}) +public class AccountController { + + /** + * account data provider + */ + private SurenessAccountProvider accountProvider = new DocumentAccountProvider(); + + /** + * 账户密码登陆获取token + * @param requestBody request + * @return token与refresh token + * + */ + @PostMapping("/form") + public ResponseEntity authGetToken(@RequestBody Map requestBody) { + + String identifier = requestBody.get("identifier"); + String password = requestBody.get("password"); + SurenessAccount account = accountProvider.loadAccount(identifier); + if (account == null || account.getPassword() == null) { + Message message = Message.builder().msg("账户密码错误") + .code(MONITOR_LOGIN_FAILED).build(); + return ResponseEntity.ok(message); + } else { + if (account.getSalt() != null) { + password = Md5Util.md5(password + account.getSalt()); + } + if (!account.getPassword().equals(password)) { + Message message = Message.builder().msg("账户密码错误") + .code(MONITOR_LOGIN_FAILED).build(); + return ResponseEntity.ok(message); + } + if (account.isDisabledAccount() || account.isExcessiveAttempts()) { + Message message = Message.builder().msg("账户过期或被锁定") + .code(MONITOR_LOGIN_FAILED).build(); + return ResponseEntity.ok(message); + } + } + // Get the roles the user has - rbac + List roles = account.getOwnRoles(); + long periodTime = 3600L; + // issue jwt + String jwt = JsonWebTokenUtil.issueJwt(UUID.randomUUID().toString(), identifier, + "token-server", periodTime, roles); + // issue refresh jwt + String refreshJwt = JsonWebTokenUtil.issueJwt(UUID.randomUUID().toString(), identifier, + "token-server-refresh", periodTime, roles); + Map resp = new HashMap<>(2); + resp.put("token", jwt); + resp.put("refreshToken", refreshJwt); + return ResponseEntity.ok().body(new Message(resp)); + } + + /** + * 账户密码登陆获取token + * @param requestBody request + * @return token与refresh token + * + */ + @PostMapping("/refresh") + public ResponseEntity refreshToken(@RequestBody Map requestBody) { + + SubjectSum subjectSum = SurenessContextHolder.getBindSubject(); + if (subjectSum == null) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + } + String identifier = String.valueOf(subjectSum.getPrincipal()); + + // Get the roles the user has - rbac + List roles = (List) subjectSum.getRoles(); + long periodTime = 3600L; + // issue jwt + String jwt = JsonWebTokenUtil.issueJwt(UUID.randomUUID().toString(), identifier, + "token-server", periodTime, roles); + // issue refresh jwt + String refreshJwt = JsonWebTokenUtil.issueJwt(UUID.randomUUID().toString(), identifier, + "token-server-refresh", periodTime, roles); + Map resp = new HashMap<>(2); + resp.put("token", jwt); + resp.put("refreshToken", refreshJwt); + return ResponseEntity.ok().body(new Message<>(resp)); + } + +} diff --git a/manager/src/main/resources/sureness.yml b/manager/src/main/resources/sureness.yml new file mode 100644 index 0000000..e37518d --- /dev/null +++ b/manager/src/main/resources/sureness.yml @@ -0,0 +1,44 @@ +## -- sureness.yml document dataSource-- ## + +# load api resource which need be protected, config role who can access these resource. +# resources that are not configured are also authenticated and protected by default, but not authorized +# eg: /api/v2/host===post===[role2,role3,role4] means /api/v2/host===post can be access by role2,role3,role4 +# eg: /api/v1/getSource3===get===[] means /api/v1/getSource3===get can not be access by any role +resourceRole: + - /account/auth/refresh===post===[role1,role2,role3,role4] + +# load api resource which do not need be protected, means them need be excluded. +# these api resource can be access by everyone +excludedResource: + - /account/auth/form===post + - /**/*.html===get + - /**/*.js===get + - /**/*.css===get + - /**/*.ico===get + - /**/*.ttf===get + - /**/*.png===get + - /**/*.gif===get + - /swagger-resources/**===get + - /v2/api-docs===get + - /v3/api-docs===get + - /**/*.png===* + +# account info +# there are three account: admin, root, tom +# eg: admin has [role1,role2] ROLE, unencrypted password is admin, encrypted password is 0192023A7BBD73250516F069DF18B500 +# eg: root has role1, unencrypted password is 23456 +# eg: tom has role3, unencrypted password is 32113 +account: + - appId: admin + credential: admin + role: [role1,role2] + - appId: tom + credential: tom@123 + role: [role1,role2,role3] + - appId: lili + # 注意 Digest认证不支持加盐加密的密码账户 + # 加盐加密的密码,通过 MD5(password+salt)计算 + # 此账户的原始密码为 lili + credential: 1A676730B0C7F54654B0E09184448289 + salt: 123 + role: [role1,role2] diff --git a/web-app/_mock/_user.ts b/web-app/_mock/_user.ts index ccb2efe..67575ed 100644 --- a/web-app/_mock/_user.ts +++ b/web-app/_mock/_user.ts @@ -102,8 +102,8 @@ export const USERS = { 'POST /user/avatar': 'ok', 'POST /login/account': (req: MockRequest) => { const data = req.body; - if (!(data.userName === 'admin' || data.userName === 'user') || data.password !== 'admin@123') { - return { msg: `Invalid username or password(admin/admin@123)` }; + if (!(data.userName === 'admin' || data.userName === 'user') || data.password !== 'admin') { + return { msg: `Invalid username or password(admin/admin)` }; } return { msg: 'ok', diff --git a/web-app/src/app/core/interceptor/default.interceptor.ts b/web-app/src/app/core/interceptor/default.interceptor.ts index 2bcfcac..b9f7b7b 100644 --- a/web-app/src/app/core/interceptor/default.interceptor.ts +++ b/web-app/src/app/core/interceptor/default.interceptor.ts @@ -9,7 +9,6 @@ import { } from '@angular/common/http'; import { Injectable, Injector } from '@angular/core'; import { Router } from '@angular/router'; -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'; @@ -51,10 +50,6 @@ export class DefaultInterceptor implements HttpInterceptor { return this.injector.get(NzNotificationService); } - private get tokenSrv(): ITokenService { - return this.injector.get(DA_SERVICE_TOKEN); - } - private get http(): _HttpClient { return this.injector.get(_HttpClient); } @@ -75,15 +70,16 @@ export class DefaultInterceptor implements HttpInterceptor { * 刷新 Token 请求 */ private refreshTokenRequest(): Observable { - const model = this.tokenSrv.get(); - return this.http.post(`/api/auth/refresh`, null, null, { headers: { refresh_token: model?.refresh_token || '' } }); + const refreshToken = this.storageSvc.getRefreshToken(); + return this.http.post(`/account/auth/refresh`, null, null, + { headers: { Authorization: `Bearer ${refreshToken}` }}); } // #region 刷新Token方式一:使用 401 重新刷新 Token private tryRefreshToken(ev: HttpResponseBase, req: HttpRequest, next: HttpHandler): Observable { // 1、若请求为刷新Token请求,表示来自刷新Token可以直接跳转登录页 - if ([`/api/auth/refresh`].some(url => req.url.includes(url))) { + if ([`/account/auth/refresh`].some(url => req.url.includes(url))) { this.toLogin(); return throwError(ev); } @@ -105,8 +101,10 @@ export class DefaultInterceptor implements HttpInterceptor { this.refreshToking = false; this.refreshToken$.next(res); // 重新保存新 token - this.storageSvc.storageAuthorizationToken(res); - this.tokenSrv.set(res); + let token = res.token; + let refreshToken = res.refreshToken; + this.storageSvc.storageAuthorizationToken(token); + this.storageSvc.storageRefreshToken(refreshToken); // 重新发起请求 return next.handle(this.reAttachToken(req)); }), @@ -134,7 +132,7 @@ export class DefaultInterceptor implements HttpInterceptor { private toLogin(): void { this.notification.error(`未登录或登录已过期,请重新登录。`, ``); - this.goTo(this.tokenSrv.login_url!); + this.goTo('/passport/login'); } private fillHeaders(headers?: HttpHeaders): { [name: string]: string } { diff --git a/web-app/src/app/routes/passport/login/login.component.html b/web-app/src/app/routes/passport/login/login.component.html index f6550c8..478521f 100644 --- a/web-app/src/app/routes/passport/login/login.component.html +++ b/web-app/src/app/routes/passport/login/login.component.html @@ -12,7 +12,7 @@ - + diff --git a/web-app/src/app/routes/passport/login/login.component.ts b/web-app/src/app/routes/passport/login/login.component.ts index 8a62270..490d2b1 100644 --- a/web-app/src/app/routes/passport/login/login.component.ts +++ b/web-app/src/app/routes/passport/login/login.component.ts @@ -8,6 +8,8 @@ import { SettingsService, _HttpClient } from '@delon/theme'; import { environment } from '@env/environment'; import { NzTabChangeEvent } from 'ng-zorro-antd/tabs'; import { finalize } from 'rxjs/operators'; +import {Message} from "../../../pojo/Message"; +import {LocalStorageService} from "../../../service/local-storage.service"; @Component({ selector: 'passport-login', @@ -28,11 +30,12 @@ export class UserLoginComponent implements OnDestroy { @Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService, private startupSrv: StartupService, private http: _HttpClient, - private cdr: ChangeDetectorRef + private cdr: ChangeDetectorRef, + private storageSvc: LocalStorageService ) { this.form = fb.group({ - userName: [null, [Validators.required, Validators.pattern(/^(admin|user)$/)]], - password: [null, [Validators.required, Validators.pattern(/^(admin@123)$/)]], + userName: [null, [Validators.required]], + password: [null, [Validators.required]], mobile: [null, [Validators.required, Validators.pattern(/^1\d{10}$/)]], captcha: [null, [Validators.required]], remember: [true] @@ -111,29 +114,28 @@ export class UserLoginComponent implements OnDestroy { this.loading = true; this.cdr.detectChanges(); this.http - .post('/login/account?_allow_anonymous=true', { + .post>('/account/auth/form', { type: this.type, - userName: this.userName.value, + identifier: this.userName.value, password: this.password.value }) .pipe( finalize(() => { - this.loading = true; + this.loading = false; this.cdr.detectChanges(); }) ) - .subscribe(res => { - if (res.msg !== 'ok') { - this.error = res.msg; + .subscribe(message => { + if (message.code !== 0) { + this.error = message.msg; this.cdr.detectChanges(); return; } // 清空路由复用信息 this.reuseTabService.clear(); // 设置用户Token信息 - // TODO: Mock expired value - res.user.expired = +new Date() + 1000 * 60 * 5; - this.tokenService.set(res.user); + this.storageSvc.storageAuthorizationToken(message.data.token); + this.storageSvc.storageRefreshToken(message.data.refreshToken); // 重新获取 StartupService 内容,我们始终认为应用信息一般都会受当前用户授权范围而影响 this.startupSrv.load().subscribe(() => { let url = this.tokenService.referrer!.url || '/'; diff --git a/web-app/src/app/routes/routes-routing.module.ts b/web-app/src/app/routes/routes-routing.module.ts index 1a305c7..6c15e87 100644 --- a/web-app/src/app/routes/routes-routing.module.ts +++ b/web-app/src/app/routes/routes-routing.module.ts @@ -19,7 +19,8 @@ const routes: Routes = [ { path: '', component: LayoutBasicComponent, - canActivate: [SimpleGuard], + // 路由守卫 在路由之前判断是否有认证或者权限进入此路由 + // canActivate: [SimpleGuard], children: [ // todo 根据路由自动生成面包屑 { path: '', redirectTo: 'dashboard', pathMatch: 'full'}, diff --git a/web-app/src/app/service/local-storage.service.ts b/web-app/src/app/service/local-storage.service.ts index 3f49e05..811ec40 100644 --- a/web-app/src/app/service/local-storage.service.ts +++ b/web-app/src/app/service/local-storage.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core'; const Authorization = 'Authorization'; +const refreshToken = 'refresh-token'; @Injectable({ providedIn: 'root' @@ -22,6 +23,14 @@ export class LocalStorageService { return this.getData(Authorization); } + public getRefreshToken(): string | null { + return this.getData(refreshToken); + } + + public storageRefreshToken(token: string) { + return this.putData(refreshToken, token); + } + public storageAuthorizationToken(token: string) { return this.putData(Authorization, token); }