一直在学习项目的开发,但是发现忽略了很重要的问题,就是如何根据原型图转换成需要的功能代码,这一块儿感觉有模糊,所以写一篇笔记记录一下,自己的思考开发和已有框架的开发是否有相似或区别。
注意,下面的所有代码段都不是完整的!只根据大概逻辑写重点的部分,详细代码请看若依框架源码!
目录
4.接口包含的业务逻辑
4.1业务逻辑前的基本配置(涉及到配置)
4.1.1 权限配置
4.1.2 redis 的配置
4.1.3 验证码配置
4.1.4 jjwt token 配置
4.2业务逻辑的设计
4.2.1.注册页面(验证码+redis)
4.2.2.登录页面
4.2.3.用户管理页面
分页逻辑
接口返回类型类
动态数据权限
业务逻辑中会涉及到所用工具的配置,比如 security 相关的,redis 相关的,大概说明一下业务逻辑。逻辑包括 controller 和service 层的。
配置不是一下子就能思考全的,我们在思考业务的时候会不断的添加配置,不断的补充配置,在后续的业务逻辑添加时,也要有所说明,否则会很容易遗忘。
4.1.1 到 4.1.4 是注册登录以及权限会用到的基本的配置,我们可以先添加进来,其中不是必须的可以先不详细添加;
开始前,先考虑一下用户模块需要的配置,首先需要有权限配置,我们使用 SpringSecurity ,我们需要实现几部分功能,
1.过滤请求,黑白名单访问;
2.自定义登录身份认证;
3.密码加密编码解码;
4.认证失败处理、退出处理;
我们先将涉及到的类和方法添加上,涉及到实体信息的,先将必须会使用的列出来,例如:自定义用户信息中的权限;当前不是必须的就忽略,例如部门、邮箱等信息;
相关的类有 6 个:
//1.security 配置类,主要处理过滤请求、处理器配置、过滤链配置
package com.ruoyi.framework.config;
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter{
。。。
}
//2.自定义用户处理类,
package com.ruoyi.framework.web.service;
@Service
public class UserDetailsServiceImpl implements UserDetailsService{
//获取要登陆的用户信息,没有就抛异常,AuthenticationManager 的子类中进行密码对比
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
。。。
return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
}
}
//3.自定义用户信息类
package com.ruoyi.common.core.domain.model;
public class LoginUser implements UserDetails{
。。。
//需要继承 get 权限、密码、用户名等的方法
//同时我们也可以扩展,例如部门等信息,这里我们不需要默认提供的方法,我们使用我们自定义的 Set permissions,因为后面我们使用自定义的权限认证的方式。
public LoginUser(Long userId, 。。。, Set permissions)
{
。。。
}
}
//4.自定义token过滤器,验证token有效,有效就会向security中添加当前用户信息
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter{
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException
{
//这里需要判断 token 是否能获取到用户,并且是否有效,需要结合登陆时的token配置。可以暂时省略
。。。
}
}
//5.认证失败处理类 返回未授权
package com.ruoyi.framework.security.handle;
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable
{
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException{
。。。
}
}
//6.自定义退出处理类 返回成功
package com.ruoyi.framework.security.handle;
@Configuration
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler
{
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException{
}
}
在设计 3. 4.类的时候,需要结合4.1.2的登录认证进行设计,会使用到 jjwt token 的相关配置,我们可以先捋一捋,我们一整个权限认证的大致流程是:
1.登陆时获取用户信息,并将权限放到 Set
permissions 中; 2.如果登陆成功了,就将生成 uuid 并作为 key ,将信息保存到 redis 里,并返回客户端token;
3.等下次访问接口时,会再过滤器中根据token获取令牌 uuid ,再通过key=uuid获取redis里面的用户信息,从用户信息中获取权限等操作。
我们上面的权限模块就涉及到了 redis 的使用,我们先配置使用 bean ,后面为了方便使用,会创建 redis 的工具类。
//1.redis 的配置
package com.ruoyi.framework.config;
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport
{
//RedisTemplate是spring对redis操作的的封装
@Bean
@SuppressWarnings(value = { "unchecked", "rawtypes" })
public RedisTemplate
验证码配置只要创建对应的bean对象就可以
//1.验证码配置
package com.ruoyi.framework.config;
@Configuration
public class CaptchaConfig
{
@Bean(name = "captchaProducer")
public DefaultKaptcha getKaptchaBean()
{
。。。
}
@Bean(name = "captchaProducerMath")
public DefaultKaptcha getKaptchaBeanMath()
{
。。。
}
}
//2.验证码文本生成器
package com.ruoyi.framework.config;
public class KaptchaTextCreator extends DefaultTextCreator
{
。。。
}
这个配置,在登录以及接口访问过滤时会使用到,因为里面的逻辑我一直不太熟悉,就先把会用到的方法全写出来,方便记住。总体来说,登录认证所需要的相关方法,主要有两大块:
1.登陆时创建token,要有:1.根据令牌创建token、2.存储用户信息到 redis ;
2.访问接口时根据token获取令牌进而获取用户信息,要有:1.验证token有效性、2.根据token获取令牌、3.根据令牌从 redis 中获取用户信息;
//1.token验证处理
package com.ruoyi.framework.web.service;
@Component
public class TokenService{
/**
* 创建令牌
*
* @param loginUser 用户信息
* @return 令牌
*/
public String createToken(LoginUser loginUser)
{
//创建 uuid
String token = IdUtils.fastUUID();
loginUser.setToken(token);
setUserAgent(loginUser);
//并将 uuid 作为 key ,loginUser(set过期时间)作为value存到 redis 里面,
refreshToken(loginUser);
Map claims = new HashMap<>();
//将 uuid 作为令牌存到 token 里面
claims.put(Constants.LOGIN_USER_KEY, token);
return createToken(claims);
}
/**
* 刷新令牌有效期
*
* @param loginUser 登录信息
*/
public void refreshToken(LoginUser loginUser)
{
loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
// 根据uuid将loginUser缓存
String userKey = getTokenKey(loginUser.getToken());
redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
}
/**
* 从数据声明生成令牌
*
* @param claims 数据声明
* @return 令牌
*/
private String createToken(Map claims)
{
String token = Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret).compact();
return token;
}
/**
* 获取用户身份信息
*
* @return 用户信息
*/
public LoginUser getLoginUser(HttpServletRequest request)
{
// 从 request 中获取请求携带的令牌
String token = getToken(request);
if (StringUtils.isNotEmpty(token))
{
try
{
//从 token 中获取令牌
Claims claims = parseToken(token);
// 从令牌中解析对应的 uuid
String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
String userKey = getTokenKey(uuid);
//将 uuid 作为 key 从 redis 中获取用户信息
LoginUser user = redisCache.getCacheObject(userKey);
return user;
}
catch (Exception e)
{
}
}
return null;
}
/**
* 获取请求token
*
* @param request
* @return token
*/
private String getToken(HttpServletRequest request)
{
String token = request.getHeader(header);
if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX))
{
token = token.replace(Constants.TOKEN_PREFIX, "");
}
return token;
}
/**
* 从令牌中获取数据声明
*
* @param token 令牌
* @return 数据声明
*/
private Claims parseToken(String token)
{
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
}
添加完基础的配置,就可以开始思考业务逻辑啦,其中思考时肯定有配置及方法是不完整的,我们后续要逐步进行添加,一定要说明在哪一部分添加的。
思考时先想清楚逻辑,想清楚后再编程代码,其中要记录下来补充了哪些配置,注意,下面的所有代码段都不是完整的!只根据大概逻辑写重点的部分,详细代码请看若依框架源码!。
注意4.2中的业务逻辑都是只针对业务,数据和菜单权限见 4.3!!!
先总结逻辑:
1.用户进入注册页面,先获取到验证码图片,其中生成验证码接口业务逻辑:
1.使用验证码工具创建验证码code,然后根据code产生图片;
2.获取一个随机uuid作为存储key,将code作为 value ,添加到 redis 中,设置 10 秒的有效期;
3.将 uuid 和 图片返回给客户端;
2.用户填写注册信息,并提交,其中注册业务逻辑:
1.获取到 uuid 和验证码,从 redis 中获取到 key = uuid 的值,若没有获取到值,抛出验证码失效;若获取到值但是不匹配,抛出验证码错误;若有并且匹配成功往下走;
2.判断数据项是否校验成功,若不成功,则返回 error 格式;若成功则往下走;
3.编码化密码然后调用持久层保存数据,根据 insert 返回值boolean 判断是否成功,成功后 success(),失败返回 error();
涉及到controller接口类:
1.生成验证码
package com.ruoyi.web.controller.common;
@RestController
public class CaptchaController
{
@GetMapping("/captchaImage")
public AjaxResult getCode(HttpServletResponse response) throws IOException
{
AjaxResult ajax = AjaxResult.success();
。。。
ajax.put("uuid", uuid);
ajax.put("img", Base64.encode(os.toByteArray()));
return ajax;
}
}
2.注册功能
package com.ruoyi.web.controller.system;
@RestController
public class SysRegisterController extends BaseController
{
@PostMapping("/register")
public AjaxResult register(@RequestBody RegisterBody user)
{
。。。
String msg = registerService.register(user);
return StringUtils.isEmpty(msg) ? success() : error(msg);
}
}
涉及到service业务处理类:
1.注册校验业务类,这个类属于架构层面的业务处理,不是系统层面的(和数据表对应的service不一样,比他层高)(架构级别的)
package com.ruoyi.framework.web.service;
@Component
public class SysRegisterService
{
/**
* 注册
*/
public String register(RegisterBody registerBody)
{
。。。
//1.验证验证码是否有效且正确
validateCaptcha(registerBody.getCode(), registerBody.getUuid());
//2.校验必填、格式等
//3.校验用户是否已注册,调用系统用户模块 userService 的方法,未注册才往下走
userService.checkUserNameUnique(username);
//4.走到这里说明验证成功并且没注册,先 encode 一下密码,然后调用系统用户模块 userService 的方法,该方法里面会调用持久层,直接 insertUser 。
boolean regFlag = userService.registerUser(sysUser);
return msg;
}
/**
* 校验验证码
* @param code 填写的验证码
* @param uuid 唯一标识
*/
public void validateCaptcha(String code, String uuid)
{
//从 redis 里面获取验证码,没获取到说明过期了;获取但判等失败说明不正确;获取到且判等成功说明成功,直接放行;
}
}
2.用户-业务层处理,对应的接口就不写了(系统级别的)
package com.ruoyi.system.service.impl;
@Service
public class SysUserServiceImpl implements ISysUserService
{
/**
* 注册用户信息
*
* @param user 用户信息
* @return 结果
*/
@Override
public boolean registerUser(SysUser user)
{
return userMapper.insertUser(user) > 0;
}
/**
* 校验用户名称是否唯一
*
* @param userName 用户名称
* @return 结果
*/
@Override
public String checkUserNameUnique(String userName)
{
int count = userMapper.checkUserNameUnique(userName);
if (count > 0)
{
return UserConstants.NOT_UNIQUE;
}
return UserConstants.UNIQUE;
}
}
涉及到持久层的逻辑:
具体看5.
先总结逻辑:
1.用户进入登录页面,先获取到验证码图片,其中生成验证码接口业务逻辑与注册一致;
2.用户填写登录信息和验证码信息,并提交,其中登录业务逻辑:
1.判断验证码是否有效,无效抛出异常,有效往下走;
2.判断是否能够根据用户名和密码获取到用户信息并判断密码是否正确,不正确就抛异常,正确就往下走;
3.随机产生 uuid ,并将其作为 key ,用户信息作为 value 保存到 redis 里面,再将 uuid 添加到令牌中并创建 token ,返给客户端;
3.登录成功后需要获取到当前用户基本信息
1.调用jwt拦截器,会拦截所有请求,先根据 request 获取 token ,根据 token 在 redis 里获取到用户信息,然后从用户信息中获取失效时间,判断 token是否有效,无效就直接调用下一个拦截器,有效就往下走;
2.token有效就将用户信息转化成我们需要的 AuthenticationToken 格式,并放到 SecurityContextHolder 里面,方便当前请求的后续业务使用。然后调用下一个拦截器
3.执行到核心接口的时候,先从 SecurityContextHolder 里面获取到当前请求线程里的用户信息,然后调用对应的业务接口,例如:角色、菜单权限等,最后放到 AjaxResult 里面返回给客户端。这里需要跟前端核对:如果是超级管理员角色可以直接填充admin,数据直接填充*:*:*,如果不是从数据库中获取
4.登录成功后需要获取到当前用户能看到得路由信息
1.调用jwt拦截器,会拦截所有请求,先根据 request 获取 token ,根据 token 在 redis 里获取到用户信息,然后从用户信息中获取失效时间,判断 token是否有效,无效就直接调用下一个拦截器,有效就往下走;
2.token有效就将用户信息转化成我们需要的 AuthenticationToken 格式,并放到 SecurityContextHolder 里面,方便当前请求的后续业务使用。然后调用下一个拦截器
3.执行到核心接口的时候,先从 SecurityContextHolder 里面获取到当前请求线程里的用户信息,然后调用对应的业务接口,例如:角色权限等,最后放到 AjaxResult 里面返回给客户端。
这里需要注意,我们在 3. 4. 中使用了一个公共接口拦截器功能,在之后的所有接口调用中,都会经过这个过滤器,之后就不再特殊说明了,它的业务逻辑就是:
1.拦截请求,通过请求判断能否获取登录用户信息,获取到就保存到请求线程里,然后调用下个拦截器(这里的保存只是保存到当前请求线程里面,在请求结束时会销毁的);没有获取就不保存,直接调用下个拦截器;
详细的:
1.调用jwt拦截器,会拦截所有请求,先根据 request 获取 token ,根据 token 在 redis 里获取到用户信息,获取不到就直接调用下一个拦截器,能够获取到说明用户没有失效,判断是否该刷新一下刷新令牌有效期,然后往下走;
2.token有效就将用户信息转化成我们需要的 AuthenticationToken 格式,并放到 SecurityContextHolder 里面,方便当前请求的后续业务使用。然后调用下一个拦截器
涉及到controller接口类:
1.生成验证码
//和注册一样
2.登录功能
package com.ruoyi.web.controller.system;
@RestController
public class SysRegisterController extends BaseController
{
/**
* 登录方法
*
* @param loginBody 登录信息
* @return 结果
*/
@PostMapping("/login")
public AjaxResult login(@RequestBody LoginBody loginBody)
{
AjaxResult ajax = AjaxResult.success();
// 生成令牌
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
loginBody.getUuid());
ajax.put(Constants.TOKEN, token);
return ajax;
}
/**
* 获取用户信息
*
* @return 用户信息
*/
@GetMapping("getInfo")
public AjaxResult getInfo()
{
SysUser user = SecurityUtils.getLoginUser().getUser();
// 角色集合
Set roles = permissionService.getRolePermission(user);
// 权限集合
Set permissions = permissionService.getMenuPermission(user);
AjaxResult ajax = AjaxResult.success();
ajax.put("user", user);
ajax.put("roles", roles);
ajax.put("permissions", permissions);
return ajax;
}
/**
* 获取路由信息
*
* @return 路由信息
*/
@GetMapping("getRouters")
public AjaxResult getRouters()
{
Long userId = SecurityUtils.getUserId();
List menus = menuService.selectMenuTreeByUserId(userId);
return AjaxResult.success(menuService.buildMenus(menus));
}
}
涉及到service业务处理类:
1.登录校验业务类,(架构级别的)
package com.ruoyi.framework.web.service;
@Component
public class SysLoginService
{
public String login(String username, String password, String code, String uuid)
{
。。。
//1.验证验证码是否有效且正确
validateCaptcha(registerBody.getCode(), registerBody.getUuid());
//2.校验必填、格式等
try{
//3.判断是否有用户,并且密码是否匹配;
//该方法会去调用 provider 中的 retrieveUser() 方法(实际上是调用UserDetailsServiceImpl.loadUserByUsername),获取数据库中的用户信息,然后再调用 additionalAuthenticationChecks() 方法判断密码是否正确;
//若密码正确会返回用户信息,若密码不正确会抛出异常,然后在 catch 里面进行捕捉,饭后我们自定义的UserPasswordNotMatchException异常
authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(username, password));
}
catch (Exception e)
{
if (e instanceof BadCredentialsException)
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
throw new UserPasswordNotMatchException();
}
}
//4.根据用户信息,调取 tokenService 生成token
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
return tokenService.createToken(loginUser);
}
/**
* 获取用户信息
*
* @return 用户信息
*/
@GetMapping("getInfo")
public AjaxResult getInfo()
{
SysUser user = SecurityUtils.getLoginUser().getUser();
// 角色集合
Set roles = permissionService.getRolePermission(user);
// 权限集合
Set permissions = permissionService.getMenuPermission(user);
AjaxResult ajax = AjaxResult.success();
ajax.put("user", user);
ajax.put("roles", roles);
ajax.put("permissions", permissions);
return ajax;
}
/**
* 获取路由信息
*
* @return 路由信息
*/
@GetMapping("getRouters")
public AjaxResult getRouters()
{
Long userId = SecurityUtils.getUserId();
List menus = menuService.selectMenuTreeByUserId(userId);
return AjaxResult.success(menuService.buildMenus(menus));
}
}
2.用户权限处理(架构级别的)
package com.ruoyi.framework.web.service;
@Component
public class SysPermissionService
{
/**
* 获取角色数据权限
*
* @param user 用户信息
* @return 角色权限信息
*/
public Set getRolePermission(SysUser user)
{
Set roles = new HashSet();
// 管理员拥有所有权限
if (user.isAdmin())
{
roles.add("admin");
}
else
{
roles.addAll(roleService.selectRolePermissionByUserId(user.getUserId()));
}
return roles;
}
/**
* 获取菜单数据权限
*
* @param user 用户信息
* @return 菜单权限信息
*/
public Set getMenuPermission(SysUser user)
{
Set perms = new HashSet();
// 管理员拥有所有权限
if (user.isAdmin())
{
perms.add("*:*:*");
}
else
{
perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId()));
}
return perms;
}
}
3.系统级别的,就不详细加了,特殊情况在特殊说明,这些类主要操作持久层
@Autowired
private SysMenuMapper menuMapper;
@Autowired
private SysRoleMapper roleMapper;
@Autowired
private SysRoleMenuMapper roleMenuMapper;
涉及到持久层的逻辑:
具体看
注意:现在只考虑菜单权限,不考虑数据权限! 后续链接待加上
分析逻辑时发现列表几乎都会依赖于分页逻辑,所以 controller 基础类里面,可以加上默认分页逻辑:
分页主要有两个逻辑:1.设置分页;2.返回分页类型的数据
1.
package com.ruoyi.common.core.controller;
public class BaseController
{
/**
* 设置请求分页数据,是为了代码简洁,分页数等数据可以不写在web层,直接通过工具获取请求数据注入
*/
protected void startPage()
{
PageUtils.startPage();
}
/**
* 响应请求分页数据,有分页自然就有分页类型的返回数据
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
protected TableDataInfo getDataTable(List> list)
{
TableDataInfo rspData = new TableDataInfo();
rspData.setCode(HttpStatus.SUCCESS);
rspData.setMsg("查询成功");
rspData.setRows(list);
rspData.setTotal(new PageInfo(list).getTotal());
return rspData;
}
}
2.package com.ruoyi.common.utils;
/**
* 分页工具类
*
*/
public class PageUtils extends PageHelper
{
/**
* 设置请求分页数据
*/
public static void startPage()
{
//1.最主要的一个工具,主要是从请求信息中获取到 每页数、当前页、排序等信息
PageDomain pageDomain = TableSupport.buildPageRequest();
Integer pageNum = pageDomain.getPageNum();
Integer pageSize = pageDomain.getPageSize();
if (StringUtils.isNotNull(pageNum) && StringUtils.isNotNull(pageSize))
{
String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());
Boolean reasonable = pageDomain.getReasonable();
//2.最重要实现分页的的语句
PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable);
}
}
}
3.
package com.ruoyi.common.core.page;
/**
* 表格数据处理
*
*/
public class TableSupport
{
/**
* 当前记录起始索引
*/
public static final String PAGE_NUM = "pageNum";
。。。
/**
* 封装分页对象
*/
public static PageDomain getPageDomain()
{
//1.自定义的分页实体类
PageDomain pageDomain = new PageDomain();
//2.通过ServletUtils 工具类,获取请求中传递参数为 key 的对应参数
pageDomain.setPageNum(ServletUtils.getParameterToInt(PAGE_NUM));
pageDomain.setPageSize(ServletUtils.getParameterToInt(PAGE_SIZE));
pageDomain.setOrderByColumn(ServletUtils.getParameter(ORDER_BY_COLUMN));
pageDomain.setIsAsc(ServletUtils.getParameter(IS_ASC));
pageDomain.setReasonable(ServletUtils.getParameterToBool(REASONABLE));
return pageDomain;
}
public static PageDomain buildPageRequest()
{
return getPageDomain();
}
}
4.表格分页数据对象
package com.ruoyi.common.core.page;
/**
* 表格分页数据对象
*
* @author ruoyi
*/
public class TableDataInfo implements Serializable
{
/** 总记录数 */
private long total;
/** 列表数据 */
private List> rows;
/** 消息状态码 */
private int code;
/** 消息内容 */
private String msg;
}
在进行新增用户逻辑时,发现数据返回的格式可以封装成一个返回类主要用于返回操作消息:
1.
package com.ruoyi.common.core.domain;
/**
* 操作消息提醒
*
*/
public class AjaxResult extends HashMap
{
private static final long serialVersionUID = 1L;
/** 状态码 */
public static final String CODE_TAG = "code";
/** 返回内容 */
public static final String MSG_TAG = "msg";
/** 数据对象 */
public static final String DATA_TAG = "data";
/**
* 初始化一个新创建的 AjaxResult 对象
*
* @param code 状态码
* @param msg 返回内容
* @param data 数据对象
*/
public AjaxResult(int code, String msg, Object data)
{
super.put(CODE_TAG, code);
super.put(MSG_TAG, msg);
if (StringUtils.isNotNull(data))
{
super.put(DATA_TAG, data);
}
}
。。。
}
1.新增、修改等非查询sql操作时,持久层会返回执行结果的个数,所以我们在公共 controller 中添加一个统一的返回类型
package com.ruoyi.common.core.controller;
/**
* web层通用数据处理
*
*/
public class BaseController
{
。。。
/**
* 响应返回结果
*
* @param rows 影响行数
* @return 操作结果
*/
protected AjaxResult toAjax(int rows)
{
return rows > 0 ? AjaxResult.success() : AjaxResult.error();
}
}
先总结逻辑:
1.用户进入页面,调用获取用户列表的接口,此处有分页;
1.经过过滤链等后,进入接口,先添加分页逻辑;
2.进入 userService 前,先被切面类拦截到,向实体类(基础实体类)中的 param 添加 sql 动态,再进入 userService 获取列表;
3.返回分页类型的数据
这里涉及到了数据权限,指判断当前登录用户是否有操作当前数据的权限,这里需要配合角色来进行判断。
角色模块中有一个设置数据权限的操作,里面有几个数据权限类型:
我们这里要实现动态数据权限获取,由于其他的模块也需要有动态的数据权限业务,所以需要做成一个公共的方便使用的,并且能够不影响主要开发业务的,试想一下如果一开始没有数据权限的业务,我们就会获取所有的数据,如果中途添加数据权限业务,我们就需要修改代码,而且需要修改很多业务。如果按照这个数据权限编写代码后,后续再有其他的数据权限判定业务就又需要修改。所以就需要有一个能够方便使用的动态数据权限。
思考一下,数据权限最核心的是通过mybatis 的 sql 的 select 语句,根据 where 中的条件进行判断,如果要改成动态的,那就最好不要在 mybatis 中添加具体的条件语句,我们可以直接传一个动态的值,值里面可以是条件语句,例如: AND u.status = #{status};
现在考虑在哪里添加动态的sql 值,调用持久层的是业务层service,如果直接加在 service 层,那么其他模块中也需要加载 service ,这就会有代码重复了。所以我们可以把编辑动态值的操作抽象出来,能实现的就是动态切面!
我们直接写一个动态SQL过滤的切面类,添加一个切入点,设置成通过注解进行拦截,拦截到之后,根据当前登录用户信息中的权限类型,添加不同的动态sql ,然后就需要将动态sql添加到被拦截也就是要执行的service 方法中。
因为aop原理是反射,所以我们可以获取到当前被拦截方法的所有形参,1.修改被拦截方法的形参,里面添加一个参数,专门存放动态sql,但是这种方法会修改业务方法!2.因为被拦截方法是获取列表类型的,一般会伴随着很多查询条件,所以我们可以直接controller层传值实体类。那么就只需要修改实体类的基础类 BaseEntity ,向里面添加一个 param 参数。
所以,执行流程就是:congtroller层调用service接口前,拦截到接口有某注解,进入连接点方法,一顿操作后,形参的实体类中携带着动态 SQL 值进入service 方法,然后再调用 mapper 方法,将动态 SQL 值拼接到 sql语句中。
1.实体基类中添加动态语句参数以及 set get 方法
package com.ruoyi.common.core.domain;
/**
* Entity基类
*
*/
public class BaseEntity implements Serializable
{
。。。
/** 请求参数 */
private Map params;
}
2.自定义的数据过滤注解类,可以传值,这里的传值主要是根据业务进行添加,因为我们的业务是数据跟随部门或个人而定的(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限 5:仅本人数据权限),相关的表只有部门和用户,所以传值这两个。
写死也可以,根据业务判断
package com.ruoyi.common.annotation;
/**
* 数据权限过滤注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope
{
/**
* 部门表的别名
*/
public String deptAlias() default "";
/**
* 用户表的别名
*/
public String userAlias() default "";
}
3.数据过滤切面类
package com.ruoyi.framework.aspectj;
/**
* 数据过滤处理
*/
@Aspect
@Component
public class DataScopeAspect
{
//拦截有这个注解的方法
@Before("@annotation(controllerDataScope)")
// DataScope 这个是注解参数,需要获取里面的参数
public void doBefore(JoinPoint point, DataScope controllerDataScope) throws Throwable
{
//1.拼接权限sql前先清空params.dataScope参数防止注入(防止前端注入)
clearDataScope(point);
//2.根据传的参数和当前登录的用户添加动态的 SQL
handleDataScope(point, controllerDataScope);
}
//具体的sql语句看源码吧,重要的就是将 sql 添加到形参中:
//获取到当前请求 service 代理类的方法的某个请求参数,例如:selectUserList(SysUser user),这个的getArgs()[0]就是 SysUser
//Object params = joinPoint.getArgs()[0];
//if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
//{
//向 SysUser 参数中的父类属性 params 添加数据,会在 mapper 层使用
//BaseEntity baseEntity = (BaseEntity) params;
//baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");
//}
}
4.向需要的service 方法中添加注解
//@Override
//@DataScope(deptAlias = "d", userAlias = "u")
//public List selectUserList(SysUser user)
//{
//return userMapper.selectUserList(user);
//}
5.修改 mapper.xml 文件,向需要的语句中添加 ${params.dataScope}
完成!
2.新增用户,
1.经过过滤链等后,进入接口;
2.判断新增的用户名、手机号码等唯一数据是否已存在;若是则返回 error,若不是则往下走;
3.获取当前登录的用户名,并设置成创建人;
4.将密码进行强编码;
5.调用 service 进行 insert 写入;
6.service 返回值如果大于 0 就返回 success(),否则就返回error();
3.修改用户
1.经过过滤链等后,进入接口;
2.检查被操作的用户信息是否是超级管理员,如果是就抛出异常,如果不是就往下走;
3.检查操作的用户信息是否有被操作用户的数据权限,如果是就往下走,如果不是就抛出异常;
4.判断新增的用户名、手机号码等唯一数据是否已存在;若是则返回 error,若不是则往下走;
5.获取当前登录的用户名,并设置成更新人;
6.调用 service 进行 update 写入;
7.service 返回值如果大于 0 就返回 success(),否则就返回error();
由于,修改等操作时,需要判断数据权限,可以直接在 service 方法中调用同类添加 @DataScope 注解的 service 方法,这样就可以直接传操作得 id 值,并且不用写重复代码。
看代码:
1.
package com.ruoyi.system.service.impl;
/**
* 用户 业务层处理
*
*/
@Service
public class SysUserServiceImpl implements ISysUserService
{
/**
* 校验用户是否有数据权限
*
* @param userId 用户id
*/
@Override
public void checkUserDataScope(Long userId)
{
if (!SysUser.isAdmin(SecurityUtils.getUserId()))
{
SysUser user = new SysUser();
user.setUserId(userId);
//这里调用的是当前aop代理对象类的类型的bean ,的列表查询方法
List users = SpringUtils.getAopProxy(this).selectUserList(user);
if (StringUtils.isEmpty(users))
{
throw new ServiceException("没有权限访问用户数据!");
}
}
}
}
4.批量删除用户
1.经过过滤链等后,进入接口;
2.检查被删除的用户中是否包含当前登录用户,如果是就返回 error,如果不是就往下走;
3.for循环:检查被操作的用户信息是否是超级管理员,如果是就抛出异常,如果不是就往下走;
4.for循环:检查操作的用户信息是否有被操作用户的数据权限,如果是就往下走,如果不是就抛出异常;
5.批量删除用户与角色关联
6.批量删除用户
7.service 返回值如果大于 0 就返回 success(),否则就返回error();
5.重置单个用户密码
1.经过过滤链等后,进入接口;
2.检查被操作的用户信息是否是超级管理员,如果是就抛出异常,如果不是就往下走;
3.检查操作的用户信息是否有被操作用户的数据权限,如果是就往下走,如果不是就抛出异常;
4.调用SecurityUtil编码化密码;
5.获取当前登录的用户名,并设置成更新人;
6.调用 service 进行 update 写入;
7.service 返回值如果大于 0 就返回 success(),否则就返回error();
6.10.用户进入页面,获取部门树级列表,此处数据列表需要和前端匹配(具体业务在部门模块);
7.获取指定用户角色列表,此处有分页;
1.经过过滤链等后,进入接口,先添加分页逻辑;
2.获取被操作用户的信息并put到 AjaxResult 返回值;
3.调用service 获取所有角色信息 roles,同时获取被操作用户的角色信息 userRoles,然后进行匹配,如果 roles 中有 userRoles ,就将角色的flag 置为 true (默认是 false),返回给controller ;
4.通过 stream 过滤 roles 并put到 AjaxResult 返回值
5.返回
8.修改用户角色
1.经过过滤链等后,进入接口;
2.检查被操作的用户信息是否是超级管理员,如果是就抛出异常,如果不是就往下走;
3.检查操作的用户信息是否有被操作用户的数据权限,如果是就往下走,如果不是就抛出异常;
4.获取当前登录的用户名,并设置成更新人;
6.调用 service 进行 update 写入,状态信息由前端传入;
7.service 返回值如果大于 0 就返回 success(),否则就返回error();
9.修改用户状态
1.经过过滤链等后,进入接口;
2.检查操作的用户信息是否有被操作用户的数据权限,如果是就往下走,如果不是就抛出异常;
3.调用 service 进行先删除被操作用户当前的用户角色关系,再添加新的用户角色关系(推荐和新增时调用一样的方法);
4.service 返回值如果大于 0 就返回 success(),否则就返回error();
涉及到controller接口类:
没什么难点,略
涉及到service业务处理类:
没什么难点,略
涉及到持久层的逻辑:
具体看5.