人人快速開發(fā)平臺 renren-fast 源碼分析(一)權(quán)限控制

其實本人很不喜歡獨立開發(fā)時使用 Java 窄绒,但由于能力有限贝次,對其他語言的 web 開發(fā)能力不足。因此在找 Java 快速搭建項目時遇到了 renren-fast .看了一下彰导,感覺還是挺適合用于獨立開發(fā)的蛔翅。但是官方的源碼解析太貴,不想花這個錢位谋,因此自己來嘗試做源碼分析山析。

首先貼上項目 README 里面的介紹和結(jié)構(gòu)。

項目特點
  • 友好的代碼結(jié)構(gòu)及注釋掏父,便于閱讀及二次開發(fā)
  • 實現(xiàn)前后端分離笋轨,通過token進(jìn)行數(shù)據(jù)交互,前端再也不用關(guān)注后端技術(shù)
  • 靈活的權(quán)限控制损同,可控制到頁面或按鈕翩腐,滿足絕大部分的權(quán)限需求
  • 頁面交互使用Vue2.x,極大的提高了開發(fā)效率
  • 完善的代碼生成機制膏燃,可在線生成entity茂卦、xml、dao组哩、service等龙、vue、sql代碼伶贰,減少70%以上的開發(fā)任務(wù)
  • 引入quartz定時任務(wù)蛛砰,可動態(tài)完成任務(wù)的添加、修改黍衙、刪除泥畅、暫停、恢復(fù)及日志查看等功能
  • 引入API模板琅翻,根據(jù)token作為登錄令牌位仁,極大的方便了APP接口開發(fā)
  • 引入Hibernate Validator校驗框架柑贞,輕松實現(xiàn)后端校驗
  • 引入云存儲服務(wù),已支持:七牛云聂抢、阿里云钧嘶、騰訊云等
  • 引入swagger文檔支持,方便編寫API接口文檔
項目結(jié)構(gòu)

renren-fast
├─db 項目SQL語句

├─common 公共模塊
│ ├─aspect 系統(tǒng)日志
│ ├─exception 異常處理
│ ├─validator 后臺校驗
│ └─xss XSS過濾

├─config 配置信息

├─modules 功能模塊
│ ├─app API接口模塊(APP調(diào)用)
│ ├─job 定時任務(wù)模塊
│ ├─oss 文件服務(wù)模塊
│ └─sys 權(quán)限模塊

├─RenrenApplication 項目啟動類

├──resources
│ ├─mapper SQL對應(yīng)的XML文件
│ └─static 靜態(tài)資源

我本人就從自己讀代碼的順序著手琳疏,看看這幾個特點都是怎么實現(xiàn)的有决。

權(quán)限控制

該項目的權(quán)限控制我會分為以下幾個部分:

  • 權(quán)限設(shè)計
  • 認(rèn)證
  • 授權(quán)
  • app模塊的認(rèn)證

權(quán)限設(shè)計

該項目的權(quán)限主要由以下幾個實體之間的關(guān)系來控制(為了不讓文章太長,我把 getter setter都刪掉了)
SysMenuEntity.java

/**
 * 菜單管理
 * 
 * @author chenshun
 * @email sunlightcs@gmail.com
 * @date 2016年9月18日 上午9:26:39
 */
@TableName("sys_menu")
public class SysMenuEntity implements Serializable {
    private static final long serialVersionUID = 1L;
    
    /**
     * 菜單ID
     */
    @TableId
    private Long menuId;

    /**
     * 父菜單ID空盼,一級菜單為0
     */
    private Long parentId;
    
    /**
     * 父菜單名稱
     */
    @TableField(exist=false)
    private String parentName;

    /**
     * 菜單名稱
     */
    private String name;

    /**
     * 菜單URL
     */
    private String url;

    /**
     * 授權(quán)(多個用逗號分隔书幕,如:user:list,user:create)
     */
    private String perms;

    /**
     * 類型     0:目錄   1:菜單   2:按鈕
     */
    private Integer type;

    /**
     * 菜單圖標(biāo)
     */
    private String icon;

    /**
     * 排序
     */
    private Integer orderNum;
    
    /**
     * ztree屬性
     */
    @TableField(exist=false)
    private Boolean open;

