shiro分布式控制登录状态_使用Shiro和token进行无状态登录

使用Shiro和token进行无状态登录使用Shiro和token进行无状态登录

我们之前可以使用shiro实现登录,但这些都是基于session或是cookie实现的,这些只能用于单机部署的服务,或是分布式服务共享会话,显然后者开销极大,所以JWT(JSON Web Token)应运而生,JWT是一套约定好的认证协议,通过请求携带令牌来访问那些需鉴权的接口。

我们在这里使用token,原理类似,但是规则更为简单,没有形式上的约束,只是在请求Head或是body中添加token用于校验用户身份,token是可以和会话共存的,此处我们使用Shiro的会话登录结合JWT来实现无状态登录,从而实现扫码登录和一般的接口访问授权。

项目中,需要实现无状态登录(单点登录,SSO),但是同时也要保持Shiro本身自带的会话登录。

Shiro本身并不适合做基于鉴权的无状态登录,更适合它的应用场景是单体应用,这点需要注意。

有关token

此文中有关token和SSO的叙述比较粗略,只是大意阐释了流程和部分相关概念。

token一般的校验流程

长token与短token

此处采用OAuth2.0的标准,多数服务都是客户端(浏览器)与服务端后台分离的令牌,前端用于确认用户身份的是短token(通常称access token),而实际保存用户信息(包含用户基本信息、角色、权限等)的是长token(通常称refresh token),只存在后台,以短token映射长token实现用户身份和权限校验。

这里的长与短,不仅指token所存信息量的多少或是token文本的长度,还指token的有效时间,一般短token对应着会话,存活时间较短,临近过期若用户仍未登出会进行刷新。使用长短token的模式其实近似于 sessionid/uuid+内存或是nosql存储用户实体对象 的处理方案。

JWT、token

token相比之下是一个很广泛的定义,一切表示令牌的内容都可以称作是token,jwt算是token中相对很经典、适用范围很广的一套规范,它将用户访问令牌封装在Json格式的数据中,包含在每次请求中。java中相关的实现为auth0的包。

无状态登录与单点登录

前文中所述的无状态登陆与单点登录还是有所不同的,常见安全框架(例如Shiro)的默认行为是有状态登录,他在cookie中存储了sessionId,同时后台中保存了sessionId对应的用户映射,这样对于每一个sessionId后台都记录了对应的登录信息,有状态的登录缺憾是在集群环境下需要进行session共享,而且功能的实际实现一般都是集成了安全框架,对外不可见,也不便于扩展(扩展可以尝试使用Spring Security安全框架,但是相较Shiro上手门槛较高)。

而无状态登录就是本文中所说的token验证方式,相比于有状态登录信息默认存储在服务本地内存中,无状态的用户信息(token与信息映射)存储在服务公有资源中,服务的横向扩展是毫无压力的,同时也解决了服务多节点跳动的sessionId变化问题,但是需要考虑token的安全存储。

总而言之,有状态服务的特点和优缺点为:

用户信息映射实时生成,安全可靠,不必担心登录信息被复用

登录用户信息存储在服务内存中,多节点的服务必须做会话共享

安全框架集成方便,但是不便于功能的扩展

相较之下,无状态服务的特点为:

token与用户信息映射是固定的编码,要通过短token、非对称加密等方式避免用户直接获取到token的内容

token编码方式固定,可不存储登录用户token或是在公有空间(etc:redis)存储短token,不必考虑服务切换会话登录状态失效的问题

需要自己集成,但是定制程度高,业务可见性高,便于扩展

为什么需要SSO

前文中所说的此网站扫码登录需要用到SSO,凭此将用户的登录状态由手机客户端转移至浏览器中,但是这种情况下的登录状态转移是SSO的落地实现,并不能很好的解释为什么要通过SSO实现它。例如,我们也可以将用户名和密码加密后直接传入扫码网站实现登录,但这么做显然是不合时宜的,主要因为:

安全性没有保障,不使用单点登录就要求用户端留存相关密码密钥,这样一来很容易直接泄露不更改便永久有效的密码,而不是一段会过期的token;

