其實本人很不喜歡獨立開發(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)證這部分就到這了时甚,我們重新梳理一下流程。
- 用戶登錄哈踱,調(diào)用
SysLoginController.login()
方法 - 瀏覽器保存返回的 token
- 用戶拿這個 token 去訪問網(wǎng)站
- 請求被 OAuth2Filter 攔截荒适,因為沒有在 Shiro 登錄,訪問禁止开镣,調(diào)用
OAuth2Filter.onAccessDenied(ServletRequest request, ServletResponse response)
方法 - 在
OAuth2Filter.onAccessDenied
方法中執(zhí)行登錄刀诬,此時調(diào)用 OAuth2Realm 的doGetAuthenticationInfo(AuthenticationToken token)
方法,查詢數(shù)據(jù)庫 token 對應(yīng)的用戶 - 完成登錄邪财,后面訪問不會再被攔截
以上就是登錄和認(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)流程窟赏。
- 登錄成功后, Shiro 保存用戶的權(quán)限信息
- 用戶在試圖請求一個帶
@RequiresPermissions
的方法箱季,會被AuthorizationAttributeSourceAdvisor
攔截 -
AuthorizationAttributeSourceAdvisor
添加了PermissionAnnotationMethodInterceptor
攔截器涯穷,判斷用戶是否具備權(quán)限 - 具備權(quán)限則放行,不具備則拋出
AuthorizationException
- 當(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)證流程。
- app模塊登錄挟冠,創(chuàng)建 JWT token
- 請求需要登錄權(quán)限的方法于购,進(jìn)入
AuthorizationInterceptor
查詢是否登錄 - 確認(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)驗不足魏割,如果有錯誤的地方歡迎指出。謝謝钢颂!