    @TableField(exist=false)
    private List<?> list;
}

SysRoleMenuEntity.java

/**
 * 角色與菜單對應(yīng)關(guān)系
 * 
 * @author chenshun
 * @email sunlightcs@gmail.com
 * @date 2016年9月18日 上午9:28:13
 */
@TableName("sys_role_menu")
public class SysRoleMenuEntity implements Serializable {
    private static final long serialVersionUID = 1L;

    @TableId
    private Long id;

    /**
     * 角色I(xiàn)D
     */
    private Long roleId;

    /**
     * 菜單ID
     */
    private Long menuId;
}

SysUserRoleEntity.java

/**
 * 用戶與角色對應(yīng)關(guān)系
 * 
 * @author chenshun
 * @email sunlightcs@gmail.com
 * @date 2016年9月18日 上午9:28:39
 */
@TableName("sys_user_role")
public class SysUserRoleEntity implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId
    private Long id;

    /**
     * 用戶ID
     */
    private Long userId;

    /**
     * 角色I(xiàn)D
     */
    private Long roleId;
}

SysRoleEntity.java

/**
 * 角色
 * 
 * @author chenshun
 * @email sunlightcs@gmail.com
 * @date 2016年9月18日 上午9:27:38
 */
@TableName("sys_role")
public class SysRoleEntity implements Serializable {
    private static final long serialVersionUID = 1L;
    
    /**
     * 角色I(xiàn)D
     */
    @TableId
    private Long roleId;

    /**
     * 角色名稱
     */
    @NotBlank(message="角色名稱不能為空")
    private String roleName;

    /**
     * 備注
     */
    private String remark;
    
    /**
     * 創(chuàng)建者ID
     */
    private Long createUserId;

    @TableField(exist=false)
    private List<Long> menuIdList;
    
    /**
     * 創(chuàng)建時間
     */
    private Date createTime;

}

然后讓我們看看demo中是怎么配置權(quán)限的

菜單管理

首先,每個用戶的賬號對應(yīng)了他是什么角色我注,然后每個角色對應(yīng)了他有哪些菜單的權(quán)限按咒。菜單的權(quán)限有目錄迟隅、菜單但骨、授權(quán)標(biāo)識三種級別。該項目對應(yīng)的前端項目 renren-fast-vue 中智袭,會一次性從服務(wù)器獲取用戶對應(yīng)的所有角色和菜單權(quán)限奔缠,然后通過目錄和菜單級別的權(quán)限顯示左側(cè)導(dǎo)航欄,以及通過授權(quán)標(biāo)識權(quán)限顯示按鈕吼野。

認(rèn)證

核心模塊是由 Shiro 來做認(rèn)證和授權(quán)的校哎,我們先來看 config 下的 ShiroConfig.java

/**
 * Shiro配置
 *
 * @author chenshun
 * @email sunlightcs@gmail.com
 * @date 2017-04-20 18:33
 */
@Configuration
public class ShiroConfig {

    @Bean("sessionManager")
    public SessionManager sessionManager(){
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionValidationSchedulerEnabled(true);
        sessionManager.setSessionIdCookieEnabled(true);
        return sessionManager;
    }

    @Bean("securityManager")
    public SecurityManager securityManager(OAuth2Realm oAuth2Realm, SessionManager sessionManager) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(oAuth2Realm);
        securityManager.setSessionManager(sessionManager);

