Skip to content

J、无状态鉴权

wj596 edited this page Jan 2, 2018 · 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鉴权相关配置属性:

#是否启用HMAC鉴权,不配置默认不启用
jsets.shiro.hmac-enabled=true
#HMAC签名算法,不配置默认HmacMD5,hmac-enabled=true时此项有用
#jsets.shiro.hmac-alg=HmacMD5
#HMAC签名全局秘钥,hmac-enabled=true时此项有用
jsets.shiro.hmac-secret-key=ofaffadfev1234567--090swctewst
#HMAC签名有效期,不配置默认1分钟,hmac-enabled=true时此项有用
#jsets.shiro.hmac-period=60000

HMAC URL过滤规则(过滤器)配置:

#匹配此路径的URL,需要通过hmac认证并具有admin角色
jsets.shiro.filte-rules[3]=/restApi/delete*-->hmacRoles[admin]
#匹配此路径的URL,需要通过hmac认证
jsets.shiro.filte-rules[4]=/restApi/**-->hmac

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 URL鉴权配置属性:

#是否启用JWT鉴权,不配置默认不启用
jsets.shiro.jwt-enabled=true
#JWT签名签名全局秘钥,jwt-enabled=true时此项有用
jsets.shiro.jwt-secret-key=ofaffadfev1234567--090swctewst

URL过滤规则(过滤器)配置:

#匹配此路径的URL,需要通过jwt认证并具有admin角色
jsets.shiro.filte-rules[5]=/restApi2/delete*-->jwtRoles[admin]
#匹配此路径的URL,需要通过jwt认证
jsets.shiro.filte-rules[6]=/restApi2/**-->jwt

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 hmac-period属性的配置(默认是60秒),计算出这个时间戳距离验签时间是否在这个范围内,如果不在就视为失效。JWT的荷载信息中有个exp属性,这个就是JWT的有效期,同样在验签时如果过了有效期也视为令牌失效。

基于缓存的方式就是每次认证完成后将摘要放入缓存,再有请求到来先到缓存中查看是否存在相同的摘要,如果存在则不予认证通过,就是我们通常所说的阅后即焚。这种处理的方式显然比第一种要安全多了,但是对缓存的负载会加大。如果不是对安全性有较高的要求还是推荐使用第一种。

无状态鉴权的账号数据:

在"接入用户数据"一节我们用ShiroAccountProvider账号数据提供者接口为有状态鉴权提供账号数据,无状态鉴权默认也是使用这个接口来获取鉴权数据。但是如果您想要无状态鉴权使用独立的账号数据也是可以的,不排除会有这样的需求,比如一个系统即是管理系统同时也行第三方系统提供服务,可能登陆管理系统需要一套账号体系,为第三方系统提供服务需要另外一套账号体系,另外我们在配置文件中的属性hmac-secret-key和jwt-secret-key都是全局秘钥,如果您想要每个无状态鉴权账号都使用自己独立的秘钥,也需要实现这个接口。

实现ShiroStatelessAccountProvider:

@Service
public class StatelessAccountProviderImpl implements ShiroStatelessAccountProvider{
	
        // API客户端管理
	@Autowired
	private ApiClientService apiClientService;
	
	/**
	 * 检查账号是否正常
	 * 如果返回false或抛出AuthenticationException则说明账号异常,不予通过认证。
	 */
	public boolean checkAccount(String appId) throws AuthenticationException{
		return apiClientService.isLocked(appId);
	}
	/**
	 * 获取客户端的签名私钥,如果客户端没有自己的秘钥返回空,则使用全局秘钥
	 */
	public String loadAppKey(String appId){
		return apiClientService.getAppKey(appId);
	}
	/**
	 * 根据客户标识加载持有角色
	 */
	public Set<String> loadRoles(String appId){
		return apiClientService.listRoles(appId);
	}
	/**
	 * 根据客户标识加载持有权限
	 */
	public Set<String> loadPermissions(String appId){
		return apiClientService.listPermissions(appId);
	}	
}

修改配置适配器DemoShiroConfiguration:

@Configuration
public class DemoShiroConfiguration extends JsetsShiroConfigurationAdapter{
    // 账号信息提供者实现
    @Autowired
    private ShiroAccountProviderImpl shiroAccountProvider;
    // 无状态鉴权账号信息提供者实现
    @Autowired
    private StatelessAccountProviderImpl statelessAccountProvider;
    // 密码错误次数超限处理器实现
    @Autowired
    private PasswdRetryLimitHandlerImpl passwdRetryLimitHandler;
    @Override
    protected void configure(SecurityManagerConfig securityManager) {
        // 设置账号信息提供者实现
        securityManager.setAccountProvider(this.shiroAccountProvider);
        // 设置密码错误次数超限处理器实现
        securityManager.setPasswdRetryLimitHandler(this.passwdRetryLimitHandler);
	// 无状态鉴权账号信息提供者实现,如果不设置此项无状态鉴权默认使用this.shiroAccountProvider
	securityManager.setStatelessAccountProvider(statelessAccountProvider);
    }
    @Override
    protected void configure(FilterChainConfig filterChain) {}
}

这样设置之后,有状态鉴权就使用shiroAccountProvider来获取账号数据,无状态鉴权就使用statelessAccountProvider来获取账号数据,如果不进行setStatelessAccountProvider操作,则有状态鉴权和无状态鉴权都使用shiroAccountProvider获取账号数据。

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

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

HMAC鉴权推荐使用场景

推荐使用JWT鉴权的场景:

JWT鉴权推荐场景

Clone this wiki locally