公司随着业务的增长,各业务进行水平扩展面临拆分;随着业务的拆分各种管理系统扑面而来,为了方便权限统一管理,不得不自己开发或使用分布式权限管理(Spring Security)。Spring Security 依赖 Spring 和初级开发人员学习难度大,中小型公司不推荐使用;Apache Shiro 是一个强大易用的安全框架,Shiro 的 API 方便理解。经过网上各路大神对 shiro 与 spring security 的比较,最终决定使用 shiro 开发一个独立的权限管理平台。
该项目是在张开涛跟我学 shiro Demo 基础上进行开发、功能完善和管理页面优化,已上传 GitHub 欢迎 fork、star 及提出改进意见。
通过注解获取当前登录用户
拦截所有请求 1. 通过 shiro Subject 获取当前用户的用户名 2. 通过用户名获取用户信息 3. 将用户信息保存 ServletRequest 对象中 代码示例:
- /**
- *
- * @ClassName: SysUserFilter
- * @Description: 请求拦截器
- * @author yangzhao
- * @date 2017年12月20日 下午2:10:23
- *
- */
- public class SysUserFilter extends PathMatchingFilter {
- @Autowired
- private UserService userService;
- @Override
- protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
- String username = (String)SecurityUtils.getSubject().getPrincipal();
- User user = userService.findByUsername(username);
- request.setAttribute(Constant.CURRENT_USER,user);
- return true;
- }
- }
通过 SpringAOP 拦截容器内所有 Java 方法参数是否有 CurrentUser 注解,若果有注解标识从 NativewebRequest 中获取 user 信息进行参数绑定 代码示例:
- /**
- *
- * @ClassName: CurrentUserMethodArgumentResolver
- * @Description: Spring方法拦截器参数绑定
- * @author yangzhao
- * @date 2017年12月20日 下午2:07:55
- *
- */
- public class CurrentUserMethodArgumentResolver implements HandlerMethodArgumentResolver {
- public CurrentUserMethodArgumentResolver() {
- }
- @Override
- public boolean supportsParameter(MethodParameter parameter) {
- if (parameter.hasParameterAnnotation(CurrentUser.class)) {
- return true;
- }
- return false;
- }
- @Override
- public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
- CurrentUser currentUserAnnotation = parameter.getParameterAnnotation(CurrentUser.class);
- return webRequest.getAttribute(currentUserAnnotation.value(), NativeWebRequest.SCOPE_REQUEST);
- }
- }
shiro 权限认证通过后进行页面跳转 代码示例:
- /**
- *
- * @ClassName: ServerFormAuthenticationFilter
- * @Description: 认证拦截器-页面跳转
- * @author yangzhao
- * @date 2017年12月20日 下午2:10:18
- *
- */
- public class ServerFormAuthenticationFilter extends FormAuthenticationFilter {
- @Override
- protected void issueSuccessRedirect(ServletRequest request, ServletResponse response) throws Exception {
- String fallbackUrl = (String) getSubject(request, response)
- .getSession().getAttribute("authc.fallbackUrl");
- if(StringUtils.isEmpty(fallbackUrl)) {
- fallbackUrl = getSuccessUrl();
- }
- WebUtils.redirectToSavedRequest(request, response, fallbackUrl);
- }
1. 判断是否认证通过 若未认证进入下一步 2. 从 ServletRequest 获取回调 url 3. 获取默认回调 url(客户端 IP 和端口) 4. 将默认回调 url 保存到 session 中 5. 将第 2 步中的回调 url 保存到 ClientSavedRequest 中(方便 server 回调时返回到当前请求 url) 5. 当前请求重定向到 server 端登录页面 代码示例:
- /**
- *
- * @ClassName: AppService
- * @Description: 客户端认证拦截
- * @author yangzhao
- * @date 2017年12月20日 下午2:03:43
- *
- */
- public class ClientAuthenticationFilter extends AuthenticationFilter {
- @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
- Subject subject = getSubject(request, response);
- return subject.isAuthenticated();
- }
- @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
- String backUrl = request.getParameter("backUrl");
- saveRequest(request, backUrl, getDefaultBackUrl(WebUtils.toHttp(request)));
- redirectToLogin(request, response);
- return false;
- }
- protected void saveRequest(ServletRequest request, String backUrl, String fallbackUrl) {
- Subject subject = SecurityUtils.getSubject();
- Session session = subject.getSession();
- HttpServletRequest httpRequest = WebUtils.toHttp(request);
- session.setAttribute("authc.fallbackUrl", fallbackUrl);
- SavedRequest savedRequest = new ClientSavedRequest(httpRequest, backUrl);
- session.setAttribute(WebUtils.SAVED_REQUEST_KEY, savedRequest);
- }
- private String getDefaultBackUrl(HttpServletRequest request) {
- String scheme = request.getScheme();
- String domain = request.getServerName();
- int port = request.getServerPort();
- String contextPath = request.getContextPath();
- StringBuilder backUrl = new StringBuilder(scheme);
- backUrl.append("://");
- backUrl.append(domain);
- if ("http".equalsIgnoreCase(scheme) && port != 80) {
- backUrl.append(":").append(String.valueOf(port));
- } else if ("https".equalsIgnoreCase(scheme) && port != 443) {
- backUrl.append(":").append(String.valueOf(port));
- }
- backUrl.append(contextPath);
- backUrl.append(getSuccessUrl());
- return backUrl.toString();
- }
- }
ClientRealm 继承自 Shiro AuthorizingRealm 该类忽略 doGetAuthenticationInfo 方法实现,所有认证操作会转到 Server 端实现 代码示例:
- /**
- *
- * @ClassName: ClientRealm
- * @Description: 客户端shiro Realm
- * @author yangzhao
- * @date 2017年12月20日 下午2:03:43
- *
- */
- public class ClientRealm extends AuthorizingRealm {
- private RemoteService remoteService;
- private String appKey;
- public void setRemoteService(RemoteService remoteService) {
- this.remoteService = remoteService;
- }
- public void setAppKey(String appKey) {
- this.appKey = appKey;
- }
- @Override
- protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
- String username = (String) principals.getPrimaryPrincipal();
- SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
- PermissionContext context = remoteService.getPermissions(appKey, username);
- authorizationInfo.setRoles(context.getRoles());
- authorizationInfo.setStringPermissions(context.getPermissions());
- return authorizationInfo;
- }
- @Override
- protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
- //永远不会被调用
- throw new UnsupportedOperationException("永远不会被调用");
- }
- }
客户端与服务端 Session 同步
添加两个方法 setFiltersStr、setFilterChainDefinitionsStr,方便在 properties 文件中配置拦截器和定义过滤链 代码示例:
- /**
- *
- * @ClassName: ClientShiroFilterFactoryBean
- * @Description: 添加两个方法setFiltersStr、setFilterChainDefinitionsStr,方便在properties文件中配置拦截器和定义过滤链
- * @author yangzhao
- * @date 2017年12月20日 下午2:03:43
- *
- */
- public class ClientShiroFilterFactoryBean extends ShiroFilterFactoryBean implements ApplicationContextAware {
- private ApplicationContext applicationContext;
- @Override public void setApplicationContext(ApplicationContext applicationContext) {
- this.applicationContext = applicationContext;
- }
- public void setFiltersStr(String filters) {
- if (StringUtils.isEmpty(filters)) {
- return;
- }
- String[] filterArray = filters.split(";");
- for (String filter: filterArray) {
- String[] o = filter.split("=");
- getFilters().put(o[0], (Filter) applicationContext.getBean(o[1]));
- }
- }
- public void setFilterChainDefinitionsStr(String filterChainDefinitions) {
- if (StringUtils.isEmpty(filterChainDefinitions)) {
- return;
- }
- String[] chainDefinitionsArray = filterChainDefinitions.split(";");
- for (String filter: chainDefinitionsArray) {
- String[] o = filter.split("=");
- getFilterChainDefinitionMap().put(o[0], o[1]);
- }
- }
- }
通过 Spring HttpInvokerServiceExporter 工具将 shiro-distributed-platform-api 模块部分 API 暴露(remoteService、userService、resourceService),请参考 spring-mvc-export-service.xml 配置文件
配置 ShiroFilterFactoryBean filterChainDefinitions 属性将以上三个接口权限设置为游客、匿名(anon),请参考 spring-config-shiro.xml 配置文件
以上属于原创文章,转载请注明作者 @怪咖 QQ:208275451 Email:yangzhao_java@163.com
来源: https://juejin.im/post/5a3e7daef265da4310489552