        return securityManager;
    }

    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);

        //oauth過濾
        Map<String, Filter> filters = new HashMap<>();
        filters.put("oauth2", new OAuth2Filter());
        shiroFilter.setFilters(filters);

        Map<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/webjars/**", "anon");
        filterMap.put("/druid/**", "anon");
        filterMap.put("/app/**", "anon");
        filterMap.put("/sys/login", "anon");
        filterMap.put("/swagger/**", "anon");
        filterMap.put("/v2/api-docs", "anon");
        filterMap.put("/swagger-ui.html", "anon");
        filterMap.put("/swagger-resources/**", "anon");
        filterMap.put("/captcha.jpg", "anon");
        filterMap.put("/**", "oauth2");
        shiroFilter.setFilterChainDefinitionMap(filterMap);

        return shiroFilter;
    }

    @Bean("lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
        proxyCreator.setProxyTargetClass(true);
        return proxyCreator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}

從 shiroFilter 這個 Bean 可以看出,系統(tǒng)使用 OAuth2Filter 這個過濾器對核心模塊資源進(jìn)行了過濾瞳步。我們先來看看登錄是怎么做的闷哆。
SysLoginController.java

/**
     * 登錄
     */
    @PostMapping("/sys/login")
    public Map<String, Object> login(@RequestBody SysLoginForm form)throws IOException {
        boolean captcha = sysCaptchaService.validate(form.getUuid(), form.getCaptcha());
        if(!captcha){
            return R.error("驗證碼不正確");
        }

        //用戶信息
        SysUserEntity user = sysUserService.queryByUserName(form.getUsername());

        //賬號不存在、密碼錯誤
        if(user == null || !user.getPassword().equals(new Sha256Hash(form.getPassword(), user.getSalt()).toHex())) {
            return R.error("賬號或密碼不正確");
        }

        //賬號鎖定
        if(user.getStatus() == 0){
            return R.error("賬號已被鎖定,請聯(lián)系管理員");
        }

        //生成token单起,并保存到數(shù)據(jù)庫
        R r = sysUserTokenService.createToken(user.getUserId());
        return r;
    }

可見登陸后 token 是存放于數(shù)據(jù)庫中的抱怔。
下面看看 filter
OAuth2Filter.java

/**
 * oauth2過濾器
 *
 * @author chenshun
 * @email sunlightcs@gmail.com
 * @date 2017-05-20 13:00
 */
public class OAuth2Filter extends AuthenticatingFilter {

    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
        //獲取請求token
        String token = getRequestToken((HttpServletRequest) request);

        if(StringUtils.isBlank(token)){
            return null;
        }

        return new OAuth2Token(token);
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if(((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())){
            return true;
        }

        return false;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        //獲取請求token,如果token不存在嘀倒,直接返回401
        String token = getRequestToken((HttpServletRequest) request);
        if(StringUtils.isBlank(token)){
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
            httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());

            String json = new Gson().toJson(R.error(HttpStatus.SC_UNAUTHORIZED, "invalid token"));

            httpResponse.getWriter().print(json);

            return false;
        }

        return executeLogin(request, response);
    }

    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        httpResponse.setContentType("application/json;charset=utf-8");
        httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
        httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
        try {
            //處理登錄失敗的異常
            Throwable throwable = e.getCause() == null ? e : e.getCause();
            R r = R.error(HttpStatus.SC_UNAUTHORIZED, throwable.getMessage());

            String json = new Gson().toJson(r);
            httpResponse.getWriter().print(json);
        } catch (IOException e1) {

        }

        return false;
    }

    /**
     * 獲取請求的token
     */
    private String getRequestToken(HttpServletRequest httpRequest){
        //從header中獲取token
        String token = httpRequest.getHeader("token");

        //如果header中不存在token屈留,則從參數(shù)中獲取token
        if(StringUtils.isBlank(token)){
            token = httpRequest.getParameter("token");
        }

        return token;
    }


}

通過這個類可以看出,項目使用 OAuth2Token 類來作為 Shiro 的 token测蘑。至于 Shiro 是怎么判斷這個 token 是否有效的灌危?我們來看看onAccessDenied(ServletRequest request, ServletResponse response)方法中最后一行,調(diào)用了executeLogin(request, response)碳胳,通過查看聲明處可以找到勇蝙,這個方法是父類AuthenticatingFilter的方法。

protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        AuthenticationToken token = createToken(request, response);
        if (token == null) {
            String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " +
                    "must be created in order to execute a login attempt.";
            throw new IllegalStateException(msg);
        }
        try {
            Subject subject = getSubject(request, response);
            subject.login(token);
            return onLoginSuccess(token, subject, request, response);
        } catch (AuthenticationException e) {
            return onLoginFailure(token, e, request, response);
        }
    }

此處調(diào)用了subject.login()方法實現(xiàn)登錄挨约。至于具體的登錄邏輯味混,就要看這個 Shiro 的 Realm 了藕帜。
OAuth2Realm.java

