- 后端:重构 oauth2 模块,方便后续 User 接入。

- 后端:重写 Admin 安全拦截器,实现类似 Shiro 的效果。
This commit is contained in:
YunaiV
2019-05-16 19:02:09 +08:00
parent 2b02b5b9e5
commit b14169a747
52 changed files with 698 additions and 481 deletions

View File

@@ -0,0 +1,22 @@
package cn.iocoder.mall.admin.sdk.annotation;
import java.lang.annotation.*;
/**
* 参考 Shiro @RequiresPermissions 设计 http://shiro.apache.org/static/1.3.2/apidocs/org/apache/shiro/authz/annotation/RequiresPermissions.html
*
* 通过将该注解添加到 Controller 的方法上,进行授权鉴定
*/
@Documented
@Target({ElementType.METHOD}) // 暂时不支持 ElementType.TYPE ,因为没有场景
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresPermissions {
/**
* 当有多个标识时,必须全部拥有权限,才可以操作
*
* @return 权限标识数组
*/
String[] value();
}

View File

@@ -0,0 +1,17 @@
package cn.iocoder.mall.admin.sdk.constant;
/**
* 逻辑类型枚举
*/
public enum LogicalEnum {
/**
* 并且
*/
AND,
/**
* 或者
*/
OR,
}

View File

@@ -1,26 +1,18 @@
package cn.iocoder.mall.admin.sdk.context;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.Set;
/**
* Security 上下文
*/
@Data
@Accessors(chain = true)
public class AdminSecurityContext {
private final Integer adminId;
private final Set<Integer> roleIds;
private Integer adminId;
private Set<Integer> roleIds;
public AdminSecurityContext(Integer adminId, Set<Integer> roleIds) {
this.adminId = adminId;
this.roleIds = roleIds;
}
public Integer getAdminId() {
return adminId;
}
public Set<Integer> getRoleIds() {
return roleIds;
}
}
}

View File

