предисловие
Запишите сценарии приложения в системном приложении, где интерфейс и сервер разделены — передача информации о пользователе.
Происхождение спроса
Взгляните сначалаweb
Классическая схема архитектуры системы, эта схема относится к сети:
существует Пользовательское исключение Dubbo, как вы с ним справляетесь?Архитектура была кратко объяснена в разделе , и здесь описываться не будет.
Кратко опишите в этой архитектуре用户信息(如userId)的传递方式
:
В настоящее время подавляющее большинство проектов представляют собой модели разработки с разделением front-end и back-end.token
Аутентификация пользователя осуществляется следующим образом:
- Клиент (ПК, мобильный, планшет и т. д.) входит в систему в первый раз и выдается сервером
token
,существуетtoken
вставить用户信息(如userId)
ждать возврата клиенту - Клиент обращается к серверному интерфейсу, который нужно вынести в заголовок
token
, и отправить его на сервер вместе с формой - Сервер
web
Унифицированный анализ слоевtoken
аутентификация при извлечении用户信息(如userId)
И продолжайте переходить на нижний уровень, на сервисный уровень для работы бизнес-логики. - сервер в
service
слой взят用户信息(如userId)
После этого выполните соответствующую операцию бизнес-логики
проблема:
Почему должен用户信息(如userId)
спрятаться вtoken
, сервер выполняет повторную обработкуtoken
выиграть? Вернуться к клиенту после входа в систему напрямую用户信息(如userId)
Разве не удобнее?
Информация, тесно связанная с пользователями, как правило, довольно чувствительна.用户信息(如userId)
Это не будет напрямую подвергать клиента прямому воздействию, это принесет риски.
В одном приложении用户信息(如userId)
процесс доставки
чтоМонолитное приложениеКраткое описаниеweb
Этаж,service
слои все в одномjvm
В процессе, вообще говоря, только一个项目
.
Войдите, чтобы подписать токен
Взгляните на следующий псевдокод для интерфейса входа:
web
Интерфейс слоя:
@Loggable(descp = "用户登录", include = "loginParam")
@PostMapping("/login")
public BaseResult<LoginVo> accountLogin(LoginParam loginParam) {
return mAccountService.login(loginParam);
}
service
Псевдокод интерфейса слоя:
public BaseResult<LoginVo> login(LoginParam param) throws BaseException {
//1.登录逻辑判断
LoginVo loginVo = handleLogin(param);
//2.签发token
String subject = userId;
String jwt = JsonWebTokenUtil.issueJWT(UUID.randomUUID().toString(), subject,
"token-server", BaseConstants.TOKEN_PERIOD_TIME, "", null, SignatureAlgorithm.HS512);
loginVo.setJwt(jwt);
return ResultUtil.success(loginVo);
}
Обратите внимание, что в приведенном выше псевдокоде выдачаtoken
КогдаuserId
Введите идентификатор клиентаsubject
, выдаетсяtoken
возвращен клиенту. используется здесьJJWT
Сгенерированоtoken
Импорт зависимостей:
<!--jjwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.8.9</version>
</dependency>
связанные инструментыJsonWebTokenUtil
:
public class JsonWebTokenUtil {
//秘钥
public static final String SECRET_KEY = BaseConstant.SECRET_KEY;
private static final ObjectMapper MAPPER = new ObjectMapper();
private static CompressionCodecResolver codecResolver = new DefaultCompressionCodecResolver();
//私有化构造
private JsonWebTokenUtil() {
}
/* *
* @Description json web token 签发
* @param id 令牌ID
* @param subject 用户标识
* @param issuer 签发人
* @param period 有效时间(秒)
* @param roles 访问主张-角色
* @param permissions 访问主张-权限
* @param algorithm 加密算法
* @Return java.lang.String
*/
public static String issueJWT(String id,String subject, String issuer, Long period,
String roles, String permissions, SignatureAlgorithm algorithm) {
// 当前时间戳
Long currentTimeMillis = System.currentTimeMillis();
// 秘钥
byte[] secreKeyBytes = DatatypeConverter.parseBase64Binary(SECRET_KEY);
JwtBuilder jwtBuilder = Jwts.builder();
if (StringUtils.isNotBlank(id)) {
jwtBuilder.setId(id);
}
if (StringUtils.isNotBlank(subject)) {
jwtBuilder.setSubject(subject);
}
if (StringUtils.isNotBlank(issuer)) {
jwtBuilder.setIssuer(issuer);
}
// 设置签发时间
jwtBuilder.setIssuedAt(new Date(currentTimeMillis));
// 设置到期时间
if (null != period) {
jwtBuilder.setExpiration(new Date(currentTimeMillis + period*1000));
}
if (StringUtils.isNotBlank(roles)) {
jwtBuilder.claim("roles",roles);
}
if (StringUtils.isNotBlank(permissions)) {
jwtBuilder.claim("perms",permissions);
}
// 压缩,可选GZIP
jwtBuilder.compressWith(CompressionCodecs.DEFLATE);
// 加密设置
jwtBuilder.signWith(algorithm,secreKeyBytes);
return jwtBuilder.compact();
}
/**
* 解析JWT的Payload
*/
public static String parseJwtPayload(String jwt){
Assert.hasText(jwt, "JWT String argument cannot be null or empty.");
String base64UrlEncodedHeader = null;
String base64UrlEncodedPayload = null;
String base64UrlEncodedDigest = null;
int delimiterCount = 0;
StringBuilder sb = new StringBuilder(128);
for (char c : jwt.toCharArray()) {
if (c == '.') {
CharSequence tokenSeq = io.jsonwebtoken.lang.Strings.clean(sb);
String token = tokenSeq!=null?tokenSeq.toString():null;
if (delimiterCount == 0) {
base64UrlEncodedHeader = token;
} else if (delimiterCount == 1) {
base64UrlEncodedPayload = token;
}
delimiterCount++;
sb.setLength(0);
} else {
sb.append(c);
}
}
if (delimiterCount != 2) {
String msg = "JWT strings must contain exactly 2 period characters. Found: " + delimiterCount;
throw new MalformedJwtException(msg);
}
if (sb.length() > 0) {
base64UrlEncodedDigest = sb.toString();
}
if (base64UrlEncodedPayload == null) {
throw new MalformedJwtException("JWT string '" + jwt + "' is missing a body/payload.");
}
// =============== Header =================
Header header = null;
CompressionCodec compressionCodec = null;
if (base64UrlEncodedHeader != null) {
String origValue = TextCodec.BASE64URL.decodeToString(base64UrlEncodedHeader);
Map<String, Object> m = readValue(origValue);
if (base64UrlEncodedDigest != null) {
header = new DefaultJwsHeader(m);
} else {
header = new DefaultHeader(m);
}
compressionCodec = codecResolver.resolveCompressionCodec(header);
}
// =============== Body =================
String payload;
if (compressionCodec != null) {
byte[] decompressed = compressionCodec.decompress(TextCodec.BASE64URL.decode(base64UrlEncodedPayload));
payload = new String(decompressed, io.jsonwebtoken.lang.Strings.UTF_8);
} else {
payload = TextCodec.BASE64URL.decodeToString(base64UrlEncodedPayload);
}
return payload;
}
/**
* 验签JWT
*
* @param jwt json web token
*/
public static JwtAccount parseJwt(String jwt, String appKey) throws ExpiredJwtException, UnsupportedJwtException,
MalformedJwtException, SignatureException, IllegalArgumentException {
Claims claims = Jwts.parser()
.setSigningKey(DatatypeConverter.parseBase64Binary(appKey))
.parseClaimsJws(jwt)
.getBody();
JwtAccount jwtAccount = new JwtAccount();
//令牌ID
jwtAccount.setTokenId(claims.getId());
//客户标识
String subject = claims.getSubject();
jwtAccount.setSubject(subject);
//用户id
jwtAccount.setUserId(subject);
//签发者
jwtAccount.setIssuer(claims.getIssuer());
//签发时间
jwtAccount.setIssuedAt(claims.getIssuedAt());
//接收方
jwtAccount.setAudience(claims.getAudience());
//访问主张-角色
jwtAccount.setRoles(claims.get("roles", String.class));
//访问主张-权限
jwtAccount.setPerms(claims.get("perms", String.class));
return jwtAccount;
}
public static Map<String, Object> readValue(String val) {
try {
return MAPPER.readValue(val, Map.class);
} catch (IOException e) {
throw new MalformedJwtException("Unable to userpager JSON value: " + val, e);
}
}
}
JWT
Связанные объектыJwtAccount
:
@Data
public class JwtAccount implements Serializable {
private static final long serialVersionUID = -895875540581785581L;
/**
* 令牌id
*/
private String tokenId;
/**
* 客户标识(用户id)
*/
private String subject;
/**
* 用户id
*/
private String userId;
/**
* 签发者(JWT令牌此项有值)
*/
private String issuer;
/**
* 签发时间
*/
private Date issuedAt;
/**
* 接收方(JWT令牌此项有值)
*/
private String audience;
/**
* 访问主张-角色(JWT令牌此项有值)
*/
private String roles;
/**
* 访问主张-资源(JWT令牌此项有值)
*/
private String perms;
/**
* 客户地址
*/
private String host;
public JwtAccount() {
}
}
web
Унифицированная аутентификация уровня, синтаксический анализtoken
Клиент обращается к серверному интерфейсу, который нужно вынести в заголовокtoken
, отправленный на сервер вместе с формой, и сервер находится вweb
слой добавленMVC
Унифицированная обработка перехватчика
новыйMVC
Перехватчик выглядит следующим образом:
public class UpmsInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
BaseResult result = null;
//获取请求uri
String requestURI = request.getRequestURI();
...省略部分逻辑
//获取认证token
String jwt = request.getHeader(BaseConstant.AUTHORIZATION);
//不传认证token,判断为无效请求
if (StringUtils.isBlank(jwt)) {
result = ResultUtil.error(ResultEnum.ERROR_REQUEST);
RequestResponseUtil.responseWrite(JSON.toJSONString(result), response);
return false;
}
//其他请求均需验证token有效性
JwtAccount jwtAccount = null;
String payload = null;
try {
// 解析Payload
payload = JsonWebTokenUtil.parseJwtPayload(jwt);
//取出payload中字段信息
if (payload.charAt(0) == '{'
&& payload.charAt(payload.length() - 1) == '}') {
Map<String, Object> payloadMap = JsonWebTokenUtil.readValue(payload);
//客户标识(userId)
String subject = (String) payloadMap.get("sub");
//查询用户签发秘钥
}
//验签token
jwtAccount = JsonWebTokenUtil.parseJwt(jwt, JsonWebTokenUtil.SECRET_KEY);
} catch (SignatureException | UnsupportedJwtException | MalformedJwtException | IllegalArgumentException e) {
//令牌错误
result = ResultUtil.error(ResultEnum.ERROR_JWT);
RequestResponseUtil.responseWrite(JSON.toJSONString(result), response);
return false;
} catch (ExpiredJwtException e) {
//令牌过期
result = ResultUtil.error(ResultEnum.EXPIRED_JWT);
RequestResponseUtil.responseWrite(JSON.toJSONString(result), response);
return false;
} catch (Exception e) {
//解析异常
result = ResultUtil.error(ResultEnum.ERROR_JWT);
RequestResponseUtil.responseWrite(JSON.toJSONString(result), response);
return false;
}
if (null == jwtAccount) {
//令牌错误
result = ResultUtil.error(ResultEnum.ERROR_JWT);
RequestResponseUtil.responseWrite(JSON.toJSONString(result), response);
return false;
}
//将用户信息放入threadLocal中,线程共享
ThreadLocalUtil.getInstance().bind(jwtAccount.getUserId());
return true;
}
//...省略部分代码
}
весьtoken
Процесс синтаксического анализа был объяснен в комментариях к коду, вы можете видеть, что синтаксический анализ завершенtoken
после выносаuserId
, поместите информацию о пользователе вthreadLocal
примерноthreadLocal
использование не обсуждается в этой статье.
//将用户信息放入threadLocal中,线程共享
ThreadLocalUtil.getInstance().bind(jwtAccount.getUserId());
Добавьте конфигурацию, чтобы перехватчик вступил в силу:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
...省略部分代码">
<!-- web拦截器 -->
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<bean class="com.easywits.upms.client.interceptor.UpmsInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
</beans>
Связанный код инструментаThreadLocalUtil
:
public class ThreadLocalUtil {
private ThreadLocal<UserInfo> userInfoThreadLocal = new ThreadLocal<>();
//new一个实例
private static final ThreadLocalUtil instance = new ThreadLocalUtil();
//私有化构造
private ThreadLocalUtil() {
}
//获取单例
public static ThreadLocalUtil getInstance() {
return instance;
}
/**
* 将用户对象绑定到当前线程中,键为userInfoThreadLocal对象,值为userInfo对象
*
* @param userInfo
*/
public void bind(UserInfo userInfo) {
userInfoThreadLocal.set(userInfo);
}
/**
* 将用户数据绑定到当前线程中,键为userInfoThreadLocal对象,值为userInfo对象
*
* @param companyId
* @param userId
*/
public void bind(String userId) {
UserInfo userInfo = new UserInfo();
userInfo.setUserId(userId);
bind(userInfo);
}
/**
* 得到绑定的用户对象
*
* @return
*/
public UserInfo getUserInfo() {
UserInfo userInfo = userInfoThreadLocal.get();
remove();
return userInfo;
}
/**
* 移除绑定的用户对象
*/
public void remove() {
userInfoThreadLocal.remove();
}
}
затем вweb
слой иservice
ты можешь его достатьuserId
:
@Loggable(descp = "用户个人资料", include = "")
@GetMapping(value = "/info")
public BaseResult<UserInfoVo> userInfo() {
//拿到用户信息
UserInfo userInfo = ThreadLocalUtil.getInstance().getUserInfo();
return mUserService.userInfo();
}
service
получение слояuserId
:
public BaseResult<UserInfoVo> userInfo() throws BaseException {
//拿到用户信息
UserInfo userInfo = ThreadLocalUtil.getInstance().getUserInfo();
UserInfoVo userInfoVo = getUserInfoVo(userInfo.getUserId);
return ResultUtil.success(userInfoVo);
}
Распределенное приложение (Dubbo)用户信息(如userId)
процесс доставки
Самая большая разница между распределенным приложением и отдельным приложением заключается в том, что оно разделено на несколько приложений.service
слой сweb
Слой разделен на два отдельных приложения, использованиеrpc
Вызывающий метод обрабатывает бизнес-логику. В приведенном выше подходе мы помещаем информацию о пользователе вthreadLocal
, относится к одному прикладному процессу, еслиservice
Если интерфейс уровня находится в другом сервисном процессе, он будет недоступен.
Есть ли способ решить проблему передачи информации о пользователе между процессами? просматриватьDubbo
Официальные документы есть隐式参数
Функции:
Документация очень ясная, просто надоweb
Следующий код вызывается в унифицированном перехватчике слоя, и пользователь может бытьid
распространиться наservice
Этаж
RpcContext.getContext().setAttachment("userId", xxx);
настроить соответственноweb
Код перехватчика слоя:
public class UpmsInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//...省略部分代码
//将用户信息放入threadLocal中,线程共享
ThreadLocalUtil.getInstance().bind(jwtAccount.getUserId());
//将用户信息隐式透传到服务层
RpcContext.getContext().setAttachment("userId", jwtAccount.getUserId());
return true;
}
//...省略部分代码
}
Тогда этот сервисный слой может получить пользовательid
в настоящее время:
public BaseResult<UserInfoVo> userInfo() throws BaseException {
//拿到用户信息
String userId = RpcContext.getContext().getAttachment("userId");
UserInfoVo userInfoVo = getUserInfoVo(userId);
return ResultUtil.success(userInfoVo);
}
Для облегчения единого управления мы можемservice
будет получено в перехватчике слояuserId
вставить сноваthreadLocal
середина,service
Layer interceptor может взглянуть на этот твит:Dubbo Custom Log перехватчик
public class DubboServiceFilter implements Filter {
private static final Logger LOGGER = LoggerFactory.getLogger(DubboServiceFilter.class);
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
//...省略部分逻辑
//获取web层透传过来的用户参数
String userId = RpcContext.getContext().getAttachment("userId");
//放入全局threadlocal 线程共享
if (StringUtils.isNotBlank(userId)) {
ThreadLocalUtil.getInstance().bind(userId);
}
//执行业务逻辑 返回结果
Result result = invoker.invoke(invocation);
//清除 防止内存泄露
ThreadLocalUtil.getInstance().remove();
//...省略部分逻辑
return result;
}
}
Это лечение,service
Слой по-прежнему может получать информацию о пользователе с помощью следующего кода:
public BaseResult<UserInfoVo> userInfo() throws BaseException {
//拿到用户信息
UserInfo userInfo = ThreadLocalUtil.getInstance().getUserInfo();
UserInfoVo userInfoVo = getUserInfoVo(userInfo.getUserId);
return ResultUtil.success(userInfoVo);
}
Справочная документация
оjwt
:https://blog.leapoahead.com/2015/09/06/understanding-jwt/
оdubbo
:http://dubbo.apache.org/zh-cn/docs/user/demos/attachment.html
Наконец
Длина длинная, резюмируя более практичныйweb
Сценарии, последующая деятельность время от времени для обновления оригинальной статьи, приветствуем общественное внимание № "Студенты Чжан Шаолинь"!