/**
     * 認(rèn)證(登錄時調(diào)用)
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String accessToken = (String) token.getPrincipal();

        //根據(jù)accessToken,查詢用戶信息
        SysUserTokenEntity tokenEntity = shiroService.queryByToken(accessToken);
        //token失效
        if(tokenEntity == null || tokenEntity.getExpireTime().getTime() < System.currentTimeMillis()){
            throw new IncorrectCredentialsException("token失效惜傲,請重新登錄");
        }

        //查詢用戶信息
        SysUserEntity user = shiroService.queryUser(tokenEntity.getUserId());
        //賬號鎖定
        if(user.getStatus() == 0){
            throw new LockedAccountException("賬號已被鎖定,請聯(lián)系管理員");
        }

        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, accessToken, getName());
        return info;
    }

可見它是先拿到了之前用戶登錄過后洽故,保存到 header 中的 token,然后根據(jù)這個 token 去數(shù)據(jù)庫查對應(yīng)的用戶盗誊。

認(rèn)證這部分就到這了时甚,我們重新梳理一下流程。

  1. 用戶登錄哈踱,調(diào)用SysLoginController.login()方法
  2. 瀏覽器保存返回的 token
  3. 用戶拿這個 token 去訪問網(wǎng)站
  4. 請求被 OAuth2Filter 攔截荒适,因為沒有在 Shiro 登錄,訪問禁止开镣,調(diào)用 OAuth2Filter.onAccessDenied(ServletRequest request, ServletResponse response)方法
  5. OAuth2Filter.onAccessDenied方法中執(zhí)行登錄刀诬,此時調(diào)用 OAuth2Realm 的doGetAuthenticationInfo(AuthenticationToken token)方法,查詢數(shù)據(jù)庫 token 對應(yīng)的用戶
  6. 完成登錄邪财,后面訪問不會再被攔截

以上就是登錄和認(rèn)證的流程

授權(quán)

同樣是 Shiro 完成授權(quán)陕壹,回到 OAuth2Realm ,看doGetAuthorizationInfo(PrincipalCollection principals)方法

/**
     * 授權(quán)(驗證權(quán)限時調(diào)用)
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SysUserEntity user = (SysUserEntity)principals.getPrimaryPrincipal();
        Long userId = user.getUserId();

        //用戶權(quán)限列表
        Set<String> permsSet = shiroService.getUserPermissions(userId);

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setStringPermissions(permsSet);
        return info;
    }

這個 realm 中的授權(quán)依然是在數(shù)據(jù)庫中獲取權(quán)限的树埠。
那么從哪里判斷這些權(quán)限呢糠馆?
看看 io.renre.modules.sys.controller 下面的類,會發(fā)現(xiàn)很多方法會用@RequiresPermissions修飾怎憋。于是就全局搜一下這個注解又碌,發(fā)現(xiàn) ShiroConfig 下的一個 Bean , AuthorizationAttributeSourceAdvisor會對帶有該注解的方法進(jìn)行一些處理。

public class AuthorizationAttributeSourceAdvisor extends StaticMethodMatcherPointcutAdvisor {

    private static final Logger log = LoggerFactory.getLogger(AuthorizationAttributeSourceAdvisor.class);

    private static final Class<? extends Annotation>[] AUTHZ_ANNOTATION_CLASSES =
            new Class[] {
                    RequiresPermissions.class, RequiresRoles.class,
                    RequiresUser.class, RequiresGuest.class, RequiresAuthentication.class
            };

    protected SecurityManager securityManager = null;

    /**
     * Create a new AuthorizationAttributeSourceAdvisor.
     */
    public AuthorizationAttributeSourceAdvisor() {
        setAdvice(new AopAllianceAnnotationsAuthorizingMethodInterceptor());
    }

    public SecurityManager getSecurityManager() {
        return securityManager;
    }

    public void setSecurityManager(org.apache.shiro.mgt.SecurityManager securityManager) {
        this.securityManager = securityManager;
    }

    /**
     * Returns <tt>true</tt> if the method or the class has any Shiro annotations, false otherwise.
     * The annotations inspected are:
     * <ul>
     * <li>{@link org.apache.shiro.authz.annotation.RequiresAuthentication RequiresAuthentication}</li>
     * <li>{@link org.apache.shiro.authz.annotation.RequiresUser RequiresUser}</li>
     * <li>{@link org.apache.shiro.authz.annotation.RequiresGuest RequiresGuest}</li>
     * <li>{@link org.apache.shiro.authz.annotation.RequiresRoles RequiresRoles}</li>
     * <li>{@link org.apache.shiro.authz.annotation.RequiresPermissions RequiresPermissions}</li>
     * </ul>
     *
     * @param method      the method to check for a Shiro annotation
     * @param targetClass the class potentially declaring Shiro annotations
     * @return <tt>true</tt> if the method has a Shiro annotation, false otherwise.
     * @see org.springframework.aop.MethodMatcher#matches(java.lang.reflect.Method, Class)
     */
    public boolean matches(Method method, Class targetClass) {
        Method m = method;

        if ( isAuthzAnnotationPresent(m) ) {
            return true;
        }

        //The 'method' parameter could be from an interface that doesn't have the annotation.
        //Check to see if the implementation has it.
        if ( targetClass != null) {
            try {
                m = targetClass.getMethod(m.getName(), m.getParameterTypes());
                return isAuthzAnnotationPresent(m) || isAuthzAnnotationPresent(targetClass);
            } catch (NoSuchMethodException ignored) {
                //default return value is false.  If we can't find the method, then obviously
                //there is no annotation, so just use the default return value.
            }
        }

        return false;
    }

    private boolean isAuthzAnnotationPresent(Class<?> targetClazz) {
        for( Class<? extends Annotation> annClass : AUTHZ_ANNOTATION_CLASSES ) {
            Annotation a = AnnotationUtils.findAnnotation(targetClazz, annClass);
            if ( a != null ) {
                return true;
            }
        }
        return false;
    }

    private boolean isAuthzAnnotationPresent(Method method) {
        for( Class<? extends Annotation> annClass : AUTHZ_ANNOTATION_CLASSES ) {
            Annotation a = AnnotationUtils.findAnnotation(method, annClass);
            if ( a != null ) {
                return true;
            }
        }
        return false;
    }

}

