单用户并发访问的问题
当用户AccessToken失效,用户使用该失效的AccessToken同时发起多个请求,会产生多AccessToken和RefreshToken认证失败问题;
多AccessToken的问题:
如果多个请求分别请求成功,则每个请求都会生成一个新的AccessToken,但前面生成的AccessToken都会被后面生成的AccessToken覆盖,导致前面的AccessToken失效,客户端会很难判断下一个请求时应该使用哪个AccessToken。须在短时间内所有的用户请求,都返回同一个AccessToken;
解决方案:10秒内的用户请求都返回第一个生成的AccessToken,这段逻辑必须要保证原子性;
单机版 synchronized (userName.intern()) {
Boolean success = this.redisTemplate.opsForValue().setIfAbsent(accessTokenKey, accessToken, 10, TimeUnit.SECONDS);
//**如果缓存新的accessToken成功,则缓存新的refreshToken
if (success != null && success) {
this.redisTemplate.opsForValue().set(refreshTokenKey, refreshToken, refreshTokenExpireTime, TimeUnit.MINUTES);
} else { //**否则,返回缓存的accessToken
accessToken = this.redisTemplate.opsForValue().get(accessTokenKey) + "";
}
}
分布式版 String script = "if 1 == redis.call('setnx', KEYS[1], ARGV[1]) then " +
" redis.call('expire', KEYS[1], ARGV[2]) " +
" redis.call('set', KEYS[2], ARGV[3]) " +
" redis.call('expire', KEYS[2], ARGV[4]) " +
" return ARGV[1] " +
"else " +
" return redis.call('get', KEYS[1]) " +
"end";
DefaultRedisScript<String> sc = new DefaultRedisScript<>(script);
sc.setResultType(String.class);
accessToken = redisTemplate.execute(sc, Arrays.asList(accessTokenKey, refreshTokenKey), accessToken, 10, refreshToken, refreshTokenExpireTime * 60);
认证失败的问题:
第一个请求认证失败,重新生成了RefreshToken,并把新RefreshToken保存到了redis中,第二个请求的RefreshToken还是上一次的,RefreshToken就会不相同,导致认证失败;
解决方案:10秒内的验证成功过的AccessToken,直接返回验证成功,不需要重新验证;
public boolean verify(String token) {
String key = this.getKey(JwtUtil.getUserName(token));
//**使用访问令牌的md5,只是为了存储到redis时短一点而已
String tokenMd5 = DigestUtils.md5DigestAsHex(token.getBytes());
Boolean hasKey = this.redisTemplate.hasKey(key);
if(hasKey == null || !hasKey)
throw new TokenExpiredException("The Token not existed or expired.");
long refreshToken = (long)this.redisTemplate.opsForValue().get(key);
if(refreshToken != JwtUtil.getSignTimeMillis(token)){
//**访问令牌如果在10秒内验证过,则返回验证成功
Boolean flag = this.redisTemplate.hasKey(tokenMd5);
if(flag != null && flag) return true;
throw new TokenExpiredException("The Token has expired.");
}
try {
return JwtUtil.verify(token, secret);
} catch (TokenExpiredException e){
//**访问令牌过期,但刷新令牌有效时,缓存访问令牌10秒
logger.debug("Cache tokenMd5={}", tokenMd5);
this.redisTemplate.opsForValue().set(tokenMd5, null, 10, TimeUnit.SECONDS);
throw new TokenExpiredException("The access token has expired.");
}
}
pings-shiro-jwt
简介
pings-shiro-jwt是基于jwt和shiro的无状态权限认证工具,可实现如下两种认证方式:
access token无状态权限认证
原理优点
实现简单不外部依赖运行环境如果多个系统之间用户信息和配置的secret相同,某个系统签发的token即可访问所有其它的任意系统 问题
如果token过期时间太短,则每次到期后,都需要用户重新登录如果token过期时间太长,由于token签发后,在有效期内无法注销,存在安全隐患 结合refresh token和access token的无状态权限认证
原理优点
安全性好,可以像session一样管理用户如果多个系统之间用户信息和配置的secret相同,某个系统签发的token即可访问所有其它的任意系统 问题
依赖redis存储refresh token,实现多个系统之间的refresh token共享
pings-shiro-jwt地址
示例:dubbo微服务脚手架
使用
1.配置
1).使用access token方式
application.yml
# 系统管理 config
sys:
jwt:
secret: ==SFddfenfV2FuZzkyNjQ1NGRTQkFQSUpXVA==
# 访问令牌过期时长(分钟),默认配置600分钟
access-token:
expire-time: 300
ShiroConfig.java
/**
*********************************************************
** @desc : Shiro配置
** @author Pings
** @date 2019/1/23
** @version v1.0
* *******************************************************
*/
@Configuration
public class ShiroConfig {
//**访问令牌过期时间(分钟)
@Value("${sys.jwt.access-token.expire-time}")
private long accessTokenExpireTime;
@Value("${sys.jwt.secret}")
private String secret;
@Reference(version = "${sys.service.version}")
private UserService userService;
@Bean
public JwtVerifier verifier(RedisTemplate<String, Object> redisTemplate){
return new AccessTokenJwtVerifier(secret, accessTokenExpireTime);
}
@Bean
public JwtRealm jwtRealm(JwtVerifier verifier){
return new JwtRealm(this.userService, verifier);
}
@Bean
@Scope("prototype")
public JwtFilter jwtFilter(JwtVerifier verifier){
return new JwtFilter(verifier);
}
@Bean("securityManager")
public DefaultWebSecurityManager securityManager(JwtRealm jwtRealm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
//**使用自定义JwtRealm
manager.setRealm(jwtRealm);
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
manager.setSubjectDAO(subjectDAO);
return manager;
}
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager, JwtFilter jwtFilter) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
//**添加自定义过滤器jwt
Map<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("jwt", jwtFilter);
factoryBean.setFilters(filterMap);
factoryBean.setSecurityManager(securityManager);
//**自定义url规则
Map<String, String> filterRuleMap = new LinkedHashMap<>();
//不拦截请求swagger-ui页面请求
filterRuleMap.put("/webjars/**", "anon");
//jwt过滤器拦截请求
filterRuleMap.put("/**", "jwt");
factoryBean.setFilterChainDefinitionMap(filterRuleMap);
return factoryBean;
}
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
2).结合refresh token和access token的方式
application.yml
# 系统管理 config
sys:
jwt:
secret: ==SFddfenfV2FuZzkyNjQ1NGRTQkFQSUpXVA==
# 访问令牌过期时长(分钟),默认配置5分钟
access-token:
expire-time: 3
# 刷新令牌过期时长(分钟),默认配置60分钟
refresh-token:
expire-time: 5
ShiroConfig.java
/**
*********************************************************
** @desc : Shiro配置
** @author Pings
** @date 2019/1/23
** @version v1.0
* *******************************************************
*/
@Configuration
public class ShiroConfig {
//**访问令牌过期时间(分钟)
@Value("${sys.jwt.access-token.expire-time}")
private long accessTokenExpireTime;
//**刷新信息过期时间(分钟)
@Value("${sys.jwt.refresh-token.expire-time}")
private long refreshTokenExpireTime;
//**密钥
@Value("${sys.jwt.secret}")
private String secret;
@Reference(version = "${sys.service.version}")
private UserService userService;
@Bean
public JwtVerifier verifier(RedisTemplate<String, Object> redisTemplate){
return RefreshTokenJwtVerifier.Builder.newBuilder(redisTemplate)
.accessTokenExpireTime(accessTokenExpireTime)
.refreshTokenExpireTime(refreshTokenExpireTime)
.secret(secret)
.build();
}
@Bean
public JwtRealm jwtRealm(JwtVerifier verifier){
return new JwtRealm(this.userService, verifier);
}
@Bean
@Scope("prototype")
public JwtFilter jwtFilter(JwtVerifier verifier){
return new JwtFilter(verifier);
}
@Bean("securityManager")
public DefaultWebSecurityManager securityManager(JwtRealm jwtRealm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
//**使用自定义JwtRealm
manager.setRealm(jwtRealm);
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
manager.setSubjectDAO(subjectDAO);
return manager;
}
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager, JwtFilter jwtFilter) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
//**添加自定义过滤器jwt
Map<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("jwt", jwtFilter);
factoryBean.setFilters(filterMap);
factoryBean.setSecurityManager(securityManager);
//**自定义url规则
Map<String, String> filterRuleMap = new LinkedHashMap<>();
//不拦截请求swagger-ui页面请求
filterRuleMap.put("/webjars/**", "anon");
//jwt过滤器拦截请求
filterRuleMap.put("/**", "jwt");
factoryBean.setFilterChainDefinitionMap(filterRuleMap);
return factoryBean;
}
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
2.自定义shiro realm
/**
*********************************************************
** @desc : jwt realm
** @author Pings
** @date 2019/5/10
** @version v1.0
* *******************************************************
*/
public class JwtRealm extends AbstractJwtRealm {
protected UserService userService;
public JwtRealm(UserService userService, JwtVerifier verifier){
super(verifier);
this.userService = userService;
}
/**权限验证*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
String userName = verifier.getUserName(principals.toString());
//**获取用户
User user = this.userService.getByUserName(userName);
//**用户角色
Set<String> roles = user.getRoles().stream().map(Role::getCode).collect(toSet());
authorizationInfo.addRoles(roles);
//**用户权限
Set<String> rights = user.getRoles().stream().map(Role::getRights).flatMap(List::stream).map(Right::getCode).collect(toSet());
authorizationInfo.addStringPermissions(rights);
return authorizationInfo;
}
/**登录验证*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
String token = (String) auth.getCredentials();
//**获取用户名称
String userName = verifier.getUserName(token);
//**用户名称为空
if (StringUtils.isBlank(userName)) {
throw new UnknownAccountException("The account in Token is empty.");
}
//**获取用户
User user = this.userService.getByUserName(userName);
if (user == null) {
throw new UnknownAccountException("The account does not exist.");
}
//**登录认证
if (verifier.verify(token)) {
return new SimpleAuthenticationInfo(token, token, "jwtRealm");
}
throw new AuthenticationException("Username or password error.");
}
}
3.login and logout
/**
*********************************************************
** @desc : 登录
** @author Pings
** @date 2019/1/22
** @param userName 用户名称
** @param password 用户密码
** @return ApiResponse
* *******************************************************
*/
@ApiOperation(value="登录", notes="验证用户名和密码")
@PostMapping(value = "/account")
public ApiResponse account(String userName, String password, HttpServletResponse response){
if(StringUtils.isBlank(userName) || StringUtils.isBlank(password))
throw new UnauthorizedException("用户名/密码不能为空");
//**md5加密
password = DigestUtils.md5DigestAsHex(password.getBytes());
User user = this.userService.getByUserName(userName);
if(user != null && user.getPassword().equals(password)) {
JwtUtil.setHttpServletResponse(response, verifier.sign(userName));
//**用户权限
Set<String> rights = user.getRoles().stream().map(Role::getRights).flatMap(List::stream).map(Right::getCode).collect(toSet());
return new ApiResponse(200, "登录成功", rights);
} else
return new ApiResponse(500, "用户名/密码错误");
}
/**
*********************************************************
** @desc : 退出登录
** @author Pings
** @date 2019/3/26
** @return ApiResponse
* *******************************************************
*/
@ApiOperation(value="退出登录", notes="退出登录")
@GetMapping(value = "/logout")
public ApiResponse logout(){
this.verifier.invalidateSign(this.getCurrentUserName());
//**退出登录
SecurityUtils.getSubject().logout();
return new ApiResponse(200, "退出登录成功");
}
更新记录
2019-05-20 搭建