基于SpringBoot的后台管理系统(Apache Shiro,Spring Session(重点))(五)

五、 Shiro权限管理,Spring Session、XssFilter

说明

若是您有幸能看到,请认阅读如下内容;javascript

  • 一、本项目临摹自abel533的Guns,他的项目 fork 自 stylefengGuns!开源的世界真好,能够学到不少知识。html

  • 二、版权归原做者全部,本身只是学习使用。跟着大佬的思路,但愿本身也能变成大佬。gogogo》。。java

  • 三、目前只是一个后台模块,但愿本身技能加强到必定时,能够把stylefeng 的 [Guns]融合进来。git

  • 四、note里面是本身的学习过程,菜鸟写的,不是大佬写的。内容都是大佬的。github

  • 五、若有拼写错误,还请见谅。目前的桌子不适合打字,本文只为本身记录.web

问你们一个问题,大家在看本文的时候,以为哪里有须要修改的地方?内容和格式方面,欢迎你们提出来。redis

目录

  • 一、SpringBoot第一站:分析了启动类。还有各类自动配置的源码点这里
  • 二、SpringBoot第二站:定义了异常、注解、Node节点、Page点这里
  • 三、SpringBoot第三站:SpringBoot数据源配置、Mybatis配置、日志记录点这里
  • 四、SpringBoot第四站:SpringBoot缓存配置、全局异常处理点这里

说明

若是您有幸能看到,请认阅读如下内容;算法

  • 一、本项目临摹自abel533的Guns,他的项目 fork 自 stylefengGuns!开源的世界真好,能够学到不少知识。spring

  • 二、版权归原做者全部,本身只是学习使用。跟着大佬的思路,但愿本身也能变成大佬。gogogo》。。api

  • 三、目前只是一个后台模块,但愿本身技能加强到必定时,能够把stylefeng 的 [Guns]融合进来。

  • 四、note里面是本身的学习过程,菜鸟写的,不是大佬写的。内容都是大佬的。

Apache Shiro

在本项目中使用了Apache 的Shiro为安全护航,其实Spring也提供了声明式安全保护的框架,那就是Spring Securitu。有兴趣的能够看下我以前的笔记Srping-Secutiry实战笔记

(1)、Spring Security 是基于Spring 应用程序提供的声明式安全保护的安全框架。Spring Sercurity 提供了完整的安全性解决方案,它可以在Web请求级别和方法调用级别处理身份认证和受权,由于是基于Spring,因此Spring Security充分利用了依赖注入(Dependency injection DI) 和面向切面的技术。

Spring Security从两个角度来解决安全性,他使用Servlet规范中的Filter保护Web请求并限制URL级别的访问。Spring Security还可以使用AOP保护方法调用——借助于对象代理和使用通知,可以取保只有具有适当权限的用户才能访问安全保护的方法。

(2)、Apache Shiro是一个功能强大且灵活的开源安全框架,主要功能包括用户认证、受权、会话管理以及加密。

Apache Shiro的首要目标是易于使用和理解。系统安全是很是复杂甚至痛苦的,但Shiro并非。一个框架应该尽量的隐藏那些复杂的细节,而且公开一组简洁直观的API以简化开发人员在系统安全上所付出的努力。

我的倾向于前者,毕竟是Spring生态系统上的一员。

Apache Shiro功能:

  • 验证用户身份
  • 控制用户访问
  • 及时响应在认证、访问控制或会话声明周期内的全部事件。
  • 实现单点登陆功能

Apache Shiro的特色: 参考这里

这些特色被Shiro开发团队称之为“应用安全的四大基石”——认证、受权、会话管理和加密:

  • 认证:有时候被称做“登陆”,也就是验证一个用户是谁。
  • 受权:处理访问控制,例如决定“谁”能够访问“什么”资源。
  • 会话管理:管理特定用户的会话,甚至在非web环境或非EJB应用环境下。
  • 加密:在保持易用性的同时使用加密算法保持数据的安全。
  • Web支持:Shiro的web api能够帮组web应用很是方便的提升安全性。
  • 缓存:缓存可让Apache Shiro的api在安全操做上的保持快速和高效。

看完这些基础的概念,咱们直接看接口的定义吧。

/** * 定义shirorealm所需数据的接口 */
public interface IShiro {

    /** * 根据帐号获取登陆用户 */
    User user(String account);

    /** * 根据系统用户获取Shiro的用户 */
    ShiroUser shiroUser(User user);

    //省略部分
    /** * 获取shiro的认证信息 */
    SimpleAuthenticationInfo info(ShiroUser shiroUser, User user, String realmName);
}