這個類繼承了 spring 的 StaticMethodMatcherPointcutAdvisor绊袋,實現(xiàn)了 aop毕匀,并且會根據(jù) realm 中獲取到的 permissions 判斷是否包含方法中聲明的@RequiresPermissions中的 permissions。如果沒有權(quán)限就會返回false癌别。

該類的構(gòu)造方法中調(diào)用了setAdvice(new AopAllianceAnnotationsAuthorizingMethodInterceptor());皂岔,AopAllianceAnnotationsAuthorizingMethodInterceptor類中會添加好幾個攔截器。

public AopAllianceAnnotationsAuthorizingMethodInterceptor() {
        List<AuthorizingAnnotationMethodInterceptor> interceptors =
                new ArrayList<AuthorizingAnnotationMethodInterceptor>(5);

        //use a Spring-specific Annotation resolver - Spring's AnnotationUtils is nicer than the
        //raw JDK resolution process.
        AnnotationResolver resolver = new SpringAnnotationResolver();
        //we can re-use the same resolver instance - it does not retain state:
        interceptors.add(new RoleAnnotationMethodInterceptor(resolver));
        interceptors.add(new PermissionAnnotationMethodInterceptor(resolver));
        interceptors.add(new AuthenticatedAnnotationMethodInterceptor(resolver));
        interceptors.add(new UserAnnotationMethodInterceptor(resolver));
        interceptors.add(new GuestAnnotationMethodInterceptor(resolver));

        setMethodInterceptors(interceptors);
    }

包括了PermissionAnnotationMethodInterceptor规个,這應(yīng)該是做權(quán)限驗證的攔截器凤薛,該攔截器初始化時傳入了PermissionAnnotationHandler,而這個 hanlder 里有這么一個方法诞仓。

