## 工业互联网平台鉴权功能 ## 1. 登录功能 登录接口: http://0.0.0.0/admin-api/system/auth/login ① 参数说明 { "tenantName": "tenant1", //租户名称(必传),需事先在平台配置 "username": "admin", //用户名(必传),平台事先维护好用户 "password": "123", //密码(必传) "captchaVerification": "PfcH6mgr==" //验证码(非必传),若开启的话需要前后台同时开启 } 其中租户名称在登录逻辑中用不到,用到的是租户对应的ID,接口请求的时候需要将Tenant-Id封装到请求头中,登录逻辑会从请求头中获取并使用,如下截图 特别注意:平台其它接口也都需要封装此租户ID ② 主要代码 1、控制层接口 `@PostMapping("/login") @PermitAll @Operation(summary = "使用账号密码登录") public CommonResult login(@RequestBody @Valid AuthLoginReqVO reqVO) { return success(authService.login(reqVO)); }` 2、参数对象 AuthLoginReqVO @Schema(description = "管理后台 - 账号密码登录 Request VO") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class AuthLoginReqVO { @Schema(description = "账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "iailabyuanma") @NotEmpty(message = "登录账号不能为空") @Length(min = 4, max = 16, message = "账号长度为 4-16 位") @Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母") private String username; @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "buzhidao") @NotEmpty(message = "密码不能为空") @Length(min = 4, max = 16, message = "密码长度为 4-16 位") private String password; // ========== 图片验证码相关 ========== @Schema(description = "验证码,验证码开启时,需要传递", requiredMode = Schema.RequiredMode.REQUIRED, example = "PfcH6mgr8tpXuMWFjvW6YVaqrswIuwmWI5dsVZSg7sGpWtDCUbHuDEXl3cFB1+VvCC/rAkSwK8Fad52FSuncVg==") @NotEmpty(message = "验证码不能为空", groups = CodeEnableGroup.class) private String captchaVerification; } 3、登录实现类 @Override public AuthLoginRespVO login(AuthLoginReqVO reqVO) { // 校验验证码 validateCaptcha(reqVO); // 使用账号密码,进行登录 AdminUserDO user = authenticate(reqVO.getUsername(), reqVO.getPassword()); // 创建 Token 令牌,记录登录日志 return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME); } 4、验证用户名密码 @Override public AdminUserDO authenticate(String username, String password) { final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME; // 校验账号是否存在 AdminUserDO user = userService.getUserByUsername(username); if (user == null) { createLoginLog(null, username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS); throw exception(AUTH_LOGIN_BAD_CREDENTIALS); } if (!userService.isPasswordMatch(password, user.getPassword())) { createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS); throw exception(AUTH_LOGIN_BAD_CREDENTIALS); } // 校验是否禁用 if (CommonStatusEnum.isDisable(user.getStatus())) { createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.USER_DISABLED); throw exception(AUTH_LOGIN_USER_DISABLED); } return user; } 用户名密码校验不通过常量如下图 5、创建令牌 private AuthLoginRespVO createTokenAfterLoginSuccess(Long userId, String username, LoginLogTypeEnum logType) { // 插入登陆日志 createLoginLog(userId, username, logType, LoginResultEnum.SUCCESS); // 创建访问令牌 OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, getUserType().getValue(), OAuth2ClientConstants.CLIENT_ID_DEFAULT, null); // 构建返回结果 return AuthConvert.INSTANCE.convert(accessTokenDO); } 其中“OAuth2ClientConstants.CLIENT_ID_DEFAULT”是在平台配置好的默认客户端,主要包括授权类型、刷新token有效期、访问token有效期等 6、创建访问令牌和刷新令牌 @Override @Transactional public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId, List scopes) { OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId); // 创建刷新令牌 OAuth2RefreshTokenDO refreshTokenDO = createOAuth2RefreshToken(userId, userType, clientDO, scopes); // 创建访问令牌 return createOAuth2AccessToken(refreshTokenDO, clientDO); } private OAuth2AccessTokenDO createOAuth2AccessToken(OAuth2RefreshTokenDO refreshTokenDO, OAuth2ClientDO clientDO) { OAuth2AccessTokenDO accessTokenDO = new OAuth2AccessTokenDO().setAccessToken(generateAccessToken()) .setUserId(refreshTokenDO.getUserId()).setUserType(refreshTokenDO.getUserType()) .setUserInfo(buildUserInfo(refreshTokenDO.getUserId(), refreshTokenDO.getUserType())) .setClientId(clientDO.getClientId()).setScopes(refreshTokenDO.getScopes()) .setRefreshToken(refreshTokenDO.getRefreshToken()) .setExpiresTime(LocalDateTime.now().plusSeconds(clientDO.getAccessTokenValiditySeconds())); accessTokenDO.setTenantId(TenantContextHolder.getTenantId()); // 手动设置租户编号,避免缓存到 Redis 的时候,无对应的租户编号 oauth2AccessTokenMapper.insert(accessTokenDO); // 记录到 Redis 中 oauth2AccessTokenRedisDAO.set(accessTokenDO); return accessTokenDO; } private OAuth2RefreshTokenDO createOAuth2RefreshToken(Long userId, Integer userType, OAuth2ClientDO clientDO, List scopes) { OAuth2RefreshTokenDO refreshToken = new OAuth2RefreshTokenDO().setRefreshToken(generateRefreshToken()) .setUserId(userId).setUserType(userType) .setClientId(clientDO.getClientId()).setScopes(scopes) .setExpiresTime(LocalDateTime.now().plusSeconds(clientDO.getRefreshTokenValiditySeconds())); oauth2RefreshTokenMapper.insert(refreshToken); return refreshToken; } 注意:访问令牌是请求后端接口的通行证,所有需要鉴权的后端api都需要携带访问令牌,访问令牌过期后会请求刷新令牌接口,重新获取访问令牌。 当刷新令牌过期后,就需要重新登录获取授权了! 7、刷新令牌 @PostMapping("/refresh-token") @PermitAll @Operation(summary = "刷新令牌") @Parameter(name = "refreshToken", description = "刷新令牌", required = true) public CommonResult refreshToken(@RequestParam("refreshToken") String refreshToken) { return success(authService.refreshToken(refreshToken)); } @Override public AuthLoginRespVO refreshToken(String refreshToken) { OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, OAuth2ClientConstants.CLIENT_ID_DEFAULT); return AuthConvert.INSTANCE.convert(accessTokenDO); } @Override public OAuth2AccessTokenDO refreshAccessToken(String refreshToken, String clientId) { // 查询访问令牌 OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectByRefreshToken(refreshToken); if (refreshTokenDO == null) { throw exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), "无效的刷新令牌"); } // 校验 Client 匹配 OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId); if (ObjectUtil.notEqual(clientId, refreshTokenDO.getClientId())) { throw exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), "刷新令牌的客户端编号不正确"); } // 移除相关的访问令牌 List accessTokenDOs = oauth2AccessTokenMapper.selectListByRefreshToken(refreshToken); if (CollUtil.isNotEmpty(accessTokenDOs)) { oauth2AccessTokenMapper.deleteBatchIds(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getId)); oauth2AccessTokenRedisDAO.deleteList(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getAccessToken)); } // 已过期的情况下,删除刷新令牌 if (DateUtils.isExpired(refreshTokenDO.getExpiresTime())) { oauth2RefreshTokenMapper.deleteById(refreshTokenDO.getId()); throw exception0(GlobalErrorCodeConstants.UNAUTHORIZED.getCode(), "刷新令牌已过期"); } // 创建访问令牌 return createOAuth2AccessToken(refreshTokenDO, clientDO); } 8、前端封装token和租户ID代码参考 // request拦截器 service.interceptors.request.use( (config: InternalAxiosRequestConfig) => { // 是否需要设置 token let isToken = (config!.headers || {}).isToken === false whiteList.some((v) => { if (config.url && config.url.indexOf(v) > -1) { return (isToken = false) } }) if (getAccessToken() && !isToken) { config.headers.Authorization = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token } // 设置租户 if (tenantEnable && tenantEnable === 'true') { const tenantId = getTenantId() if (tenantId) config.headers['tenant-id'] = tenantId } const method = config.method?.toUpperCase() // 防止 GET 请求缓存 if (method === 'GET') { config.headers['Cache-Control'] = 'no-cache' config.headers['Pragma'] = 'no-cache' } // 自定义参数序列化函数 else if (method === 'POST') { const contentType = config.headers['Content-Type'] || config.headers['content-type'] if (contentType === 'application/x-www-form-urlencoded') { if (config.data && typeof config.data !== 'string') { config.data = qs.stringify(config.data) } } } return config }, (error: AxiosError) => { // Do something with request error console.log(error) // for debug return Promise.reject(error) } ) 2. 退出登录功能 登出接口: http://0.0.0.0/admin-api/system/auth/logout 核心代码如下 @PostMapping("/logout") @PermitAll @Operation(summary = "登出系统") public CommonResult logout(HttpServletRequest request) { String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader(), securityProperties.getTokenParameter()); if (StrUtil.isNotBlank(token)) { authService.logout(token, LoginLogTypeEnum.LOGOUT_SELF.getType()); } return success(true); } @Override public void logout(String token, Integer logType) { // 删除访问令牌 OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.removeAccessToken(token); if (accessTokenDO == null) { return; } // 删除成功,则记录登出日志 createLogoutLog(accessTokenDO.getUserId(), accessTokenDO.getUserType(), logType); } @Override public OAuth2AccessTokenDO removeAccessToken(String accessToken) { // 删除访问令牌 OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessToken); if (accessTokenDO == null) { return null; } oauth2AccessTokenMapper.deleteById(accessTokenDO.getId()); oauth2AccessTokenRedisDAO.delete(accessToken); // 删除刷新令牌 oauth2RefreshTokenMapper.deleteByRefreshToken(accessTokenDO.getRefreshToken()); return accessTokenDO; }