--------------------------------------------------------------------------------
/** * 自定义Authentication对象,使得Subject除了携带用户的登陆名外还能够携带更多信息 */
public class ShiroUser implements Serializable {

    private static final long serialVersionUID = 1L;

    public Integer id;          // 主键ID
    public String account;      // 帐号
    public String name;         // 姓名
    public Integer deptId;      // 部门id
    public List<Integer> roleList; // 角色集
    public String deptName;        // 部门名称
    public List<String> roleNames; // 角色名称集
    //Setter、Getter略
复制代码

ShiroFactroy工厂,SpringContextHolder是Spring的ApplicationContext的持有者,能够用静态方法的方式获取spring容器中的bean

@Service
@DependsOn("springContextHolder")
@Transactional(readOnly = true)
public class ShiroFactroy implements IShiro {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private MenuMapper menuMapper;

    public static IShiro me() {
        return SpringContextHolder.getBean(IShiro.class);
    }

    @Override
    public User user(String account) {

        User user = userMapper.getByAccount(account);

        // 帐号不存在
        if (null == user) {
            throw new CredentialsException();
        }
        // 帐号被冻结
        if (user.getStatus() != ManagerStatus.OK.getCode()) {
            throw new LockedAccountException();
        }
        return user;
    }

    @Override
    public ShiroUser shiroUser(User user) {
        ShiroUser shiroUser = new ShiroUser();

        shiroUser.setId(user.getId());            // 帐号id
        shiroUser.setAccount(user.getAccount());// 帐号
        shiroUser.setDeptId(user.getDeptid());    // 部门id
        shiroUser.setDeptName(ConstantFactory.me().getDeptName(user.getDeptid()));// 部门名称
        shiroUser.setName(user.getName());        // 用户名称

        Integer[] roleArray = Convert.toIntArray(user.getRoleid());// 角色集合
        List<Integer> roleList = new ArrayList<Integer>();
        List<String> roleNameList = new ArrayList<String>();
        for (int roleId : roleArray) {
            roleList.add(roleId);
            roleNameList.add(ConstantFactory.me().getSingleRoleName(roleId));
        }
        shiroUser.setRoleList(roleList);
        shiroUser.setRoleNames(roleNameList);

        return shiroUser;
    }

  //省略部分

    @Override
    public SimpleAuthenticationInfo info(ShiroUser shiroUser, User user, String realmName) {
        String credentials = user.getPassword();
        // 密码加盐处理
        String source = user.getSalt();
        ByteSource credentialsSalt = new Md5Hash(source);
        return new SimpleAuthenticationInfo(shiroUser, credentials, credentialsSalt, realmName);
    }

}
复制代码

这个类是重点

public class ShiroDbRealm extends AuthorizingRealm {

    /** * 登陆认证 */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {
        IShiro shiroFactory = ShiroFactroy.me();
        UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
        User user = shiroFactory.user(token.getUsername());
        ShiroUser shiroUser = shiroFactory.shiroUser(user);
        SimpleAuthenticationInfo info = shiroFactory.info(shiroUser, user, super.getName());
        return info;
    }
-------------------------------------------------------------------------------------------------
    /** * 权限认证 */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        IShiro shiroFactory = ShiroFactroy.me();
        ShiroUser shiroUser = (ShiroUser) principals.getPrimaryPrincipal();
        List<Integer> roleList = shiroUser.getRoleList();

        Set<String> permissionSet = new HashSet<>();
        Set<String> roleNameSet = new HashSet<>();

        for (Integer roleId : roleList) {
            List<String> permissions = shiroFactory.findPermissionsByRoleId(roleId);
            if (permissions != null) {
                for (String permission : permissions) {
                    if (ToolUtil.isNotEmpty(permission)) {
                        permissionSet.add(permission);
                    }
                }
            }
            String roleName = shiroFactory.findRoleNameByRoleId(roleId);
            roleNameSet.add(roleName);
        }

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.addStringPermissions(permissionSet);
        info.addRoles(roleNameSet);
        return info;
    }
-------------------------------------------------------------------------------------
    /** * 设置认证加密方式 */
    @Override
    public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
        HashedCredentialsMatcher md5CredentialsMatcher = new HashedCredentialsMatcher();
        md5CredentialsMatcher.setHashAlgorithmName(ShiroKit.hashAlgorithmName);
        md5CredentialsMatcher.setHashIterations(ShiroKit.hashIterations);
        super.setCredentialsMatcher(md5CredentialsMatcher);
    }
}

复制代码

让咱们来具体看看怎么用?权限是设置好了,可是每次用的时候须要检查是否拥有权限

/** * 检查用接口 */
public interface ICheck {