/**
     * Ensures that the calling <code>Subject</code> has the Annotation's specified permissions, and if not, throws an
     * <code>AuthorizingException</code> indicating access is denied.
     *
     * @param a the RequiresPermission annotation being inspected to check for one or more permissions
     * @throws org.apache.shiro.authz.AuthorizationException
     *          if the calling <code>Subject</code> does not have the permission(s) necessary to
     *          continue access or execution.
     */
    public void assertAuthorized(Annotation a) throws AuthorizationException {
        if (!(a instanceof RequiresPermissions)) return;

        RequiresPermissions rpAnnotation = (RequiresPermissions) a;
        String[] perms = getAnnotationValue(a);
        Subject subject = getSubject();

        if (perms.length == 1) {
            subject.checkPermission(perms[0]);
            return;
        }
        if (Logical.AND.equals(rpAnnotation.logical())) {
            getSubject().checkPermissions(perms);
            return;
        }
        if (Logical.OR.equals(rpAnnotation.logical())) {
            // Avoid processing exceptions unnecessarily - "delay" throwing the exception by calling hasRole first
            boolean hasAtLeastOnePermission = false;
            for (String permission : perms) if (getSubject().isPermitted(permission)) hasAtLeastOnePermission = true;
            // Cause the exception if none of the role match, note that the exception message will be a bit misleading
            if (!hasAtLeastOnePermission) getSubject().checkPermission(perms[0]);
            
        }
    }

上面寫的很清楚了缤苫,判斷是否具有權(quán)限,如果無權(quán)限拋出異常AuthorizingException
也就是說墅拭,如果用戶的請求調(diào)用了無權(quán)限的方法活玲,會拋出異常。
接下來只需要找到哪里處理了這個異常,只需要全局搜索一下這個異常就行了舒憾。很快會發(fā)現(xiàn)RRExceptionHandler類處理了這個異常镀钓,并且給了用戶友好提示。

/**
 * 異常處理器
 * 
 * @author chenshun
 * @email sunlightcs@gmail.com
 * @date 2016年10月27日 下午10:16:19
 */
@RestControllerAdvice
public class RRExceptionHandler {
    private Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * 處理自定義異常
     */
    @ExceptionHandler(RRException.class)
    public R handleRRException(RRException e){
        R r = new R();
        r.put("code", e.getCode());
        r.put("msg", e.getMessage());

        return r;
    }

    @ExceptionHandler(NoHandlerFoundException.class)
    public R handlerNoFoundException(Exception e) {
        logger.error(e.getMessage(), e);
        return R.error(404, "路徑不存在镀迂,請檢查路徑是否正確");
    }

    @ExceptionHandler(DuplicateKeyException.class)
    public R handleDuplicateKeyException(DuplicateKeyException e){
        logger.error(e.getMessage(), e);
        return R.error("數(shù)據(jù)庫中已存在該記錄");
    }

    @ExceptionHandler(AuthorizationException.class)
    public R handleAuthorizationException(AuthorizationException e){
        logger.error(e.getMessage(), e);
        return R.error("沒有權(quán)限丁溅,請聯(lián)系管理員授權(quán)");
    }

    @ExceptionHandler(Exception.class)
    public R handleException(Exception e){
        logger.error(e.getMessage(), e);
        return R.error();
    }
}

這順便還發(fā)現(xiàn)了這個項目的異常處理是怎么做的。

到此為止探遵,我們重新梳理一下授權(quán)流程窟赏。

  1. 登錄成功后, Shiro 保存用戶的權(quán)限信息
  2. 用戶在試圖請求一個帶@RequiresPermissions的方法箱季,會被AuthorizationAttributeSourceAdvisor攔截
  3. AuthorizationAttributeSourceAdvisor添加了PermissionAnnotationMethodInterceptor攔截器涯穷,判斷用戶是否具備權(quán)限
  4. 具備權(quán)限則放行,不具備則拋出AuthorizationException
  5. 當(dāng)拋出AuthorizationException時藏雏,處理該異常拷况,給用戶友好提示

app模塊的認(rèn)證

這部分就簡單了點,用的不是 Shiro掘殴,是JWT赚瘦,只有認(rèn)證沒有授權(quán)。
還是從 config 著手杯巨,我們找到io.renren.modules.app.config.WebMvcConfig蚤告,發(fā)現(xiàn)配置了AuthorizationInterceptor

