基於SpringBoot的後臺管理系統(Apache Shiro,Spring Session(重點))(五)

Guo_1_9發表於2018-03-05

5、 Shiro許可權管理,Spring Session、XssFilter

說明

如果您有幸能看到,請認閱讀以下內容;

  • 1、本專案臨摹自abel533的Guns,他的專案 fork 自 stylefengGuns!開源的世界真好,可以學到很多知識。

  • 2、版權歸原作者所有,自己只是學習使用。跟著大佬的思路,希望自己也能變成大佬。gogogo》。。

  • 3、目前只是一個後臺模組,希望自己技能增強到一定時,可以把stylefeng 的 [Guns]融合進來。

  • 4、note裡面是自己的學習過程,菜鳥寫的,不是大佬寫的。內容都是大佬的。

  • 5、如有拼寫錯誤,還請見諒。目前的桌子不適合打字,本文只為自己記錄.

問大家一個問題,你們在看本文的時候,覺得哪裡有需要修改的地方?內容和格式方面,歡迎大家提出來。

目錄

  • 1、SpringBoot第一站:分析了啟動類。還有各種自動配置的原始碼點這裡
  • 2、SpringBoot第二站:定義了異常、註解、Node節點、Page點這裡
  • 3、SpringBoot第三站:SpringBoot資料來源配置、Mybatis配置、日誌記錄點這裡
  • 4、SpringBoot第四站:SpringBoot快取配置、全域性異常處理點這裡

說明

如果您有幸能看到,請認閱讀以下內容;

  • 1、本專案臨摹自abel533的Guns,他的專案 fork 自 stylefengGuns!開源的世界真好,可以學到很多知識。

  • 2、版權歸原作者所有,自己只是學習使用。跟著大佬的思路,希望自己也能變成大佬。gogogo》。。

  • 3、目前只是一個後臺模組,希望自己技能增強到一定時,可以把stylefeng 的 [Guns]融合進來。

  • 4、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的特點: 參考這裡

基於SpringBoot的後臺管理系統(Apache Shiro,Spring Session(重點))(五)

這些特點被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;

    }
}
複製程式碼

自己只是過了一遍,要想更深入的瞭解,還需好好努力啊,只是自己的筆記。

相關文章