其实本人很不喜欢独立开发时使用 Java ,但由于能力有限,对其他语言的 web 开发能力不足。因此在找 Java 快速搭建项目时遇到了 renren-fast .看了一下,感觉还是挺适合用于独立开发的。但是官方的源码解析太贵,不想花这个钱,因此自己来尝试做源码分析。
首先贴上项目 README 里面的介绍和结构。
项目特点
- 友好的代码结构及注释,便于阅读及二次开发
- 实现前后端分离,通过token进行数据交互,前端再也不用关注后端技术
- 灵活的权限控制,可控制到页面或按钮,满足绝大部分的权限需求
- 页面交互使用Vue2.x,极大的提高了开发效率
- 完善的代码生成机制,可在线生成entity、xml、dao、service、vue、sql代码,减少70%以上的开发任务
- 引入quartz定时任务,可动态完成任务的添加、修改、删除、暂停、恢复及日志查看等功能
- 引入API模板,根据token作为登录令牌,极大的方便了APP接口开发
- 引入Hibernate Validator校验框架,轻松实现后端校验
- 引入云存储服务,已支持:七牛云、阿里云、腾讯云等
- 引入swagger文档支持,方便编写API接口文档
项目结构
renren-fast
├─db 项目SQL语句
│
├─common 公共模块
│ ├─aspect 系统日志
│ ├─exception 异常处理
│ ├─validator 后台校验
│ └─xss XSS过滤
│
├─config 配置信息
│
├─modules 功能模块
│ ├─app API接口模块(APP调用)
│ ├─job 定时任务模块
│ ├─oss 文件服务模块
│ └─sys 权限模块
│
├─RenrenApplication 项目启动类
│
├──resources
│ ├─mapper SQL对应的XML文件
│ └─static 静态资源
我本人就从自己读代码的顺序着手,看看这几个特点都是怎么实现的。
权限控制
该项目的权限控制我会分为以下几个部分:
- 权限设计
- 认证
- 授权
- app模块的认证
权限设计
该项目的权限主要由以下几个实体之间的关系来控制(为了不让文章太长,我把 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;
/**
* 授权(多个用逗号分隔,如:user:list,user:create)
*/
private String perms;
/**
* 类型 0:目录 1:菜单 2:按钮
*/
private Integer type;
/**
* 菜单图标
*/
private String icon;
/**
* 排序
*/
private Integer orderNum;
/**
* ztree属性
*/
@TableField(exist=false)
private Boolean open;
@TableField(exist=false)
private List<?> list;
}
SysRoleMenuEntity.java
/**
* 角色与菜单对应关系
*
* @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;
/**
* 角色ID
*/
private Long roleId;
/**
* 菜单ID
*/
private Long menuId;
}
SysUserRoleEntity.java
/**
* 用户与角色对应关系
*
* @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;
/**
* 角色ID
*/
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;
/**
* 角色ID
*/
@TableId
private Long roleId;
/**
* 角色名称
*/
@NotBlank(message="角色名称不能为空")
private String roleName;
/**
* 备注
*/
private String remark;
/**
* 创建者ID
*/
private Long createUserId;
@TableField(exist=false)
private List<Long> menuIdList;
/**
* 创建时间
*/
private Date createTime;
}
然后让我们看看demo中是怎么配置权限的
首先,每个用户的账号对应了他是什么角色,然后每个角色对应了他有哪些菜单的权限。菜单的权限有目录、菜单、授权标识三种级别。该项目对应的前端项目 renren-fast-vue 中,会一次性从服务器获取用户对应的所有角色和菜单权限,然后通过目录和菜单级别的权限显示左侧导航栏,以及通过授权标识权限显示按钮。
认证
核心模块是由 Shiro 来做认证和授权的,我们先来看 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 可以看出,系统使用 OAuth2Filter 这个过滤器对核心模块资源进行了过滤。我们先来看看登录是怎么做的。
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("账号已被锁定,请联系管理员");
}
//生成token,并保存到数据库
R r = sysUserTokenService.createToken(user.getUserId());
return r;
}
可见登陆后 token 是存放于数据库中的。
下面看看 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,则从参数中获取token
if(StringUtils.isBlank(token)){
token = httpRequest.getParameter("token");
}
return token;
}
}
通过这个类可以看出,项目使用 OAuth2Token 类来作为 Shiro 的 token。至于 Shiro 是怎么判断这个 token 是否有效的?我们来看看onAccessDenied(ServletRequest request, ServletResponse response)
方法中最后一行,调用了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);
}
}
此处调用了subject.login()
方法实现登录。至于具体的登录逻辑,就要看这个 Shiro 的 Realm 了。
OAuth2Realm.java
/**
* 认证(登录时调用)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String accessToken = (String) token.getPrincipal();
//根据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("账号已被锁定,请联系管理员");
}
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, accessToken, getName());
return info;
}
可见它是先拿到了之前用户登录过后,保存到 header 中的 token,然后根据这个 token 去数据库查对应的用户。
认证这部分就到这了,我们重新梳理一下流程。
- 用户登录,调用
SysLoginController.login()
方法 - 浏览器保存返回的 token
- 用户拿这个 token 去访问网站
- 请求被 OAuth2Filter 拦截,因为没有在 Shiro 登录,访问禁止,调用
OAuth2Filter.onAccessDenied(ServletRequest request, ServletResponse response)
方法 - 在
OAuth2Filter.onAccessDenied
方法中执行登录,此时调用 OAuth2Realm 的doGetAuthenticationInfo(AuthenticationToken token)
方法,查询数据库 token 对应的用户 - 完成登录,后面访问不会再被拦截
以上就是登录和认证的流程
授权
同样是 Shiro 完成授权,回到 OAuth2Realm ,看doGetAuthorizationInfo(PrincipalCollection principals)
方法
/**
* 授权(验证权限时调用)
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SysUserEntity user = (SysUserEntity)principals.getPrimaryPrincipal();
Long userId = user.getUserId();
//用户权限列表
Set<String> permsSet = shiroService.getUserPermissions(userId);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setStringPermissions(permsSet);
return info;
}
这个 realm 中的授权依然是在数据库中获取权限的。
那么从哪里判断这些权限呢?
看看 io.renre.modules.sys.controller
下面的类,会发现很多方法会用@RequiresPermissions
修饰。于是就全局搜一下这个注解,发现 ShiroConfig
下的一个 Bean , AuthorizationAttributeSourceAdvisor
会对带有该注解的方法进行一些处理。
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
,实现了 aop,并且会根据 realm 中获取到的 permissions 判断是否包含方法中声明的@RequiresPermissions
中的 permissions。如果没有权限就会返回false。
该类的构造方法中调用了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
,这应该是做权限验证的拦截器,该拦截器初始化时传入了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]);
}
}
上面写的很清楚了,判断是否具有权限,如果无权限抛出异常AuthorizingException
也就是说,如果用户的请求调用了无权限的方法,会抛出异常。
接下来只需要找到哪里处理了这个异常,只需要全局搜索一下这个异常就行了。很快会发现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("数据库中已存在该记录");
}
@ExceptionHandler(AuthorizationException.class)
public R handleAuthorizationException(AuthorizationException e){
logger.error(e.getMessage(), e);
return R.error("没有权限,请联系管理员授权");
}
@ExceptionHandler(Exception.class)
public R handleException(Exception e){
logger.error(e.getMessage(), e);
return R.error();
}
}
这顺便还发现了这个项目的异常处理是怎么做的。
到此为止,我们重新梳理一下授权流程。
- 登录成功后, Shiro 保存用户的权限信息
- 用户在试图请求一个带
@RequiresPermissions
的方法,会被AuthorizationAttributeSourceAdvisor
拦截 -
AuthorizationAttributeSourceAdvisor
添加了PermissionAnnotationMethodInterceptor
拦截器,判断用户是否具备权限 - 具备权限则放行,不具备则抛出
AuthorizationException
- 当抛出
AuthorizationException
时,处理该异常,给用户友好提示
app模块的认证
这部分就简单了点,用的不是 Shiro,是JWT,只有认证没有授权。
还是从 config 着手,我们找到io.renren.modules.app.config.WebMvcConfig
,发现配置了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);
}
}
找到这个拦截器
/**
* 权限(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());
}
//设置userId到request里,后续根据userId,获取用户信息
request.setAttribute(USER_KEY, Long.parseLong(claims.getSubject()));
return true;
}
}
很容易看到它判断方法是否需要 Login, 然后从 header 获取 token,将登录信息设置到 request 中去。
然后看看 controller, 发现有的方法中需要 @LoginUser 这个参数,同样地全局搜索,找到LoginUserHandlerMethodArgumentResolver
类
/**
* 有@LoginUser注解的方法参数,注入当前登录用户
* @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;
}
}
可见此处从刚刚拦截器中设进去的 request 域的值中获取了用户信息。
总结一下app认证流程。
- app模块登录,创建 JWT token
- 请求需要登录权限的方法,进入
AuthorizationInterceptor
查询是否登录 - 确认已登录,如果方法需要登录信息,从 request 域中获取
总结
权限控制部分基本就是这么实现的。
实际上本人认为一个请求方法需要什么权限,可以在后台管理系统配置,不一定要写死在代码中。
实现方法也不难,首先,每个 controller 方法需要的权限可以存在数据库中,然后读取到 Redis 中,自己写一个类继承AuthorizationAttributeSourceAdvisor
,每次调用方法先获取方法对应的权限,然后跟用户所具备的权限比较一下。这样就不用每次配置 url 和权限的时候都要改代码了。
本人经验不足,如果有错误的地方欢迎指出。谢谢!