/**
 * MVC配置
 *
 * @author chenshun
 * @email sunlightcs@gmail.com
 * @date 2017-04-20 22:30
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Autowired
    private AuthorizationInterceptor authorizationInterceptor;
    @Autowired
    private LoginUserHandlerMethodArgumentResolver loginUserHandlerMethodArgumentResolver;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authorizationInterceptor).addPathPatterns("/app/**");
    }

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(loginUserHandlerMethodArgumentResolver);
    }
}

找到這個攔截器

/**
 * 權(quán)限(Token)驗證
 * @author chenshun
 * @email sunlightcs@gmail.com
 * @date 2017-03-23 15:38
 */
@Component
public class AuthorizationInterceptor extends HandlerInterceptorAdapter {
    @Autowired
    private JwtUtils jwtUtils;

    public static final String USER_KEY = "userId";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Login annotation;
        if(handler instanceof HandlerMethod) {
            annotation = ((HandlerMethod) handler).getMethodAnnotation(Login.class);
        }else{
            return true;
        }

        if(annotation == null){
            return true;
        }

        //獲取用戶憑證
        String token = request.getHeader(jwtUtils.getHeader());
        if(StringUtils.isBlank(token)){
            token = request.getParameter(jwtUtils.getHeader());
        }

        //憑證為空
        if(StringUtils.isBlank(token)){
            throw new RRException(jwtUtils.getHeader() + "不能為空", HttpStatus.UNAUTHORIZED.value());
        }

        Claims claims = jwtUtils.getClaimByToken(token);
        if(claims == null || jwtUtils.isTokenExpired(claims.getExpiration())){
            throw new RRException(jwtUtils.getHeader() + "失效努酸,請重新登錄", HttpStatus.UNAUTHORIZED.value());
        }

        //設(shè)置userId到request里服爷,后續(xù)根據(jù)userId,獲取用戶信息
        request.setAttribute(USER_KEY, Long.parseLong(claims.getSubject()));

        return true;
    }
}

很容易看到它判斷方法是否需要 Login获诈, 然后從 header 獲取 token仍源,將登錄信息設(shè)置到 request 中去。
然后看看 controller舔涎, 發(fā)現(xiàn)有的方法中需要 @LoginUser 這個參數(shù)笼踩,同樣地全局搜索,找到LoginUserHandlerMethodArgumentResolver

/**
 * 有@LoginUser注解的方法參數(shù)亡嫌,注入當(dāng)前登錄用戶
 * @author chenshun
 * @email sunlightcs@gmail.com
 * @date 2017-03-23 22:02
 */
@Component
public class LoginUserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
    @Autowired
    private UserService userService;

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterType().isAssignableFrom(UserEntity.class) && parameter.hasParameterAnnotation(LoginUser.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container,
                                  NativeWebRequest request, WebDataBinderFactory factory) throws Exception {
        //獲取用戶ID
        Object object = request.getAttribute(AuthorizationInterceptor.USER_KEY, RequestAttributes.SCOPE_REQUEST);
        if(object == null){
            return null;
        }

        //獲取用戶信息
        UserEntity user = userService.selectById((Long)object);

        return user;
    }
}

可見此處從剛剛攔截器中設(shè)進(jìn)去的 request 域的值中獲取了用戶信息嚎于。

總結(jié)一下app認(rèn)證流程。

  1. app模塊登錄挟冠,創(chuàng)建 JWT token
  2. 請求需要登錄權(quán)限的方法于购,進(jìn)入AuthorizationInterceptor查詢是否登錄
  3. 確認(rèn)已登錄,如果方法需要登錄信息知染,從 request 域中獲取

總結(jié)

權(quán)限控制部分基本就是這么實現(xiàn)的肋僧。
實際上本人認(rèn)為一個請求方法需要什么權(quán)限,可以在后臺管理系統(tǒng)配置,不一定要寫死在代碼中嫌吠。
實現(xiàn)方法也不難,首先辫诅,每個 controller 方法需要的權(quán)限可以存在數(shù)據(jù)庫中凭戴,然后讀取到 Redis 中,自己寫一個類繼承AuthorizationAttributeSourceAdvisor炕矮,每次調(diào)用方法先獲取方法對應(yīng)的權(quán)限簇宽,然后跟用戶所具備的權(quán)限比較一下。這樣就不用每次配置 url 和權(quán)限的時候都要改代碼了吧享。

