Skip to content

J、无状态鉴权

wangjie edited this page Dec 25, 2019 · 15 revisions

无状态(Stateless)鉴权通常应用在微服务(REST API)架构中,使用数字摘要(签名)技术生成一个token作为认证和授权的凭证,整个认证和授权过程不依赖于cookie或session,服务端不保留客户端状态因此每次请求都要携带这个token。

jsets-shiro-spring-boot-starter提供两种无状态鉴权方式,分别是散列消息认证码(HMAC)、JSON WEB TOKEN(JWT)。

HMAC鉴权

HMAC运算利用哈希算法,以一个密钥和一个消息为输入,生成一个消息摘要作为输出,使用HMAC作为rest api的安全验证协议最显著的两个特点就是防止密码在网络上传递和防止请求信息被篡改。

HMAC请求流程:

HMAC认证请求流程

服务端接收到请求,也按照和请求发送时一样的摘要生成方式和密码生成服务端摘要,如果请求的摘要和服务端摘要不同则说明是非法请求。

HMAC鉴权相关配置属性:

#jsets-shiro配置
jsets:
  shiro:
    #是否启用HMAC鉴权,不配置默认不启用
    hmac-enabled: true 
    #是否启用HMAC签名即时销毁,确保一个HMAC签名只能使用一次,不配置默认不启用
    hmac-burn-enabled: true 
    #HMAC签名算法,不配置默认HmacMD5,hmac-enabled=true时此项有用
    hmac-alg: HmacMD5 
    #HMAC签名全局秘钥,hmac-enabled=true时此项有用
    hmac-secret-key: ofaffadfev1234567--090swctewst 
    #HMAC签名有效期(单位为毫秒),不配置默认1分钟,hmac-enabled=true时此项有用
    hmac-period: 60000 

HMAC鉴权规则(过滤器)静态配置示例:

#匹配'/restApi/delete*'的路径的,需要通过hmac认证并且用户具有admin角色
/restApi/delete*-->hmacRoles[admin]
#匹配'/restApi/**'的路径,需要通过hmac认证
/restApi/**-->hmac

实际项目中通常使用ShiroFilteRulesProvider接口提供,参见"鉴权规则"一节。

JWT鉴权:

JWT(json web token)是一个轻量级开放标准,也是使用HASH算法进行摘要,生成token中包含了头信息和荷载信息。JWT是一个自包含的令牌,即在荷载信息中包含用户鉴权所需所有信息(用户名、角色、权限等等),只需要对token本身进行验签,验签过程中不需要通过数据库查询用户信息。 JWT荷载信息:

Playload//荷载信息
{
    "iss": "token-server",//签发者
    "exp ": "Mon Nov 13 15:28:41 CST 2017",//过期时间
    "sub ": "wangjie",//用户名
    "aud": "web-server-1"//接收方,
    "nbf": "Mon Nov 13 15:40:12 CST 2017",/生效时间
    "jat": "Mon Nov 13 15:20:41 CST 2017",//签发时间
    "jti": "0023",//令牌ID标识
    "claim": {"auth":"ROLE_ADMIN"}//访问主张
}

JWT鉴权相关配置属性:

#jsets-shiro配置
jsets:
  shiro:
    #是否启用JWT鉴权,不配置默认不启用
    jwt-enabled: true 
    #是否启用JWT令牌即时销毁,确保JWT令牌只能只用一次,不配置默认不启用
    #jwt-burn-enabled: true 
    #JWT签名签名全局秘钥,jwt-enabled=true时此项有用
    jwt-secret-key: ofaffadfev1234567--090swctewst 

JWT鉴权规则(过滤器)静态配置示例:

#匹配'/restApi2/delete*'的路径的,需要通过JWT认证并且用户具有admin角色
/restApi2/delete*-->jwtRoles[admin]
#匹配'/restApi2/**-->jwt'的路径,需要通过jwt认证
/restApi2/**-->jwt-->jwt

实际项目中通常使用ShiroFilteRulesProvider接口提供,参见"鉴权规则"一节。

HMAC摘要和JWT令牌生成工具:

CryptoUtil

如果您要在客户端生成HMAC摘要,使用这个工具类中的hmacDigest(String plaintext,String secretKey,String algName)方法即可,其中algName属性为算法名称,可选的常量如下:

// HMAC 加密算法名称
public static final String HMAC_MD5 = "HmacMD5";// 128位
public static final String HMAC_SHA1 = "HmacSHA1";// 126位
public static final String HMAC_SHA256 = "HmacSHA256";// 256位
public static final String HMAC_SHA512 = "HmacSHA512";// 512位

请保证客户端的秘钥、加密算法和服务端一致。

如果您要在客户端生成JWT令牌,需先在客户端引入jjwt包:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

然后使用这个工具类中的issueJwt()方法,进行令牌签发,请保证秘钥和验证端一致。

无状态鉴权的响应状态:

在"ajax响应"一节中我们用401状态响应未登陆,用403状态响应未授权,无状态鉴权的响应状态和JSON消息与其一致。

重放攻击:

所谓重放攻击是指攻击者发送一个目的主机已接收过的包,来达到欺骗系统的目的,主要用于身份认证过程,破坏认证的正确。我们主要是通过时间戳和缓存两种方式来处理。

HMAC签名由于在生成摘要时混入了时间戳(毫秒精度)即使请求的内容一样,每次生成的摘要内容也不一样,所以具备了一次性消费的特性。验证端在验签时会根据上面hmac-period属性的配置(默认是60秒)计算出这个签名生成的时间距离验签时间是否在这个范围内,如果不在就视为失效。JWT的荷载信息中有个exp属性,这个是JWT的有效期,同样在验签时如果过了有效期也视为令牌失效。

基于缓存的方式就是每次验证完成后将签名或令牌ID放入缓存,下次验证时先到缓存中查看是否存在相同的签名或令牌ID,如果存在则视签名和令牌为作废的,不管HMAC和JWT设置的有效期是多少,每个签名或令牌只能使用一次,就是我们通常所说的阅后即焚。您可以在配置文件中启用:

#jsets-shiro配置
jsets:
  shiro:
    #是否启用HMAC签名即时销毁,确保一个HMAC签名只能使用一次,不配置默认不启用
    hmac-burn-enabled: true 
    #是否启用JWT令牌即时销毁,确保JWT令牌只能只用一次,不配置默认不启用
    #jwt-burn-enabled: true  

这种处理的方式显然时间戳的方式安全多少,但是对存储的负载比较大,所以只有您使用的缓存是ehcache或者redis时启用burn-enabled才有效,如果不是对安全性有较高的要求还是推荐使用时间戳来防止重放攻击。

无状态鉴权的账号数据:

在"接入用户数据"一节我们用ShiroAccountProvider接口为系统提供账号数据。

不排除有这样的需求,一个系统即是管理系统,同时也为第三方系统提供服务。管理系统使用MVC模式基于Seesion进行鉴权,为第三方系统提供服务则使用JWT(HMAC)进行无状态鉴权,提供REST服务,使用两套用户体系。

可以使用ShiroAccountProvider为MVC(session)鉴权提供用户数据;使用ShiroStatelessAccountProvider为Rest(HMAC\JWT)鉴权提供用户数据。

ShiroStatelessAccountProvider示例,如下:

@Service
public class StatelessAccountProviderImpl implements ShiroStatelessAccountProvider {
	
	/**
	 * 认证账号
	 * 如果返回false或抛出AuthenticationException则说明账号异常,不予通过认证。
	 */
	@Override
	public boolean checkAccount(String account) throws AuthenticationException {
		return false;
	}

	/**
	 * 获取account的专有签名私钥
         * 如果您要使用全局秘钥,即'application.yml'中配置的hmac-secret-key或jwt-secret-key,此方法返回null即可。
         * 如果您要使用account的专有签名私钥,将'application.yml'中配置的hmac-secret-key或jwt-secret-key属性注释掉即可。
	 */
	@Override
	public String loadAppKey(String account) {
		return null;
	}

	/**
	 * 根据账号加载其角色列表
	 */
	@Override
	public Set<String> loadRoles(String account) {
		return null;
	}

	@Override
	public Set<String> loadPermissions(String appId) {
		return null;
	}
}

