背景
公司一旧项目权鉴改造
达到目的
任意一端登录(web、app、h5)后可以携带对应的token 来请求访问后台服务资源。
#### 传统认证流程:
互联网服务离不开用户认证。一般流程是下面这样:
- 用户向服务器发送用户名和密码
- 服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等
- 服务器向用户返回一个 session_id,写入用户的 Cookie。
- 用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。
- 服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。
这种模式的问题
- cookie存储的内容有限制4k
- cookie的有效范围是当前域名下,所以在分布式环境下或者前后端分离的项目中都不适用,即使要 用也会很麻烦。
- app端内嵌网页互相跳转。如有原生跳到h5,或者 内嵌网页跳到原生等,和服务端交互用非cookie方式,不兼容。(浏览器提供了一种叫 cookie 的机制)。
- Session数据共享问题。单机没有问题,如果网站请求流量较大,那么单台 tomcat 设备是无法
承接这些流量的,这个时候就需要开始对服务器做集群。
Session 共享问题的解决方法:
- session sticky (仅供了解)
- session sticky(粘性) , 保证同一个会话的请求都在同一个 web 服务器上处理。
- 类似nginx 负载均衡中的hash算法。(没有办法进行 4 层网络转发,只能在 7 层网络上进行解析并转发)
- session replication(仅供了解)
- session 复制,通过相关技术实现 session 复制,使得集群 中的各个服务器相互保存各自节点存储的 session 数据。
- tomcat 本身就可以实现 session 复制的功能,基于 IP 组播 放方式。
- 网络开销、带宽、内存影响大。
- session 统一存储
- 集群中的各个节点的 session 数据,统一存储到一个存储 设备中。那么每个节点去拿 session 的时候,就不是从自己 的内存中去获得,而是从相应的第三方存储中去拿。对于 这个方案来说,无论是哪个节点新增或者修改了 session 数 据,最终都会发生在这个集中存储的地方。 这个存储设备可以是 redis、mysql。
- Cookie Based 方法,简单来说,就是不依赖容器本身的 Session 机制。而是服务端基于一定的算法,生成一个 token 给到客户端, 客户端每次请求,都会携带这个 token。 当服务端收到 token 以后,先验证 token 是否有效,再解 密这个 token 获取关键数据进行处理(处理时可以存储解密出来的信息)。
Cookie Based 实现方式
基于纯 Cookie 的方式,也就是客户端每 次请求都携带身 份信息给到服务端。比较典型的方式是 JWT,全称是 JSON Web Tokens。(JWT 强调的是服务端不对 token 进行存储,而是直接通过签名算法验证并解密 token 得
到相应数据进行处理)https://jwt.io/
1、传统授权流程
2、公钥私钥授权流程
3、根据业务场景验证完token后可以继续解密出来验证用户信息。
改造代码落地
之前的代码(验证token):
/** * 用户令牌信息(验证Token) **
@author
* @date 2019-06-26
*/
@GetMapping("/user")
public Object list(Principal user, String sso_cookie)
{
if (verifyCookie) {
String ssoCookie = SpringUtil.getSsoCookie(user);
if (StringUtil.isBlank(sso_cookie) || !sso_cookie.equals(ssoCookie))
{
throw new BusinessException(ResultCode.TOKEN_INVALID);
}
}return user; }
以改为兼容客户端的代码:
1、登录时生成 jwt token 并保存到 redis(可以将唯一值 如org+userId 作为key 保存)
private LoginVo packetTokenVo(LoginVo tokenVo, LoginDto dto) {
if (StringUtils.isNotBlank(dto.getClient())) {
String orgId = dto.getOrgId() == null ? null : dto.getOrgId().toString();
int userType = dto.getUsertype() == null ? 0 : Integer.parseInt(dto.getUsertype());
JwtAccessToken jwtAccessToken = new JwtAccessToken(dto.getUserName(), orgId, dto.getIdCard(), 1, dto.getClient(), userType);
String accessTokenStr = JwtUtil.jwtAccessTokenHM256(jwtAccessToken, JwtUtil.SECRET);
JwtRefreshToken jwtRefreshToken = new JwtRefreshToken(dto.getUserName(), dto.getIdCard(), 1, dto.getClient(), userType, orgId);
String refreshTokenStr = JwtUtil.jwtRefreshTokenHM256(jwtRefreshToken, JwtUtil.SECRET);
//存到redis
cacheAccessTokenAndRefreshToken(atkey, accessTokenStr, rtkey, refreshTokenStr);
tokenVo.setAccessToken(accessTokenStr);
tokenVo.setRefreshToken(refreshTokenStr);
}
return tokenVo;
}
2、验证jwt token,直接 JwtUtil.unJwtToken(jwt) 就可以验证token,这点比较方便,如果需要再进一步验证
token 可以解密出来根据唯一值找数据,再验证一遍。
@GetMapping("/validate")
public Object validate(String jwt, String sso_cookie) {
if (verifyCookie) {
JwtToken rawJwtToken = JwtUtil.unJwtToken(jwt);
if (rawJwtToken == null || StringUtils.isEmpty(rawJwtToken.getClientId())) {
throw new BusinessException(ResultCode.TOKEN_INVALID);
}
//check(clientDetail);
String accessKeyStr = TokenKeyUtil.getAccessTokenKey(verifyJwtToken.getClientId(), verifyJwtToken.getAppType(), verifyJwtToken.getUserId(), verifyJwtToken.getOrgId());
Long validation = (Long) redisTemplate.execute(validateAccessTokenScript, Arrays.asList(accessKeyStr), jwt);
if (!accessKeyStr.equals(jwt)) {
throw new BusinessException(ResultCode.TOKEN_INVALID);
}
}
return jwt;
}
3 附,加密解密代码
//生成jwt token,可以用非对称加密
public static String jwtAccessTokenHM256(JwtAccessToken jwtAccessToken, String secret) {
try {
Algorithm algorithm = Algorithm.HMAC256(secret);
String token = JWT.create()
.withClaim("username", jwtAccessToken.getUsername()).withClaim("userId", jwtAccessToken.getUserId()).withClaim("orgId", jwtAccessToken.getOrgId()).withClaim("appType", jwtAccessToken.getAppType()).withClaim("clientId", jwtAccessToken.getClientId()).withClaim("tokenType", jwtAccessToken.getTokenType()).withClaim("userType", jwtAccessToken.getUserType()).withIssuer(ISSUER).withIssuedAt(new Date()).sign(algorithm);
return token;
} catch (Exception exception) {
logger.error("jwt处理异常:", exception);
throw new RuntimeException();
}
}
//解密jwt token
public static JwtToken unJwtToken(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
Map<String, Claim> claimMap = jwt.getClaims();
String username = getString(claimMap.get("username"));
String userId = claimMap.get("userId").asString();
String orgId = getString(claimMap.get("orgId"));
int appType = claimMap.get("appType").asInt();
String clientId = claimMap.get("clientId").asString();
String tokenType = claimMap.get("tokenType").asString();
int userType = claimMap.get("userType").asInt();
JwtToken jwtToken = null;
if (TokenType.ACCESS_TOKEN.equals(tokenType)) {
jwtToken = new JwtAccessToken(username, orgId, userId, appType, clientId, userType);
} else if (TokenType.REFRESH_TOKEN.equals(tokenType)) {
jwtToken = new JwtRefreshToken(username, userId, appType, clientId, userType, orgId);
}
return jwtToken;
} catch (Exception e) {
logger.error("jwt处理异常:", e);
throw new BusinessException("jwt解析错误");
}
}
总结:
jwt 实际上就是定义了一套数据加密以及 验签的算法的规范,根据这个规范来实现单点登录,以及数据传输及验签功能。但是这个方案不能传递敏感信息(所以比如密码一般不放在Jwt中),因为 jwt 中的部分内容可
以解 密,只是不能修改而已。