本人經(jīng)驗不足魏割,如果有錯誤的地方歡迎指出。謝謝钢颂!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末钞它,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子殊鞭,更是在濱河造成了極大的恐慌遭垛,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,997評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件操灿,死亡現(xiàn)場離奇詭異锯仪,居然都是意外死亡,警方通過查閱死者的電腦和手機趾盐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評論 3 392
  • 文/潘曉璐 我一進(jìn)店門庶喜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人救鲤,你說我怎么就攤上這事久窟。” “怎么了本缠?”我有些...
    開封第一講書人閱讀 163,359評論 0 353
  • 文/不壞的土叔 我叫張陵斥扛,是天一觀的道長。 經(jīng)常有香客問我丹锹,道長稀颁,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,309評論 1 292
  • 正文 為了忘掉前任楣黍,我火速辦了婚禮匾灶,結(jié)果婚禮上鉴象,老公的妹妹穿的比我還像新娘专普。我一直安慰自己,他們只是感情好抄伍,可當(dāng)我...
    茶點故事閱讀 67,346評論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著张肾,像睡著了一般芭析。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上吞瞪,一...
    開封第一講書人閱讀 51,258評論 1 300
  • 那天馁启,我揣著相機與錄音,去河邊找鬼芍秆。 笑死惯疙,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的妖啥。 我是一名探鬼主播霉颠,決...
    沈念sama閱讀 40,122評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼荆虱!你這毒婦竟也來了蒿偎?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,970評論 0 275
  • 序言:老撾萬榮一對情侶失蹤怀读,失蹤者是張志新(化名)和其女友劉穎诉位,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體菜枷,經(jīng)...
    沈念sama閱讀 45,403評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡苍糠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,596評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了啤誊。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片岳瞭。...
    茶點故事閱讀 39,769評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖坷衍,靈堂內(nèi)的尸體忽然破棺而出寝优,到底是詐尸還是另有隱情,我是刑警寧澤枫耳,帶...
    沈念sama閱讀 35,464評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站孟抗,受9級特大地震影響迁杨,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜凄硼,卻給世界環(huán)境...
    茶點故事閱讀 41,075評論 3 327
  • 文/蒙蒙 一铅协、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧摊沉,春花似錦狐史、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,705評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽苍柏。三九已至,卻和暖如春姜贡,著一層夾襖步出監(jiān)牢的瞬間试吁,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,848評論 1 269
  • 我被黑心中介騙來泰國打工楼咳, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留熄捍,地道東北人。 一個月前我還...
    沈念sama閱讀 47,831評論 2 370
  • 正文 我出身青樓母怜,卻偏偏與公主長得像余耽,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子苹熏,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,678評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 說明:本文很多觀點和內(nèi)容來自互聯(lián)網(wǎng)以及各種資料宾添,如果侵犯了您的權(quán)益,請及時聯(lián)系我柜裸,我會刪除相關(guān)內(nèi)容缕陕。 權(quán)限管理 基...
    寇寇寇先森閱讀 7,593評論 8 76
  • 拜拜啦,晚安
    于想閱讀 302評論 0 0
  • 工作小記 今天是還蠻值得紀(jì)念的一天疙挺,一個完全由我自己開發(fā)的猜你喜歡系統(tǒng)終于初步完成了扛邑。基本流程是先將oracle里...
    耗子小王閱讀 191評論 0 0
  • 雖然大腿練得疼铐然,但是心里竟然還是有點期待晚上的訓(xùn)練蔬崩。喜歡這種感覺。 不知是不是腿的疼痛完美的掩蓋了膝蓋的疼痛搀暑,一點...
    舒暢姑娘閱讀 168評論 0 0
  • 昨夜下了一夜的雨沥阳! 夢醒時分雨聲瀝瀝。 連日下了二十多天的雨了自点。 阿彌陀佛的甘露雨桐罕, 一直撒向十方眾生。 你是向上...
    中醫(yī)李奇飛閱讀 643評論 0 1