很久没写更新内容了,新的一年也开始了,是时候该把自己的东西整理一遍了。2018年也没少看书,但是真正属于自己的东西很少很少,或者学习的时候浅尝辄止,也是时候给自己清醒清醒了。
公司自己的项目是基于Spring Boot敏捷开发的,起初对于接口的鉴权等认证操作都很粗糙,网上也搜集了一下其他资料,总的来说。比较详细的鉴权的两种方式如下:
其一是认证与鉴权,对于请求的用户身份的授权以及合法性鉴权;其二是API级别的操作权限控制,这个在第一点之后,当鉴定完用户身份合法之后,对于该用户的某个具体请求是否具有该操作执行权限进行校验。
认证与鉴权
对于第一个需求,笔者调查了一些实现方案:
-
分布式
Session
方案
分布式会话方案原理主要是将关于用户认证的信息存储在共享存储中,且通常由用户会话作为 key 来实现的简单分布式哈希映射。当用户访问微服务时,用户数据可以从共享存储中获取。在某些场景下,这种方案很不错,用户登录状态是不透明的。同时也是一个高可用且可扩展的解决方案。这种方案的缺点在于共享存储需要一定保护机制,因此需要通过安全链接来访问,这时解决方案的实现就通常具有相当高的复杂性了。 -
基于
OAuth2 Token
方案
随着 Restful API、微服务的兴起,基于Token
的认证现在已经越来越普遍。Token和Session ID 不同,并非只是一个 key。Token 一般会包含用户的相关信息,通过验证 Token 就可以完成身份校验。用户输入登录信息,发送到身份认证服务进行认证。AuthorizationServer验证登录信息是否正确,返回用户基础信息、权限范围、有效时间等信息,客户端存储接口。用户将 Token 放在 HTTP 请求头中,发起相关 API 调用。被调用的微服务,验证Token
。ResourceServer返回相关资源和数据。
这边选用了第二种方案,基于OAuth2 Token
认证的好处如下:
- 服务端无状态:Token 机制在服务端不需要存储 session 信息,因为 Token 自身包含了所有用户的相关信息。
- 性能较好,因为在验证 Token 时不用再去访问数据库或者远程服务进行权限校验,自然可以提升不少性能。
- 现在很多应用都是同时面向移动端和web端,
OAuth2 Token
机制可以支持移动设备。 - OAuth2与Spring Security结合使用,有提供很多开箱即用的功能,大多特性都可以通过配置灵活的变更。
- 最后一点,也很重要,Spring Security OAuth2的文档写得较为详细。
oauth2根据使用场景不同,分成了4种模式:
- 授权码模式(authorization code)
- 简化模式(implicit)
- 密码模式(resource owner password credentials)
- 客户端模式(client credentials)
对于上述oauth2四种模式不熟的同学,可以自行百度oauth2,阮一峰的文章有解释。常使用的是password模式和client模式。
操作权限控制
对于第二个需求,笔者主要看了Spring Security和Shiro。
-
Shiro
Shiro是一个强大而灵活的开源安全框架,能够非常清晰的处理认证、授权、管理会话以及密码加密。Shiro很容易入手,上手快控制粒度可糙可细。自由度高,Shiro既能配合Spring使用也可以单独使用。 -
Spring Security
Spring社区生态很强大。除了不能脱离Spring,Spring Security具有Shiro所有的功能。而且Spring Security对Oauth、OpenID也有支持,Shiro则需要自己手动实现。Spring Security的权限细粒度更高。但是Spring Security太过复杂。
看了下网上的评论,貌似一边倒向Shiro。大部分人提出的Spring Security
问题就是比较复杂难懂,文档太长。不管是Shiro
还是Spring Security
,其实现都是基于过滤器,对于自定义实现过滤器,我想对于很多开发者并不是很难,但是这需要团队花费时间与封装可用的jar包出来,对于后期维护和升级,以及功能的扩展。很多中小型公司并不一定具有这样的时间和人力投入这件事。笔者综合评估了下复杂性与所要实现的权限需求,以及上一个需求调研的结果,既然Spring Security
功能足够强大且稳定,最终选择了Spring Security
。
废话少说,先来一波基于JWT实现的TOKEN的方式:
package com.whb.web.utils;
import java.text.ParseException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSObject;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.Payload;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jose.crypto.MACVerifier;
import net.minidev.json.JSONObject;
public class JwtUtils {
/**
* 1.创建一个32-byte的密匙
*/
private static final byte[] secret = "geiwodiangasfdjsikolkjikolkijswe".getBytes();
// 生成一个token
public static String creatToken(Map<String, Object> payloadMap) throws JOSEException {
// 3.先建立一个头部Header
/**
* JWSHeader参数:1.加密算法法则,2.类型,3.。。。。。。。 一般只需要传入加密算法法则就可以。 这里则采用HS256
*
* JWSAlgorithm类里面有所有的加密算法法则,直接调用。
*/
JWSHeader jwsHeader = new JWSHeader(JWSAlgorithm.HS256);
// 建立一个载荷Payload
Payload payload = new Payload(new JSONObject(payloadMap));
// 将头部和载荷结合在一起
JWSObject jwsObject = new JWSObject(jwsHeader, payload);
// 建立一个密匙
JWSSigner jwsSigner = new MACSigner(secret);
// 签名
jwsObject.sign(jwsSigner);
// 生成token
return jwsObject.serialize();
}
/**
* 解析一个token
*
* @param token
* @return
* @throws ParseException
* @throws JOSEException
* @throws java.text.ParseException
*/
public static Map<String, Object> valid(String token) throws ParseException, JOSEException {
// 解析token
JWSObject jwsObject = JWSObject.parse(token);
// 获取到载荷
Payload payload = jwsObject.getPayload();
// 建立一个解锁密匙
JWSVerifier jwsVerifier = new MACVerifier(secret);
Map<String, Object> resultMap = new HashMap<>();
// 判断token
if (jwsObject.verify(jwsVerifier)) {
resultMap.put("Result", 0);
// 载荷的数据解析成json对象。
JSONObject jsonObject = payload.toJSONObject();
resultMap.put("data", jsonObject);
// 判断token是否过期
if (jsonObject.containsKey("exp")) {
Long expTime = Long.valueOf(jsonObject.get("exp").toString());
Long nowTime = new Date().getTime();
// 判断是否过期
if (nowTime > expTime) {
// 已经过期
resultMap.clear();
resultMap.put("Result", 2);
}
}
} else {
resultMap.put("Result", 1);
}
return resultMap;
}
//生成token的业务逻辑
public static String TokenTest(String uid) {
//获取生成token
Map<String, Object> map = new HashMap<>();
//建立载荷,这些数据根据业务,自己定义。
map.put("uid", uid);
//生成时间
map.put("sta", new Date().getTime());
//过期时间
map.put("exp", new Date().getTime()+6);
try {
String token = JwtUtils.creatToken(map);
System.out.println("token="+token);
return token;
} catch (JOSEException e) {
System.out.println("生成token失败");
e.printStackTrace();
}
return null;
}
//处理解析的业务逻辑
public static void ValidToken(String token) {
//解析token
try {
if (token != null) {
Map<String, Object> validMap = JwtUtils.valid(token);
int i = (int) validMap.get("Result");
if (i == 0) {
System.out.println("token解析成功");
JSONObject jsonObject = (JSONObject) validMap.get("data");
System.out.println("uid是" + jsonObject.get("uid"));
System.out.println("sta是"+jsonObject.get("sta"));
System.out.println("exp是"+jsonObject.get("exp"));
} else if (i == 2) {
System.out.println("token已经过期");
}
}
} catch (ParseException e) {
e.printStackTrace();
} catch (JOSEException e) {
e.printStackTrace();
}
}
public static void main(String[] ages) {
//获取token
String uid = "kkksuejrmf";
String token = TokenTest(uid);
//解析token
ValidToken(token);
}
}