diff --git a/manager/pom.xml b/manager/pom.xml index ecca547..98e172e 100644 --- a/manager/pom.xml +++ b/manager/pom.xml @@ -108,7 +108,7 @@ com.usthe.sureness spring-boot-starter-sureness - 1.0.6 + 1.0.6.beta1 diff --git a/manager/src/main/java/com/usthe/manager/controller/AccountController.java b/manager/src/main/java/com/usthe/manager/controller/AccountController.java index 6a3578a..a8284d6 100644 --- a/manager/src/main/java/com/usthe/manager/controller/AccountController.java +++ b/manager/src/main/java/com/usthe/manager/controller/AccountController.java @@ -1,25 +1,29 @@ package com.usthe.manager.controller; import com.usthe.common.entity.dto.Message; +import com.usthe.manager.pojo.dto.LoginDto; 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.jsonwebtoken.Claims; import io.swagger.annotations.Api; -import org.springframework.http.HttpStatus; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; 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 javax.validation.constraints.NotNull; 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_CODE; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; @@ -32,87 +36,95 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; @Api(tags = "认证注册TOKEN管理API") @RestController() @RequestMapping(value = "/account/auth", produces = {APPLICATION_JSON_VALUE}) +@Slf4j public class AccountController { + /** + * TOKEN有效期时间 单位秒 + */ + private static final long PERIOD_TIME = 3600L; + /** * account data provider */ private SurenessAccountProvider accountProvider = new DocumentAccountProvider(); - /** - * 账户密码登陆获取token - * @param requestBody request - * @return token与refresh token - * - */ @PostMapping("/form") - public ResponseEntity authGetToken(@RequestBody Map requestBody) { + @ApiOperation(value = "账户登陆", notes = "账户密码登陆获取关联用户信息") + public ResponseEntity>> authGetToken(@RequestBody LoginDto loginDto) { - String identifier = requestBody.get("identifier"); - String password = requestBody.get("password"); - SurenessAccount account = accountProvider.loadAccount(identifier); + SurenessAccount account = accountProvider.loadAccount(loginDto.getIdentifier()); if (account == null || account.getPassword() == null) { - Message message = Message.builder().msg("账户密码错误") + Message> message = Message.>builder().msg("账户密码错误") .code(MONITOR_LOGIN_FAILED_CODE).build(); return ResponseEntity.ok(message); } else { + String password = loginDto.getCredential(); if (account.getSalt() != null) { password = Md5Util.md5(password + account.getSalt()); } if (!account.getPassword().equals(password)) { - Message message = Message.builder().msg("账户密码错误") + Message> message = Message.>builder().msg("账户密码错误") .code(MONITOR_LOGIN_FAILED_CODE).build(); return ResponseEntity.ok(message); } if (account.isDisabledAccount() || account.isExcessiveAttempts()) { - Message message = Message.builder().msg("账户过期或被锁定") + Message> message = Message.>builder().msg("账户过期或被锁定") .code(MONITOR_LOGIN_FAILED_CODE).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); + // 签发TOKEN + String issueToken = JsonWebTokenUtil.issueJwt(loginDto.getIdentifier(), PERIOD_TIME, roles); + Map customClaimMap = new HashMap<>(1); + customClaimMap.put("refresh", true); + String issueRefresh = JsonWebTokenUtil.issueJwt(loginDto.getIdentifier(), PERIOD_TIME << 5, customClaimMap); Map resp = new HashMap<>(2); - resp.put("token", jwt); - resp.put("refreshToken", refreshJwt); - return ResponseEntity.ok().body(new Message(resp)); + resp.put("token", issueToken); + resp.put("refreshToken", issueRefresh); + return ResponseEntity.ok(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(); + @GetMapping("/refresh/{refreshToken}") + @ApiOperation(value = "TOKEN刷新", notes = "使用刷新TOKEN重新获取TOKEN") + public ResponseEntity>> refreshToken( + @ApiParam(value = "刷新TOKEN", example = "xxx") + @PathVariable("refreshToken") @NotNull String refreshToken) { + String userId; + boolean isRefresh; + try { + Claims claims = JsonWebTokenUtil.parseJwt(refreshToken); + userId = String.valueOf(claims.getSubject()); + isRefresh = claims.get("refresh", Boolean.class); + } catch (Exception e) { + log.info(e.getMessage()); + Message> message = Message.>builder().msg("刷新TOKEN过期或错误") + .code(MONITOR_LOGIN_FAILED_CODE).build(); + return ResponseEntity.ok(message); } - 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); + if (userId == null || !isRefresh) { + Message> message = Message.>builder().msg("非法的刷新TOKEN") + .code(MONITOR_LOGIN_FAILED_CODE).build(); + return ResponseEntity.ok(message); + } + SurenessAccount account = accountProvider.loadAccount(userId); + if (account == null) { + Message> message = Message.>builder().msg("TOKEN对应的账户不存在") + .code(MONITOR_LOGIN_FAILED_CODE).build(); + return ResponseEntity.ok(message); + } + List roles = account.getOwnRoles(); + // 签发TOKEN + String issueToken = JsonWebTokenUtil.issueJwt(userId, PERIOD_TIME, roles); + Map customClaimMap = new HashMap<>(1); + customClaimMap.put("refresh", true); + String issueRefresh = JsonWebTokenUtil.issueJwt(userId, PERIOD_TIME << 5, customClaimMap); Map resp = new HashMap<>(2); - resp.put("token", jwt); - resp.put("refreshToken", refreshJwt); - return ResponseEntity.ok().body(new Message<>(resp)); + resp.put("token", issueToken); + resp.put("refreshToken", issueRefresh); + return ResponseEntity.ok(new Message<>(resp)); } } diff --git a/manager/src/main/java/com/usthe/manager/pojo/dto/LoginDto.java b/manager/src/main/java/com/usthe/manager/pojo/dto/LoginDto.java new file mode 100644 index 0000000..5ab14c9 --- /dev/null +++ b/manager/src/main/java/com/usthe/manager/pojo/dto/LoginDto.java @@ -0,0 +1,39 @@ +package com.usthe.manager.pojo.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Range; + +import javax.validation.constraints.NotBlank; + +import static io.swagger.annotations.ApiModelProperty.AccessMode.READ_ONLY; + +/** + * 登陆注册账户信息传输体 username phone email + * @author tomsun28 + * @date 20:36 2019-08-01 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@ApiModel(description = "账户信息传输体") +public class LoginDto { + + @ApiModelProperty(value = "类型", example = "1", accessMode = READ_ONLY, position = 0) + @Range(min = 0, max = 4, message = "1.账户(邮箱用户名手机号)密码登陆 2.github登陆 3.微信登陆") + private Byte type; + + @ApiModelProperty(value = "用户标识", example = "1", accessMode = READ_ONLY, position = 0) + @NotBlank(message = "Identifier can not null") + private String identifier; + + @ApiModelProperty(value = "密钥", example = "1", accessMode = READ_ONLY, position = 0) + @NotBlank(message = "Credential can not null") + private String credential; + +} diff --git a/web-app/src/app/core/interceptor/default.interceptor.ts b/web-app/src/app/core/interceptor/default.interceptor.ts index e9261ad..3edf55e 100644 --- a/web-app/src/app/core/interceptor/default.interceptor.ts +++ b/web-app/src/app/core/interceptor/default.interceptor.ts @@ -16,6 +16,8 @@ import { NzNotificationService } from 'ng-zorro-antd/notification'; import { BehaviorSubject, Observable, of, throwError } from 'rxjs'; import { catchError, filter, mergeMap, switchMap, take } from 'rxjs/operators'; +import { Message } from '../../pojo/Message'; +import { AuthService } from '../../service/auth.service'; import { LocalStorageService } from '../../service/local-storage.service'; const CODE_MESSAGE: { [key: number]: string } = { @@ -25,7 +27,7 @@ const CODE_MESSAGE: { [key: number]: string } = { 204: '删除数据成功。', 400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。', 401: '用户没有权限(令牌、用户名、密码错误)。', - 403: '用户得到授权,但是访问是被禁止的。', + 403: '用户无权限访问此资源。', 404: '发出的请求针对的是不存在的记录,服务器没有进行操作。', 406: '请求的格式不可得。', 409: '请求与服务器端目标资源的当前状态相冲突', @@ -42,11 +44,11 @@ const CODE_MESSAGE: { [key: number]: string } = { */ @Injectable() export class DefaultInterceptor implements HttpInterceptor { - private refreshTokenEnabled = environment.api.refreshTokenEnabled; + // 是否正在刷新TOKEN过程 private refreshToking = false; private refreshToken$: BehaviorSubject = new BehaviorSubject(null); - constructor(private injector: Injector, private storageSvc: LocalStorageService) {} + constructor(private injector: Injector, private authSvc: AuthService, private storageSvc: LocalStorageService) {} private get notification(): NzNotificationService { return this.injector.get(NzNotificationService); @@ -61,19 +63,20 @@ export class DefaultInterceptor implements HttpInterceptor { } private checkStatus(ev: HttpResponseBase): void { - // if (ev.status >= 200 && ev.status < 500) { - // return; - // } const errorText = CODE_MESSAGE[ev.status] || ev.statusText; - this.notification.error(`抱歉服务器繁忙 ${ev.status}: ${ev.url}`, errorText); + console.warn(` ${ev.status}: ${ev.url}`, errorText); + this.notification.error(` ${ev.status}: ${ev.url}`, errorText); } /** * 刷新 Token 请求 */ - private refreshTokenRequest(): Observable { + private refreshTokenRequest(): Observable> { const refreshToken = this.storageSvc.getRefreshToken(); - return this.http.post(`/account/auth/refresh`, null, null, { headers: { Authorization: `Bearer ${refreshToken}` } }); + if (refreshToken == null) { + return throwError('refreshToken is null.'); + } + return this.authSvc.refreshToken(refreshToken); } // #region 刷新Token方式一:使用 401 重新刷新 Token @@ -95,32 +98,38 @@ export class DefaultInterceptor implements HttpInterceptor { // 3、尝试调用刷新 Token this.refreshToking = true; this.refreshToken$.next(null); - return this.refreshTokenRequest().pipe( switchMap(res => { - // 通知后续请求继续执行 + // 判断刷新TOKEN是否正确 this.refreshToking = false; - this.refreshToken$.next(res); - // 重新保存新 token - let token = res.token; - let refreshToken = res.refreshToken; - this.storageSvc.storageAuthorizationToken(token); - this.storageSvc.storageRefreshToken(refreshToken); - // 重新发起请求 - return next.handle(this.reAttachToken(req)); + if (res.code === 0 && res.data != undefined) { + let token = res.data.token; + let refreshToken = res.data.refreshToken; + if (token != undefined) { + this.storageSvc.storageAuthorizationToken(token); + this.storageSvc.storageRefreshToken(refreshToken); + // 通知后续请求继续执行 + this.refreshToken$.next(token); + // 重新发起请求 + return next.handle(this.reAttachToken(req)); + } else { + console.warn(`flush new token failed. ${res.msg}`); + return throwError('flush new token failed.'); + } + } else { + console.warn(`flush new token failed. ${res.msg}`); + return throwError('flush new token failed.'); + } }), catchError(err => { + // token 刷新失败 + console.warn(`flush new token failed. ${err.msg}`); this.refreshToking = false; this.toLogin(); return throwError(err); }) ); } - - /** - * 重新附加新 Token 信息 - * - */ private reAttachToken(req: HttpRequest): HttpRequest { let token = this.storageSvc.getAuthorizationToken(); return req.clone({ @@ -158,8 +167,6 @@ export class DefaultInterceptor implements HttpInterceptor { return next.handle(newReq).pipe( mergeMap(httpEvent => { if (httpEvent instanceof HttpResponseBase) { - // todo 处理成功状态响应 - return of(httpEvent); } else { return of(httpEvent); @@ -169,12 +176,7 @@ export class DefaultInterceptor implements HttpInterceptor { // 处理失败响应,处理token过期自动刷新 switch (err.status) { case 401: - if (this.refreshTokenEnabled) { - return this.tryRefreshToken(err, req, next); - } - this.toLogin(); - break; - case 403: + return this.tryRefreshToken(err, newReq, next); case 404: case 500: this.goTo(`/exception/${err.status}?url=${req.urlWithParams}`); @@ -191,7 +193,6 @@ export class DefaultInterceptor implements HttpInterceptor { break; } this.checkStatus(err); - console.warn(`${err.status} == ${err.message}`); return throwError(err); }) ); diff --git a/web-app/src/app/layout/basic/widgets/notify.component.ts b/web-app/src/app/layout/basic/widgets/notify.component.ts index 61912d0..89be038 100644 --- a/web-app/src/app/layout/basic/widgets/notify.component.ts +++ b/web-app/src/app/layout/basic/widgets/notify.component.ts @@ -60,6 +60,10 @@ export class HeaderNotifyComponent implements OnInit { if (message.code === 0) { let page = message.data; let alerts = page.content; + if (alerts == undefined) { + this.loading = false; + return; + } this.data[0].list = []; alerts.forEach(alert => { let item = { diff --git a/web-app/src/app/service/auth.service.spec.ts b/web-app/src/app/service/auth.service.spec.ts new file mode 100644 index 0000000..f1251ca --- /dev/null +++ b/web-app/src/app/service/auth.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(AuthService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/web-app/src/app/service/auth.service.ts b/web-app/src/app/service/auth.service.ts new file mode 100644 index 0000000..1ea6022 --- /dev/null +++ b/web-app/src/app/service/auth.service.ts @@ -0,0 +1,18 @@ +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + +import { Message } from '../pojo/Message'; + +const account_auth_refresh_uri = '/account/auth/refresh'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthService { + constructor(private http: HttpClient) {} + + public refreshToken(refreshToken: string): Observable> { + return this.http.get>(`${account_auth_refresh_uri}/${refreshToken}`); + } +}