    /** * 检查指定角色 */
    boolean check(Object[] permissions);

    /** * 检查全体角色 */
    boolean checkAll();
}
--------------------------------------------------------------------------------
/** * 权限自定义检查 */
@Service
@DependsOn("springContextHolder")
@Transactional(readOnly = true)
public class PermissionCheckFactory implements ICheck {

    public static ICheck me() {
        return SpringContextHolder.getBean(ICheck.class);
    }

    @Override
    public boolean check(Object[] permissions) {
        ShiroUser user = ShiroKit.getUser();
        if (null == user) {
            return false;
        }
        String join = CollectionKit.join(permissions, ",");
        if (ShiroKit.hasAnyRoles(join)) {
            return true;
        }
        return false;
    }
    public boolean checkAll() {...}
}
复制代码
/** * 权限检查工厂 */
public class PermissionCheckManager {
    private final static PermissionCheckManager me = new PermissionCheckManager();

    private ICheck defaultCheckFactory = SpringContextHolder.getBean(ICheck.class);

    public static PermissionCheckManager me() {
        return me;
    }
    //....
    public static boolean check(Object[] permissions) {
        return me.defaultCheckFactory.check(permissions);
    }

    public static boolean checkAll() {
        return me.defaultCheckFactory.checkAll();
    }
}
复制代码

这时候就须要把权限设置为一个切面,在须要的时候直接织入。

/** * 权限注解,用于检查权限 规定访问权限 */
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Permission {
    String[] value() default {};
}
-------------------------------------------------------------------------------
/** * AOP 权限自定义检查 */
@Aspect
@Component
public class PermissionAop {

    @Pointcut(value = "@annotation(com.guo.guns.common.annotion.Permission)")
    private void cutPermission() {

    }

    @Around("cutPermission()")
    public Object doPermission(ProceedingJoinPoint point) throws Throwable {
        MethodSignature ms = (MethodSignature) point.getSignature();
        Method method = ms.getMethod();
        Permission permission = method.getAnnotation(Permission.class);
        Object[] permissions = permission.value();
        if (permissions == null || permissions.length == 0) {
            //检查全体角色
            boolean result = PermissionCheckManager.checkAll();
            if (result) {
                return point.proceed();
            } else {
                throw new NoPermissionException();
            }
        } else {
            //检查指定角色
            boolean result = PermissionCheckManager.check(permissions);
            if (result) {
                return point.proceed();
            } else {
                throw new NoPermissionException();
            }
        }
    }
}
复制代码

让咱们看一下在代码中具体是如何使用的,只有具备管理员才具有修改的权限。

/** * 管理员角色的名字 */
String ADMIN_NAME = "administrator";
--------------------------------------------------------------------------------
/** * 角色修改 */
@RequestMapping(value = "/edit")
@BussinessLog(value = "修改角色", key = "name", dict = Dict.RoleDict)
@Permission(Const.ADMIN_NAME)
@ResponseBody
public Tip edit(@Valid Role role, BindingResult result) {
    if (result.hasErrors()) {
        throw new BussinessException(BizExceptionEnum.REQUEST_NULL);
    }
    roleMapper.updateByPrimaryKeySelective(role);

    //删除缓存
    CacheKit.removeAll(Cache.CONSTANT);
    return SUCCESS_TIP;
}

复制代码

Spring Session

以前处理Session的办法是将HTTP session状态保存在独立的数据存储中,这个存储位于运行应用程序代码的JVM以外。使用 tomcat-redis-session-manager 开源项目解决分布式session跨域的问题,他的主要思想是利用Servlet容器提供的插件功能,自定义HttpSession的建立和管理策略,并经过配置的方式替换掉默认的策略。使用过tomcat-redis-session-manager 的都应该知道,配置相对仍是有一点繁琐的,须要人为的去修改Tomcat的配置,须要耦合Tomcat等Servlet容器的代码,而且对于分布式Redis集群的管理并非很好,与之相对的我的认为比较好的一个框架Spring Session能够真正对用户透明的去管理分布式Session。参考

Spring Session提供了一套建立和管理Servlet HttpSession的方案。Spring Session提供了集群Session(Clustered Sessions)功能,默认采用外置的Redis来存储Session数据,以此来解决Session共享的问题。

Spring Session不依赖于Servlet容器,而是Web应用代码层面的实现,直接在已有项目基础上加入spring Session框架来实现Session统一存储在Redis中。若是你的Web应用是基于Spring框架开发的,只须要对现有项目进行少许配置,便可将一个单机版的Web应用改成一个分布式应用,因为不基于Servlet容器,因此能够随意将项目移植到其余容器。