有些使用登录中心的应用跟客户端(浏览器)并不互相信任,例如对接第三方平台(QQ、微信等)实现登陆,第三方肯定不能将用户的全部信息传回,他们反而把整个登陆过程承包了下来,返回给我们的只是用户能对外公开的基本信息,或是一段id,每次都要请求第三方再校验;

业务处理

假定的一般实现:不使用框架

假如实现登录功能不使用框架(Shiro),而只在后台使用生成jwt的相关依赖,我们需要一片能被所有服务访问的公共存储维护用户的在线信息,最佳手段是采取诸如nosql的k-v存储,此处假定采用redis实现,那么需要在redis中存储如下数据:

用户标识(id)和token(此处指长token)的映射

token和用户的信息映射

但是因为需要将用户的token存在前端,参考上文中的长短token,我们还需要存储用户到短token的映射,可将上面的用户标识直接更正为短token(或者可以使用自定义的sessionId,注意务必确保全局唯一)。

无状态登录系统的标配

无状态登录系统标配的组件和逻辑有以下几种:

一套token和用户的映射关系;

一套针对用户密码的双向加密或是单向编码算法;

一个用户鉴权中心,可以是独立的服务;

一个用户信息存储介质(一般是noSQL);

可以基于Shiro开发鉴权中心,但是实际上不是很合适。

流程设计

基于以上的思想,设计使用Shiro的单点登录,整体方案的实施需要两个主要的模块,身份认证和安全控制,本篇身份认证采用jwt,安全控制采用Shiro框架。

流程设计如下:

实战

关于基本使用请参考安全框架的使用:shiro——鱼鱼的Java小站,此处主要附Token的生成和基于Redis的存储。

编写加密算法

@Service

