Skip to content

Commit

Permalink
[新增功能](20200420): 鉴权系统-完成授权部分
Browse files Browse the repository at this point in the history
解决
1. Token刷新并发处理问题
	dolyw/ShiroJwt#29
2.
授权部分完成
3. 添加授权异常拦截器 ControllerAspect
4. 测试授权.LocationController
  • Loading branch information
Marcabo committed Apr 20, 2020
1 parent 3ff110c commit bd3a7bf
Show file tree
Hide file tree
Showing 20 changed files with 459 additions and 83 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ private Constant() {}
*/
public static final String PREFIX_SHIRO_REFRESH_TOKEN = "shiro:refresh_token:";

/**
* redis-key-前缀-shiro:refresh_token:transition:
*/
public static final String PREFIX_SHIRO_REFRESH_TOKEN_TRANSITION = "shiro:refresh_token_transition:";

/**
* JWT-account:
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,16 @@
import com.herion.everyknow.web.response.EKnowResponse;
import com.herion.everyknow.web.utils.ResultUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.lang.annotation.Annotation;
import java.util.Collection;
Expand All @@ -29,7 +34,7 @@
@Order(1)
@Aspect
@Slf4j
//@RestControllerAdvice
@RestControllerAdvice
public class ControllerAspect {

// /**
Expand All @@ -54,6 +59,23 @@ public class ControllerAspect {
// return ResultUtils.getFailureResponse("500","未知异常", null);
// }

/**
* aroundController 中的 异常处理只能处理 Controller 层中的异常.进入 Controller 层之前的异常无法捕获
* 这个方法就是为了 捕获 Shiro 的 AuthorizationException(授权类异常)
* // TODO 后续需要完善, UnauthenticatedException UnauthenticatedException UnauthenticatedException
* @param e
* @return
* @throws Throwable
*/
@ExceptionHandler(AuthorizationException.class)
public EKnowResponse AuthorizationExceptionHandler(Exception e) {
log.info(e.getMessage());
if (e instanceof UnauthenticatedException) {
UnauthorizedException unauthorizedException = (UnauthorizedException) e;
}
return ResultUtils.getFailureResponse(EnumResponseType.NO_PERMISSION.getHttp(),EnumResponseType.NO_PERMISSION.getMsg(), EnumResponseType.NO_PERMISSION, null);
}


@Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
public Object aroundController(ProceedingJoinPoint jp) throws Throwable {
Expand Down Expand Up @@ -147,7 +169,7 @@ private Object getErr(Exception e, Object[] args) {
boolean pageFlag = this.isPage(args);
if (!pageFlag) {
// result = ResultUtils.getFailureResponse("500", e.getClass().getName() + ": " + e.getMessage(), null);
result = ResultUtils.getFailureResponse("500", e.getMessage(), null);
result = ResultUtils.getFailureResponse(EnumResponseType.SYS_ERR.getHttp(), e.getMessage(), null);
} else {
EKnowPageRequest eKnowPageRequest = null;
// 如果是分页请求入参只有一个
Expand All @@ -157,7 +179,7 @@ private Object getErr(Exception e, Object[] args) {
if (args[0] instanceof CommonHttpPageRequest) {
eKnowPageRequest = ((CommonHttpPageRequest) args[0]).geteKnowRequest();
}
result = ResultUtils.getPageResponse("500", "未知异常", EnumResponseType.SYS_ERR, null, eKnowPageRequest);
result = ResultUtils.getPageResponse(EnumResponseType.SYS_ERR.getHttp(), "未知异常", EnumResponseType.SYS_ERR, null, eKnowPageRequest);
}
return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@
* @create 2020-03-18 23:20
*/
public enum EnumResponseType {
SUCCESS("EKNOW_SUCCESS","执行成功"),
SYS_ERR("EKNOW_SYS_ERR","系统错误"),
NO_LOGIN("EKNOW_NO_LOGIN", "登录已过期,请重新登录"),
ERR_TOKEN("EKNOW_ERR_TOKEN", "Token或秘钥不正确");
SUCCESS("200","EKNOW_SUCCESS","执行成功"),
SYS_ERR("500","EKNOW_SYS_ERR","系统错误"),
NO_LOGIN("401","EKNOW_NO_LOGIN", "登录已过期,请重新登录"),
ERR_TOKEN("401","EKNOW_ERR_TOKEN", "Token或秘钥不正确"),
NO_PERMISSION("403","EKNOW_NO_PERMISSION", "没有权限");

String http;
String code;
String msg;

EnumResponseType(String code, String msg) {
EnumResponseType(String http, String code, String msg) {
this.http = http;
this.code = code;
this.msg = msg;
}
Expand All @@ -33,6 +36,10 @@ public static EnumResponseType find(String code) {
return null;
}

public String getHttp() {
return http;
}

public String getCode() {
return code;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public class EknowConfig {

private static String encryptJWTKey;

private static long refreshTokenTransitionExpireTime;

public static Boolean getMustLogin() {
return mustLogin;
}
Expand Down Expand Up @@ -65,4 +67,13 @@ public static String getEncryptJWTKey() {
public void setEncryptJWTKey(String encryptJWTKey) {
EknowConfig.encryptJWTKey = encryptJWTKey;
}

public static long getRefreshTokenTransitionExpireTime() {
return refreshTokenTransitionExpireTime;
}

@Value("${eknow.refreshTokenTransitionExpireTime}")
public void setRefreshTokenTransitionExpireTime(long refreshTokenTransitionExpireTime) {
EknowConfig.refreshTokenTransitionExpireTime = refreshTokenTransitionExpireTime;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
Expand Down Expand Up @@ -69,22 +70,22 @@ protected boolean isAccessAllowed(ServletRequest request, ServletResponse respon
String msg = e.getMessage();
// 获取应用异常 (该 Cause是导致抛出 throwable(异常)的 throwable(异常))
Throwable throwable = e.getCause();
EKnowResponse<Object> failureResponse = ResultUtils.getFailureResponse("401", msg, EnumResponseType.ERR_TOKEN, null);;
EKnowResponse<Object> failureResponse = ResultUtils.getFailureResponse(EnumResponseType.ERR_TOKEN.getHttp(), msg, EnumResponseType.ERR_TOKEN, null);;
if (throwable instanceof SignatureVerificationException) {
// 该异常为 JWT 异常, AccessToken 认证失败(Token 或 秘钥不对)
failureResponse = ResultUtils.getFailureResponse("401", "Token 或 秘钥不正确", EnumResponseType.ERR_TOKEN, null);
failureResponse = ResultUtils.getFailureResponse(EnumResponseType.ERR_TOKEN.getHttp(), "Token 或 秘钥不正确", EnumResponseType.ERR_TOKEN, null);
} else if (throwable instanceof TokenExpiredException) {
// 该异常为 JWT 的 AccessToken 已过期, 判断RefreshToken 未过期就进行 AccessToken 刷新
if (this.refreshToken(request, response)) {
return true;
} else {
failureResponse = ResultUtils.getFailureResponse("401", "登录已过期,请重新登录", EnumResponseType.ERR_TOKEN, null);
failureResponse = ResultUtils.getFailureResponse(EnumResponseType.ERR_TOKEN.getHttp(), "登录已过期,请重新登录", EnumResponseType.ERR_TOKEN, null);
}
} else {
// 应用异常
if (throwable != null) {
// 获取应用异常
failureResponse = ResultUtils.getFailureResponse("401", throwable.getMessage(), EnumResponseType.ERR_TOKEN, null);
failureResponse = ResultUtils.getFailureResponse(EnumResponseType.ERR_TOKEN.getHttp(), throwable.getMessage(), EnumResponseType.ERR_TOKEN, null);
}
}
// Token认证失败直接返回 Response信息
Expand All @@ -102,7 +103,7 @@ protected boolean isAccessAllowed(ServletRequest request, ServletResponse respon
// mustLoginFlag = true 开启任何请求都必须登录才可访问
final Boolean mustLoginFlag = EknowConfig.getMustLogin();
if (mustLoginFlag) {
EKnowResponse<Object> failureResponse = ResultUtils.getFailureResponse("401", "登录已过期,请重新登录", EnumResponseType.NO_LOGIN, null);
EKnowResponse<Object> failureResponse = ResultUtils.getFailureResponse(EnumResponseType.ERR_TOKEN.getHttp(), "登录已过期,请重新登录", EnumResponseType.NO_LOGIN, null);
this.response401(response, failureResponse);
return false;
}
Expand Down Expand Up @@ -185,30 +186,49 @@ private boolean refreshToken(ServletRequest request, ServletResponse response) {
String token = this.getAuthzHeader(request);
// 获取 Token的账号信息
String username = JWTUtil.getUsername(token);
// 判断Redis 中 RefreshToken是否存在
if (JedisUtil.exists(Constant.PREFIX_SHIRO_REFRESH_TOKEN + username)) {
// Redis中 RefreshToken还存在, 获取RefreshToken的时间戳
String currentTimeMillisRedis = JedisUtil.getObject(Constant.PREFIX_SHIRO_REFRESH_TOKEN + username).toString();
// 获取当前AccessToken中的时间戳, 与 RefreshToken的时间戳作对比, 如果当前时间戳一致, 进行 AccessToken刷新
if (JWTUtil.getCurrentTimeMillis(token).equals(currentTimeMillisRedis)) {
// 获取当前最新时间戳
String currentTimeMillis = String.valueOf(System.currentTimeMillis());
// 设置RefreshToken中的时间戳为当前最新时间戳, 且刷新过期时间重新设置为 30 分钟过期(配置文件可配置 RefreshTokenExpireTime属性)
JedisUtil.setObject(Constant.PREFIX_SHIRO_REFRESH_TOKEN + username, currentTimeMillis, (int)EknowConfig.getRefreshTokenExpireTime());
System.out.println(EknowConfig.getRefreshTokenExpireTime());
// 刷新 AccessToken.设置时间戳为当前最新时间戳
token = JWTUtil.sign(username, currentTimeMillis);
// 将刷新的AccessToken再次进行Shiro的登录
JWTToken jwtToken = new JWTToken(token);
// 提交给 UserRealm 进行认证, 如果错误会抛出异常被捕获
this.getSubject(request, response).login(jwtToken);
// 最后将刷新的AccessToken存放在Response的 Header中的 Authorization字段返回
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
httpServletResponse.setHeader("Authorization", token);
httpServletResponse.setHeader("Access-Control-Expose-Headers", "Authorization");
return true;
// 刷新Token 时需要进行同步防止出现问题. synchronized 为单机版, 需要使用集群时请使用 Redission 分布式锁
synchronized (this) {
// 判断Redis 中 RefreshToken是否存在
if (JedisUtil.exists(Constant.PREFIX_SHIRO_REFRESH_TOKEN + username)) {
// Redis中 RefreshToken还存在, 获取RefreshToken的时间戳
String currentTimeMillisRedis = JedisUtil.getObject(Constant.PREFIX_SHIRO_REFRESH_TOKEN + username).toString();
// 获取当前AccessToken中的时间戳, 与 RefreshToken的时间戳作对比, 如果当前时间戳一致, 进行 AccessToken刷新
if (JWTUtil.getCurrentTimeMillis(token).equals(currentTimeMillisRedis)) {
// 获取当前最新时间戳
String currentTimeMillis = String.valueOf(System.currentTimeMillis());
// 设置RefreshToken中的时间戳为当前最新时间戳, 且刷新过期时间重新设置为 30 分钟过期(配置文件可配置 RefreshTokenExpireTime属性)
JedisUtil.setObject(Constant.PREFIX_SHIRO_REFRESH_TOKEN + username, currentTimeMillis, (int)EknowConfig.getRefreshTokenExpireTime());
System.out.println(EknowConfig.getRefreshTokenExpireTime());
// 刷新 AccessToken.设置时间戳为当前最新时间戳
token = JWTUtil.sign(username, currentTimeMillis);
// 将刷新的AccessToken再次进行Shiro的登录
JWTToken jwtToken = new JWTToken(token);
// 提交给 UserRealm 进行认证, 如果错误会抛出异常被捕获
this.getSubject(request, response).login(jwtToken);

// 刷新AccessToken成功, Redis设置RefreshToken过渡时间, value为旧token,这个是为了解决Token刷新并发的问题
JedisUtil.setObject(Constant.PREFIX_SHIRO_REFRESH_TOKEN_TRANSITION + username, this.getAuthzHeader(request), (int) EknowConfig.getRefreshTokenTransitionExpireTime());

// 最后将刷新的AccessToken存放在Response的 Header中的 Authorization字段返回
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
httpServletResponse.setHeader("Authorization", token);
httpServletResponse.setHeader("Access-Control-Expose-Headers", "Authorization");
return true;
} else {
// 说明当前Token已经被刷新,判断当前账号是否存在RefreshToken 过渡时间, 是就放行
if (JedisUtil.exists(Constant.PREFIX_SHIRO_REFRESH_TOKEN_TRANSITION + username)) {
String oldToken = JedisUtil.getObject(Constant.PREFIX_SHIRO_REFRESH_TOKEN_TRANSITION + username).toString();
// 判断Token是否一致
if (this.getAuthzHeader(request).equals(oldToken)) {
// 提交给UserRealm进行认证, 如果错误他回抛出异常并被捕获, 如果没有抛出异常则代表登入成功, 返回 true
this.getSubject(request, response).login(new JWTToken(oldToken));
return true;
}
}
}
}
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public ShiroFilterFactoryBean shiroFilterFactoryBean(@Autowired SecurityManager
* http://shiro.apache.org/web.html#urls-
*/
LinkedHashMap<String, String> filterRuleMap = new LinkedHashMap<>();
// filterRuleMap.put("/user/login", "anon");
filterRuleMap.put("/user/login", "anon");
filterRuleMap.put("/**", "jwt");
factoryBean.setFilterChainDefinitionMap(filterRuleMap);
return factoryBean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import com.herion.everyknow.common.bean.Constant;
import com.herion.everyknow.seims.dao.SysUserDao;
import com.herion.everyknow.seims.dao.entity.SysUser;
import com.herion.everyknow.seims.service.SysUserService;
import com.herion.everyknow.seims.dao.entity.*;
import com.herion.everyknow.seims.service.*;
import com.herion.everyknow.seims.utils.JWTUtil;
import com.herion.everyknow.seims.utils.JedisUtil;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -14,8 +14,12 @@
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
* @Description 自定义 Realm(用来认证授权)
* @Description 自定义 Realm(用来认证授权) 在 ShiroConfig 中 注入
* @auther wubo25320
* @create 2020-04-15 16:44
*/
Expand All @@ -25,6 +29,18 @@ public class UserRealm extends AuthorizingRealm {
@Autowired
private SysUserService sysUserService;

@Autowired
private SysUserRoleService sysUserRoleService;

@Autowired
private SysRoleService sysRoleService;

@Autowired
private SysRolePermissionService sysRolePermissionService;

@Autowired
private SysPermissionService sysPermissionService;

// 大坑!!! 必须重写此方法, 不然shiro 会报错
@Override
public boolean supports(AuthenticationToken token) {
Expand All @@ -40,9 +56,24 @@ public boolean supports(AuthenticationToken token) {
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
String username = JWTUtil.getUsername(principalCollection.toString());

System.out.println("授权逻辑测试");
return null;
// 查询当前用户信息
SysUser user = sysUserService.getUser(username);
// 查询当前用户角色信息
SysUserRole sysUserRole = sysUserRoleService.queryByUserId(user.getId());
if (sysUserRole != null) {
// 添加角色
SysRole sysRole = sysRoleService.queryById(sysUserRole.getRoleId());
simpleAuthorizationInfo.addRole(sysRole.getRoleName());
// 查询当前角色权限信息
List<SysRolePermission> sysRolePermissionList = sysRolePermissionService.queryByRoleId(sysUserRole.getRoleId());
List<SysPermission> sysPermisssionList = new ArrayList<>();
for (SysRolePermission sysRolePermission : sysRolePermissionList) {
sysPermisssionList.add(sysPermissionService.queryById(sysRolePermission.getPermissionId()));
}
// 添加权限
simpleAuthorizationInfo.addStringPermissions(sysPermisssionList.stream().map(permission -> permission.getPermissionCode()).collect(Collectors.toList()));
}
return simpleAuthorizationInfo;
}

/**
Expand Down Expand Up @@ -93,6 +124,16 @@ protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authent
throw new UnknownAccountException("该帐号不存在(The account does not exist.)");
}

// 判断当前账号是否在 RefreshToken过渡时间, 是就放行
if (JedisUtil.exists(Constant.PREFIX_SHIRO_REFRESH_TOKEN_TRANSITION + username)) {
// 获取RefreshToken过渡时间Key保存的旧的 token
String oldToken = JedisUtil.getObject(Constant.PREFIX_SHIRO_REFRESH_TOKEN_TRANSITION + username).toString();
// 判断旧Token是否一致
if (token.equals(oldToken)) {
return new SimpleAuthenticationInfo(token, token, getName());
}
}

// 开始认证,要 AccessToken 认证通过, 且Redis中存在RefreshToken, 且两个Token时间戳一致
// AccessToken 认证通过.主要就是判断有没有过期(AccessTokenExpireTime)
if (JWTUtil.verify(token) && JedisUtil.exists(Constant.PREFIX_SHIRO_REFRESH_TOKEN + username)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,6 @@ public interface SysPermissionDao {
*/
SysPermission queryById(Integer id);

/**
* 查询指定行数据
*
* @param offset 查询起始位置
* @param limit 查询条数
* @return 对象列表
*/
List<SysPermission> queryAllByLimit(@Param("offset") int offset, @Param("limit") int limit);


/**
* 通过实体作为筛选条件查询
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,6 @@ public interface SysRoleDao {
*/
SysRole queryById(Integer id);

/**
* 查询指定行数据
*
* @param offset 查询起始位置
* @param limit 查询条数
* @return 对象列表
*/
List<SysRole> queryAllByLimit(@Param("offset") int offset, @Param("limit") int limit);


/**
* 通过实体作为筛选条件查询
*
Expand Down
Loading

0 comments on commit bd3a7bf

Please sign in to comment.