@@ -17,7 +17,7 @@ public class AdminSecurityContextHolder {
AdminSecurityContext ctx = SECURITY_CONTEXT.get();
// 为空时,设置一个空的进去
if (ctx == null) {
ctx = new AdminSecurityContext(null, null);
ctx = new AdminSecurityContext();
SECURITY_CONTEXT.set(ctx);
}
return ctx;

View File

@@ -1,37 +1,48 @@
package cn.iocoder.mall.admin.sdk.interceptor;
import cn.iocoder.common.framework.constant.MallConstants;
import cn.iocoder.common.framework.constant.UserTypeEnum;
import cn.iocoder.common.framework.exception.ServiceException;
import cn.iocoder.common.framework.util.HttpUtil;
import cn.iocoder.common.framework.util.MallUtil;
import cn.iocoder.common.framework.vo.CommonResult;
import cn.iocoder.common.framework.util.StringUtil;
import cn.iocoder.mall.admin.api.AdminService;
import cn.iocoder.mall.admin.api.OAuth2Service;
import cn.iocoder.mall.admin.api.bo.admin.AdminAuthorizationBO;
import cn.iocoder.mall.admin.api.bo.oauth2.OAuth2AuthenticationBO;
import cn.iocoder.mall.admin.api.constant.AdminErrorCodeEnum;
import cn.iocoder.mall.admin.api.dto.oauth2.OAuth2GetTokenDTO;
import cn.iocoder.mall.admin.sdk.annotation.RequiresPermissions;
import cn.iocoder.mall.admin.sdk.context.AdminSecurityContext;
import cn.iocoder.mall.admin.sdk.context.AdminSecurityContextHolder;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.Set;
/**
* 安全拦截器
* Admin 安全拦截器
*/
@Component
public class AdminSecurityInterceptor extends HandlerInterceptorAdapter {
@Reference(validation = "true", version = "${dubbo.consumer.OAuth2Service.version:1.0.0}")
private OAuth2Service oauth2Service;
@Reference(validation = "true", version = "${dubbo.consumer.AdminService.version:1.0.0}")
private AdminService adminService;
/**
* 忽略的 URL 集合,即无需经过认证
*
* 对于 Admin 的系统,默认所有接口都需要进行认证
*/
@Value("${admins.security.ignore_url:#{null}}")
@Value("${admins.security.ignore_urls:#{null}}")
private Set<String> ignoreUrls;
public AdminSecurityInterceptor setIgnoreUrls(Set<String> ignoreUrls) {
@@ -42,39 +53,46 @@ public class AdminSecurityInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 设置当前访问的用户类型。注意,即使未登陆,我们也认为是管理员
MallUtil.setUserType(request, MallConstants.USER_TYPE_ADMIN);
// 校验访问令牌是否正确。若正确,返回授权信息
MallUtil.setUserType(request, UserTypeEnum.ADMIN.getValue());
// 根据 accessToken 获得认证信息,判断是谁
String accessToken = HttpUtil.obtainAuthorization(request);
OAuth2AuthenticationBO authentication = null;
if (accessToken != null) {
CommonResult<OAuth2AuthenticationBO> result = oauth2Service.checkToken(accessToken);
// TODO sin 先临时跳过 认证
// CommonResult<OAuth2AuthenticationBO> result = CommonResult.success(new OAuth2AuthenticationBO()
// .setAdminId(1)
// .setRoleIds(Sets.newHashSet(1, 2, 3, 4)));
if (result.isError()) { // TODO 芋艿,如果访问的地址无需登录,这里也不用抛异常
throw new ServiceException(result.getCode(), result.getMessage());
}
authentication = result.getData();
// 添加到 AdminSecurityContext
AdminSecurityContext context = new AdminSecurityContext(authentication.getAdminId(), authentication.getRoleIds());
AdminSecurityContextHolder.setContext(context);
// 同时也记录管理员编号到 AdminAccessLogInterceptor 中。因为:
// AdminAccessLogInterceptor 需要在 AdminSecurityInterceptor 之前执行,这样记录的访问日志才健全
// AdminSecurityInterceptor 执行后,会移除 AdminSecurityContext 信息,这就导致 AdminAccessLogInterceptor 无法获得管理员编号
// 因此,这里需要进行记录
if (authentication.getAdminId() != null) {
MallUtil.setUserId(request, authentication.getAdminId());
}
} else {
String url = request.getRequestURI();
if (ignoreUrls != null && !ignoreUrls.contains(url)) { // TODO 临时写死。非登陆接口,必须已经认证身份,不允许匿名访问
throw new ServiceException(AdminErrorCodeEnum.OAUTH_NOT_LOGIN.getCode(), AdminErrorCodeEnum.OAUTH_NOT_LOGIN.getMessage());
ServiceException serviceException = null;
if (StringUtil.hasText(accessToken)) {
try {
authentication = oauth2Service.getAuthentication(new OAuth2GetTokenDTO().setAccessToken(accessToken)
.setUserType(UserTypeEnum.ADMIN.getValue()));
} catch (ServiceException e) {
serviceException = e;
}
}
// 校验是否需要已授权
// TODO sin 暂时不校验
// checkPermission(request, authentication);
// 进行鉴权
String url = request.getRequestURI();
boolean needAuthentication = ignoreUrls == null || !ignoreUrls.contains(url);
AdminAuthorizationBO authorization = null;
if (needAuthentication) {
if (serviceException != null) { // 认证失败,抛出上面认证失败的 ServiceException 异常
throw serviceException;
}
if (authentication == null) { // 无认证信息,抛出未登陆 ServiceException 异常
throw new ServiceException(AdminErrorCodeEnum.OAUTH2_NOT_LOGIN.getCode(), AdminErrorCodeEnum.OAUTH2_NOT_LOGIN.getMessage());
}
authorization = checkPermission(handler, authentication);
}
// 鉴权完成,初始化 AdminSecurityContext 上下文
AdminSecurityContext context = new AdminSecurityContext();
AdminSecurityContextHolder.setContext(context);
if (authentication != null) {
context.setAdminId(authentication.getUserId());
MallUtil.setUserId(request, authentication.getUserId()); // 记录到 request 中,避免 AdminSecurityContext 后续清理掉后,其它地方需要用到 userId
if (authorization != null) {
context.setRoleIds(authorization.getRoleIds());
}
}
// 返回成功
return super.preHandle(request, response, handler);
}
@@ -85,14 +103,18 @@ public class AdminSecurityInterceptor extends HandlerInterceptorAdapter {
AdminSecurityContextHolder.clear();
}
private void checkPermission(HttpServletRequest request, OAuth2AuthenticationBO authentication) {
Integer adminId = authentication != null ? authentication.getAdminId() : null;
Set<Integer> roleIds = authentication != null ? authentication.getRoleIds() : null;
String url = request.getRequestURI();
CommonResult<Boolean> result = oauth2Service.checkPermission(adminId, roleIds, url);
if (result.isError()) {
throw new ServiceException(result.getCode(), result.getMessage());
}
private AdminAuthorizationBO checkPermission(Object handler, OAuth2AuthenticationBO authentication) {
// 获得 @RequiresPermissions 注解
Assert.isTrue(handler instanceof HandlerMethod, "handler 必须是 HandlerMethod 类型");
HandlerMethod handlerMethod = (HandlerMethod) handler;
RequiresPermissions requiresPermissions = handlerMethod.getMethodAnnotation(RequiresPermissions.class);
// 执行校验
return adminService.checkPermissions(authentication.getUserId(),
requiresPermissions != null ? Arrays.asList(requiresPermissions.value()) : null);
}
private void checkPermission() {
}
}