public class PswEncodeService {

//使用了SHA-1

public String pswEncode(String text,String salt){

return encode(text+salt,"SHA1",encodeCount(salt));

}

public boolean checkPsw(String text,String cipher,String salt){

return checkcode(text+salt,cipher,"SHA1",encodeCount(salt));

}

/**

* @param text 待加密明文

* @param key 加密方式

* @param x 递归加密次数

*  采用地柜加密更加安全

*/

public String encode(String text,String key,int x){

if (text == null) {

return null;

}

if(x>1){

try {

MessageDigest messageDigest = MessageDigest.getInstance(key);

messageDigest.update(text.getBytes());

return encode(getFormattedText(messageDigest.digest()),key,x-1);

} catch (Exception e) {

throw new RuntimeException(e);

}

}else{

return text;

}

}

public String encode(String text,String key){

return encode(text,key,1);

}

public boolean checkcode(String text,String cipher,String key,int x){

if(encode(text,key,x).equals(cipher))

return true;

else

return false;

}

/**

* 按照既定的明文编码规则获取加密迭代次数 为1000——1999次不等

* @param text key明文

*

*/

public int encodeCount(String text){

int a=1024

//可以设计不同的迭代次数

return a;

};

private static final char[] HEX = {'0', '1', '2', '3', '4', '5',

'6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};

private static String getFormattedText(byte[] bytes) {

int len = bytes.length;

StringBuilder buf = new StringBuilder(len * 2);

// 把密文转换成十六进制的字符串形式

for (int j = 0; j 

buf.append(HEX[(bytes[j] >> 4) & 0x0f]);

buf.append(HEX[bytes[j] & 0x0f]);

}

return buf.toString();

}

}

Token生成与校验、存储

token的生成方法与校验,首先引入依赖:

com.auth0

java-jwt

3.4.1

然后编写token类:

@Service

public class TokenService {

// 过期时间60分钟

private static final long EXPIRE_TIME = 60*60*1000;

@Autowired

private PswService pswService;

@Autowired

LoginLogMapper loginLogMapper;

private Logger logger = LoggerFactory.getLogger(TokenService.class);

/**

* 校验token是否正确

* @param token 密钥

* @param secret 用户的密码

* @return 是否正确

*/

public boolean verify(String token, String username, String secret) {

try {

Algorithm algorithm = Algorithm.HMAC256(pswEncodeFacade.pswEncode(secret,username));

JWTVerifier verifier = JWT.require(algorithm)

.withClaim("username", username)

.build();

DecodedJWT jwt = verifier.verify(token);

return true;

} catch (Exception exception) {

return false;

}

}

/**

* 获得token中的信息无需secret解密也能获得

* @return token中包含的用户名

*/

public String getUsername(String token) {

try {

DecodedJWT jwt = JWT.decode(token);

return jwt.getClaim("username").asString();

} catch (JWTDecodeException e) {

return null;

}

}

public String getUserId(String token) {

try {

DecodedJWT jwt = JWT.decode(token);

return jwt.getClaim("id").asString();

} catch (JWTDecodeException e) {

return null;

}

}

/**

* 生成签名,60min后过期

* @param username 用户名

* @param secret 用户的密码

* @return 超级加密的token

*/

public String sign(String username, String secret,String id) {

Date date = new Date(System.currentTimeMillis()+EXPIRE_TIME);

Algorithm algorithm = Algorithm.HMAC256(pswEncodeFacade.pswEncode(secret,username));

// 附带username信息,也可在token中直接标示更多信息

return JWT.create()

.withClaim("username", username)

.withClaim("id",id)

.withExpiresAt(date)

.sign(algorithm);

}

}

基于Redis存储Shiro会话

编写cacheManager:

@Component

public class RedisCacheManager4Shiro implements CacheManager {

@Resource

RedisTemplate redisTemplate;

private static final Logger logger = LoggerFactory.getLogger(RedisCacheManager4Shiro.class);

//@Resource RedisUtil redisUtil;

@Override

public  Cache getCache(String arg0) throws CacheException {

return new RedisCache();

}

class RedisCache implements Cache{

public RedisCache() {

redisTemplate.boundHashOps(CACHE_KEY).expire(180, TimeUnit.MINUTES);

}

//Cache 前缀

private static final String CACHE_KEY = "shiro_redis_subject";

@Override

public void clear() throws CacheException {

redisTemplate.delete(CACHE_KEY);

}

private String toString(Object obj){

if(obj instanceof String){

return obj.toString();

}else{

return JSONObject.toJSONString(obj);

}

}

@SuppressWarnings("unchecked")

@Override

public V get(K k) throws CacheException {

logger.info("get field:{}", toString(k));

return (V)redisTemplate.boundHashOps(CACHE_KEY).get(k);

}

@SuppressWarnings("unchecked")

@Override

public Set keys() {

logger.info("keys");

return (Set)redisTemplate.boundHashOps(CACHE_KEY).keys();

}

@Override

public V put(K k, V v) throws CacheException {

logger.info("put field:{}, value:{}", toString(k), toString(v));

redisTemplate.boundHashOps(CACHE_KEY).put(k, v);

return v;

}

@Override

public V remove(K k) throws CacheException {

logger.info("remove field:{}", toString(k));

V v = get(k);

redisTemplate.boundHashOps(CACHE_KEY).delete(k);

return v;

}

@Override

public int size() {

int size = redisTemplate.boundHashOps(CACHE_KEY).size().intValue();

logger.info("size:{}", size);

return size;

}

@SuppressWarnings("unchecked")

@Override

public Collection values() {

logger.info("values");

return (Collection)redisTemplate.boundHashOps(CACHE_KEY).values();

}

public String getCacheKey() {

return "RedisCache";

}

}

}

然后在我们配置时指定:

//配置核心安全事务管理器

@Bean(name = "securityManager")

public SecurityManager securityManager(//@Qualifier("sessionManager")WebSessionManager webSessionManager,

@Qualifier("authRealm") AuthRealm authRealm,

@Qualifier("redisCacheManager4Shiro") RedisCacheManager4Shiro redisCacheManager4Shiro) {

DefaultWebSecurityManager manager = new DefaultWebSecurityManager();

manager.setCacheManager(redisCacheManager4Shiro);

manager.setSessionManager(redisSessionManager);

manager.setRealm(authRealm);

return manager;

}

2020-03-22鱼鱼

你可能感兴趣的:(shiro分布式控制登录状态)