/** * spring session配置 */
//@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800) //session过时时间 若是部署多机环境,须要打开注释
@ConditionalOnProperty(prefix = "guns", name = "spring-session-open", havingValue = "true")
public class SpringSessionConfig {


}

复制代码

由于是声明式的,因此用起来很简单

/** * 静态调用session的拦截器 */
@Aspect
@Component
public class SessionInterceptor extends BaseController {

    @Pointcut("execution(* com.guo.guns.*..controller.*.*(..))")
    public void cutService() {
    }

    @Around("cutService()")
    public Object sessionKit(ProceedingJoinPoint point) throws Throwable {

        HttpSessionHolder.put(super.getHttpServletRequest().getSession());
        try {
            return point.proceed();
        } finally {
            HttpSessionHolder.remove();
        }
    }
}
--------------------------------------------------------------------------------
/** * 验证session超时的拦截器 * * @author fengshuonan * @date 2017年6月7日21:08:48 */
@Aspect
@Component
@ConditionalOnProperty(prefix = "guns", name = "session-open", havingValue = "true")
public class SessionTimeoutInterceptor extends BaseController {

    @Pointcut("execution(* com.guo.guns.*..controller.*.*(..))")
    public void cutService() {
    }

    @Around("cutService()")
    public Object sessionTimeoutValidate(ProceedingJoinPoint point) throws Throwable {

        String servletPath = HttpKit.getRequest().getServletPath();

        if (servletPath.equals("/kaptcha") || servletPath.equals("/login") || servletPath.equals("/global/sessionError")) {
            return point.proceed();
        }else{
            if(ShiroKit.getSession().getAttribute("sessionFlag") == null){
                ShiroKit.getSubject().logout();
                throw new InvalidSessionException();
            }else{
                return point.proceed();
            }
        }
    }
}

复制代码

让咱们看下具体是如何使用的》

/** * 获取shiro指定的sessionKey * */
@SuppressWarnings("unchecked")
public static <T> T getSessionAttr(String key) {
    Session session = getSession();
    return session != null ? (T) session.getAttribute(key) : null;
}

/** * 设置shiro指定的sessionKey * */
public static void setSessionAttr(String key, Object value) {
    Session session = getSession();
    session.setAttribute(key, value);
}

/** * 移除shiro指定的sessionKey */
public static void removeSessionAttr(String key) {
    Session session = getSession();
    if (session != null)
        session.removeAttribute(key);
}
-------------------登陆执行中的步骤-----------------------------------------
ShiroUser shiroUser = ShiroKit.getUser();
super.getSession().setAttribute("shiroUser", shiroUser);
super.getSession().setAttribute("username", shiroUser.getAccount());

LogManager.me().executeLog(LogTaskFactory.loginLog(shiroUser.getId(), getIp()));

ShiroKit.getSession().setAttribute("sessionFlag",true);

return REDIRECT + "/";
复制代码

super.getSession()调用的是BaseController中的方法。

防止XSS攻击

防止XSS攻击,经过XssFilter类对全部的输入的非法字符串进行过滤以及替换。

public class XssFilter implements Filter {

    FilterConfig filterConfig = null;

    public void init(FilterConfig filterConfig) throws ServletException {
        this.filterConfig = filterConfig;
    }

    public void destroy() {
        this.filterConfig = null;
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        chain.doFilter(new XssHttpServletRequestWrapper(
                (HttpServletRequest) request), response);
    }

}
---------------------------------------------------------------------------------
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {

    public XssHttpServletRequestWrapper(HttpServletRequest servletRequest) {

        super(servletRequest);

    }

    public String[] getParameterValues(String parameter) {

        String[] values = super.getParameterValues(parameter);

        if (values == null) {

            return null;

        }

        int count = values.length;

        String[] encodedValues = new String[count];

        for (int i = 0; i < count; i++) {

            encodedValues[i] = cleanXSS(values[i]);

        }

        return encodedValues;

    }
    //省略部分

    private String cleanXSS(String value) {

        //You'll need to remove the spaces from the html entities below

        value = value.replaceAll("<", "& lt;").replaceAll(">", "& gt;");

        value = value.replaceAll("\\(", "& #40;").replaceAll("\\)", "& #41;");

        value = value.replaceAll("'", "& #39;");

        value = value.replaceAll("eval\\((.*)\\)", "");

        value = value.replaceAll("[\\\"\\\'][\\s]*javascript:(.*)[\\\"\\\']", "\"\"");

        value = value.replaceAll("script", "");

        return value;

    }
}
复制代码

本身只是过了一遍,要想更深刻的了解,还需好好努力啊,只是本身的笔记。

相关文章
相关标签/搜索