设备和token

This commit is contained in:
liulu
2024-12-06 15:38:22 +08:00
parent bcf8dfb5f3
commit 04cfab2f82
25 changed files with 588 additions and 25 deletions
+7
View File
@@ -49,6 +49,13 @@
<groupId>com.dm</groupId>
<artifactId>DmJdbcDriver</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
</dependencies>
@@ -0,0 +1,111 @@
package com.sunyard.chsm.auth;
import com.sunyard.chsm.model.R;
import com.sunyard.chsm.service.AppLoginService;
import com.sunyard.chsm.utils.JsonUtils;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
/**
* @author liulu
* @version V1.0
* @since 2023/8/4
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class AppTokenFilter extends OncePerRequestFilter {
public static final Collection<String> WHITE_URL = Arrays.asList(
"/appUser/getAppToken",
"/appUser/getAppTokenTest"
);
private final AppLoginService appLoginService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String requestURI = request.getRequestURI();
if (WHITE_URL.contains(requestURI)) {
filterChain.doFilter(request, response);
return;
}
String tokenValue = extractToken(request);
if (ObjectUtils.isEmpty(tokenValue)) {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getOutputStream().write(JsonUtils.toJsonBytes(R.error("请先获取token后再访问")));
return;
}
try {
AppUser user = appLoginService.verifyToken(tokenValue);
request.setAttribute("APP_USER", user);
filterChain.doFilter(request, response);
} catch (ExpiredJwtException ex) {
log.warn("token已过期: {}", requestURI);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getOutputStream().write(JsonUtils.toJsonBytes(R.error("token已过期")));
} catch (JwtException ex) {
log.warn("token格式错误: {}", requestURI);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getOutputStream().write(JsonUtils.toJsonBytes(R.error("token格式错误")));
} catch (Exception ex) {
log.error("未知异常: {}", requestURI, ex);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getOutputStream().write(JsonUtils.toJsonBytes(R.error("发生异常")));
}
}
protected String extractToken(HttpServletRequest request) {
// first check the header...
String token = extractHeaderToken(request);
// bearer type allows a request parameter as well
if (token == null) {
logger.debug("Token not found in headers. Trying request parameters.");
token = request.getParameter("access_token");
if (token == null) {
logger.debug("Token not found in request parameters. Not an OAuth2 request.");
}
}
return token;
}
private static final String BEARER_TYPE = "Bearer";
private String extractHeaderToken(HttpServletRequest request) {
Enumeration<String> headers = request.getHeaders("Authorization");
while (headers.hasMoreElements()) { // typically there is only one (most servers enforce that)
String value = headers.nextElement();
if (value.toLowerCase().startsWith(BEARER_TYPE.toLowerCase())) {
String authHeaderValue = value.substring(BEARER_TYPE.length()).trim();
int commaIndex = authHeaderValue.indexOf(',');
if (commaIndex > 0) {
authHeaderValue = authHeaderValue.substring(0, commaIndex);
}
return authHeaderValue;
}
}
return null;
}
}
@@ -0,0 +1,19 @@
package com.sunyard.chsm.auth;
import lombok.Data;
import java.util.List;
/**
* @author liulu
* @since 2024/12/6
*/
@Data
public class AppUser {
private Long appId;
private String name;
private List<Long> serviceIds;
}
@@ -1,4 +1,4 @@
package com.sunyard.chsm.config;
package com.sunyard.chsm.auth;
import org.springframework.web.servlet.HandlerInterceptor;
@@ -0,0 +1,19 @@
package com.sunyard.chsm.auth;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
/**
* @author liulu
* @since 2024/12/6
*/
public class UserContext {
public static AppUser getCurrentUser() {
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
return (AppUser) requestAttributes.getAttribute("APP_USER", RequestAttributes.SCOPE_REQUEST);
}
}
@@ -0,0 +1,96 @@
package com.sunyard.chsm.config;
import com.sunyard.chsm.model.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.IllegalFormatException;
import java.util.List;
import java.util.Optional;
/**
* 全局controller异常处理
*
* @auther: ZS
* @description:
* @param:
* @return:
* @date: 2020/5/25 16:24
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionResolver {
/**
* 校验器
*/
@ExceptionHandler(BindException.class)
@ResponseBody
public R<?> handleBindException(BindException e) {
log.info("校验器异常", e);
FieldError fieldError = e.getBindingResult().getFieldError();
return R.error(400, fieldError.getDefaultMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public R<?> validExceptionHandler(MethodArgumentNotValidException ex) {
return Optional.of(ex)
.map(MethodArgumentNotValidException::getFieldError)
.map(FieldError::getDefaultMessage)
.map(message -> R.error(400, message))
.get();
}
private static final List<Class<? extends Exception>> EX_CLASS = Arrays.asList(
IllegalArgumentException.class, IllegalStateException.class,
IllegalFormatException.class,
UnsupportedOperationException.class);
@ExceptionHandler(Exception.class)
public R<?> exceptionHandler(Exception ex, HttpServletRequest request) {
String errorMessage = buildErrorMessage(ex);
for (Class<? extends Exception> eClass : EX_CLASS) {
Exception exception = findException(eClass, ex);
if (exception == null) {
continue;
}
log.warn("Validation failed -> {} ", errorMessage);
return R.error(400, exception.getMessage());
}
log.error("request: {} - {}, 系统异常 -> ", request.getMethod(), request.getRequestURI(), ex);
return R.error("系统异常");
}
public static <T extends Exception> T findException(Class<T> exClass, Throwable ex) {
for (int i = 0; ex != null && i < 3; i++) {
if (exClass.isAssignableFrom(ex.getClass())) {
//noinspection unchecked
return (T) ex;
}
ex = ex.getCause();
}
return null;
}
public static String buildErrorMessage(Throwable ex) {
StringBuilder sb = new StringBuilder();
sb.append(ex.getMessage());
StackTraceElement[] stackTrace = ex.getStackTrace();
int i = 0;
for (StackTraceElement stackTraceElement : stackTrace) {
if (i++ >= 5) {
break;
}
sb.append("\n\t").append(stackTraceElement.toString());
}
return sb.toString();
}
}
@@ -1,17 +1,49 @@
package com.sunyard.chsm.controller;
import com.sunyard.chsm.param.AppTokenReq;
import com.sunyard.chsm.param.AppTokenResp;
import com.sunyard.chsm.service.AppLoginService;
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.annotation.Resource;
import javax.validation.Valid;
/**
* 应用Token服务接口
*
* @author liulu
* @version V1.0
* @since 2023/8/4
* @since 2024/12/4
*/
@RestController
@RequestMapping
public class AppLoginController {
@Resource
private AppLoginService appLoginService;
/**
* 获取应用Token接口
* 密码服务平台为各个接入应用提供生成应用授权Token,
* 接入应用需在密码服务平台获取了到平台分配的appKey和appSecret
*
* @param appTokenReq 请求参数
* @return
*/
@PostMapping("/appUser/getAppToken")
public AppTokenResp getAppToken(@Valid @RequestBody AppTokenReq appTokenReq) {
return appLoginService.getAppToken(appTokenReq);
}
@PostMapping("/appUser/getAppTokenTest")
public AppTokenResp getAppTokenForTest(@Valid @RequestBody AppTokenReq appTokenReq) {
return appLoginService.getAppTokenForTest(appTokenReq);
}
}
@@ -0,0 +1,39 @@
package com.sunyard.chsm.controller;
import com.sunyard.chsm.model.R;
import com.sunyard.chsm.model.dto.KeyInfoDTO;
import com.sunyard.chsm.service.KeyInfoService;
import org.springframework.validation.annotation.Validated;
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.annotation.Resource;
/**
* 密钥管理类接口
*
* @author liulu
* @since 2024/12/6
*/
@RestController
@RequestMapping("/key/manage")
public class KeyManageController {
@Resource
private KeyInfoService keyInfoService;
/**
* 启用密钥
*
* @param param 密钥id
* @return id
*/
@PostMapping("/enable")
public R<Void> enableKey(@Validated @RequestBody KeyInfoDTO.IDs param) {
keyInfoService.enableKey(param.getIds());
return R.ok();
}
}
@@ -0,0 +1,113 @@
package com.sunyard.chsm.service;
import com.sunyard.chsm.auth.AppUser;
import com.sunyard.chsm.constant.SecurityConstant;
import com.sunyard.chsm.enums.EnableStatus;
import com.sunyard.chsm.mapper.ApplicationMapper;
import com.sunyard.chsm.model.entity.Application;
import com.sunyard.chsm.param.AppTokenReq;
import com.sunyard.chsm.param.AppTokenResp;
import com.sunyard.chsm.utils.CodecUtils;
import com.sunyard.chsm.utils.gm.BCSM3Utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
/**
* @author liulu
* @since 2024/12/5
*/
@Slf4j
@Service
public class AppLoginService {
/**
* token 过期时间, 分钟
*/
@Value("${chsm.token.expireTime:720}")
private Integer tokenExpireTime;
@Resource
private ApplicationMapper applicationMapper;
public AppTokenResp getAppToken(AppTokenReq req) {
Long random = req.getRandom();
long now = System.currentTimeMillis();
Assert.isTrue(now - random <= 5 * 60 * 1000, "请求已超时");
String appKey = req.getAppKey();
Application application = applicationMapper.selectByAppKey(appKey);
Assert.isTrue(EnableStatus.ENABLED.getCode().equals(application.getStatus()), "此应用已停用");
String data = appKey + random + application.getAppSecret();
byte[] hmac = BCSM3Utils.hmac(application.getAppSecret().getBytes(), data.getBytes());
String serverHmac = CodecUtils.encodeHex(hmac);
if (!Objects.equals(req.getHmac(), serverHmac)) {
log.warn("appKey: {}, req hmac: {}, server hmac: {}", appKey, req.getHmac(), serverHmac);
throw new IllegalArgumentException("应用认证失败");
}
AppTokenResp resp = new AppTokenResp();
resp.setToken(genToken(application));
return resp;
}
private String genToken(Application app) {
Map<String, Object> claims = new HashMap<>();
claims.put("appId", app.getId());
claims.put("name", app.getName());
claims.put("serviceIds", Collections.singletonList(app.getId()));
Date now = new Date();
return Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + tokenExpireTime * 60 * 1000))
.setId(UUID.randomUUID().toString())
.signWith(SignatureAlgorithm.HS512, SecurityConstant.JWT_SIGN_KEY)
.compact();
}
public AppTokenResp getAppTokenForTest(@Valid AppTokenReq req) {
// Long random = req.getRandom();
// long now = System.currentTimeMillis();
// Assert.isTrue(now - random <= 5 * 60 * 1000, "请求已超时");
Application application = applicationMapper.selectByAppKey(req.getAppKey());
Assert.isTrue(EnableStatus.ENABLED.getCode().equals(application.getStatus()), "此应用已停用");
if (!Objects.equals(req.getHmac(), application.getAppSecret())) {
log.warn("appKey: {}, req hmac: {},", req.getAppKey(), req.getHmac());
throw new IllegalArgumentException("应用认证失败");
}
AppTokenResp resp = new AppTokenResp();
resp.setToken(genToken(application));
return resp;
}
public AppUser verifyToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(SecurityConstant.JWT_SIGN_KEY)
.parseClaimsJws(token)
.getBody();
AppUser user = new AppUser();
user.setAppId(claims.get("appId", Long.class));
user.setName(claims.get("name", String.class));
user.setServiceIds(claims.get("serviceIds", List.class));
return user;
}
}
@@ -12,7 +12,7 @@ spring:
# 数据源
datasource:
driverClassName: dm.jdbc.driver.DmDriver
url: jdbc:dm://172.16.18.44:5236?schema=SSP&useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai&useSSL=true&characterEncoding=UTF-8
url: jdbc:dm://172.16.18.44:5236?schema=SUNYARD_SSP&useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai&useSSL=true&characterEncoding=UTF-8
username: SUNYARD
# Jasypt加密 可到common-utils中找到JasyptUtil加解密工具类生成加密结果 格式为ENC(加密结果)
password: 123456