配置ShiroCustomizer:

@Configuration
public class ApplicationConfig{
	// 账号数据提供服务
	@Autowired
	private AccountProviderImpl accountProviderImpl;
	// 自定义的加密实现
	//@Autowired
	//private MyDESPasswordProvider myDESPasswordProvider;
	// 自定义的验证码实现
	@Autowired
	private MyCaptchaService myCaptchaService;
	// 密码输入错误次数超限处理器
	@Autowired
	private PasswdRetryLimitHandler passwdRetryLimitHandler;
	// 鉴权规则数据提供服务
	@Autowired
	private FilteRulesProviderImpl filteRulesProviderImpl;
	// 无状态鉴权(HMAC\JWT)专用的账号数据提供服务
	@Autowired
	private StatelessAccountProviderImpl statelessAccountProviderImpl;
	
	@Bean
	public ShiroCustomizer shiroCustomizer() {
		ShiroCustomizer customizer = new ShiroCustomizer();
		// 设置账号数据提供服务
		customizer.setShiroAccountProvider(accountProviderImpl); 
		// 设置加密实现
		//customizer.setPasswordProvider(myDESPasswordProvider);
		// 设置验证码实现
		customizer.setCaptchaProvider(myCaptchaService);
		// 设置密码输入错误次数超限处理器
		customizer.setPasswdRetryLimitListener(passwdRetryLimitHandler);
		// 设置鉴权规则数据提供服务
		customizer.setShiroFilteRulesProvider(filteRulesProviderImpl);
		// 设置无状态鉴权(HMAC\JWT)专用的账号数据提供服务
		customizer.setShiroStatelessAccountProvider(statelessAccountProviderImpl);
		return customizer;
	}
}

HMAC摘要和JWT令牌使用使用场景:

推荐使用HMAC进行鉴权场景:

HMAC鉴权推荐使用场景

推荐使用JWT鉴权的场景:

JWT鉴权推荐场景