目录
实现注册超级管理员功能(持久层)
一、判定系统是否已经绑定超级管理员
二、编写保存用户记录的代码
三、编写查询用户ID的代码
实现注册超级管理员功能(业务层)
一、获取OpenId
二、编写注册新用户的业务代码
掌握 RBAC 权限模型
一、RBAC权限模型
二、前后端权限验证
三、如何查询用户的权限列表?
实现注册超级管理员功能(Web层)
一、创建表单类
二、创建Controller类
定义全局路径和封装Ajax(移动端)
一、封装全局路径
二、封装Ajax
完成注册超级管理员功能(移动端)
实现用户登录功能(持久层&业务层)
一、如何判定登陆
二、编写持久层代码
三、编写业务层代码
实现用户登录功能(Web层)
一、创建表单类
二、创建登陆Web方法
实现用户登录功能(移动端)
观察Emos后端项目运行细节
一、为什么XSSFilter最先执行?
二、OAuth2Filter的执行
实现注册超级管理员功能(持久层)
mybatis-generate工具多表操作,生成的pojo和dao文件中 字段可能和数据表中的,不是一一对应。重新单表生成一次即可。
一、判定系统是否已经绑定超级管理员
Emos系统中只可以绑定唯一的超级管理员账号,所以用户输入了 000000 这个激活码的时候,后端Java项目必须要判断是否可以绑定超级管理员。如果用户表中没有超级管理员记录,则可以绑定。否则就不能绑定超级管理员。
我们通过SQL语句就能查询出来用户表是否存在超级管理员账号,只需要查询 root字段 值为1的记录数量就可以了。
在 TbUserDao.xml 文件中写入下面的SQL语句:
<select id="haveRootUser" resultType="boolean">
SELECT IF(COUNT(*),TRUE,FALSE) FROM tb_user WHERE root=1;
</select>
在 TbUserDao.java 文件中创建DAO方法:
@Mapper
public interface TbUserDao {
public boolean haveRootUser();
}
二、编写保存用户记录的代码
假设业务层判定用户可以注册成为超级管理员,于是我们要把用户的数据保存在用户表,这就需要我们编写相关的SQL语句和DAO代码。
在 TbUserDao.xml 文件中写入下面的SQL语句:
<insert id="insert" parameterType="HashMap">
INSERT INTO tb_user
SET
<if test="openId!=null">
open_id = #{openId},
</if>
<if test="nickname!=null">
nickname = #{nickname},
</if>
<if test="photo!=null">
photo = #{photo},
</if>
<if test="name!=null">
name = #{name},
</if>
<if test="sex!=null">
sex = #{sex},
</if>
<if test="tel!=null">
tel = #{tel},
</if>
<if test="email!=null">
email=#{email},
</if>
<if test="hiredate!=null">
hiredate = #{hiredate},
</if>
role = #{role},
root = #{root},
<if test="deptName!=null">
dept_id = ( SELECT id FROM tb_dept WHERE dept_name = #{deptName} ),
</if>
status = #{status},
create_time = #{createTime}
</insert>
在 TbUserDao.java 文件中创建DAO方法:
@Mapper
public interface TbUserDao {
……
public int insert(HashMap param);
}
三、编写查询用户ID的代码
如果在员工表中插入新纪录,由于主键是自动生成的,所以我们并不知道新纪录的主键值是多少。于是我们要编写代码,根据OpenId查询用户ID
在 TbUserDao.xml 文件中写入下面的SQL语句:
<select id="searchIdByOpenId" parameterType="String" resultType="Integer">
SELECT id FROM tb_user WHERE open_id=#{openId} AND status = 1
</select>
在 TbUserDao.java 文件中创建DAO方法:
@Mapper
public interface UserDao {
……
public Integer searchIdByOpenId(String openId);
}
实现注册超级管理员功能(业务层)
既然要写业务层的代码,肯定要先声明一个接口,然后再去定义实现类。为什么?业务层的代码经常随着需求的变化而发生变化。如果用户提出来一个新的需求, 那么咱们不是在原有业务层实现类上改代码,而是从接口里面再派生出一个子类,在新的子类里面去定义新的需求对应的代码。比如说电商网站上订单结算业务模块,普通日子是一个结算规则,促销节又是一个结算规则。如果在已有的业务模块实现类上,去添加“促销节”的代码,“促销节”过了后,代码还要再改回去,非常麻烦。从一个复杂的Java类里剥离复杂的业务代码,难度非常大。如果我们从接口中派生出一个新的实现类,在新的实现类中定义“促销节”规则,“促销节”过去后,系统使用原有的类。
在MyBatis中,我们只需要定义接口,实现类由MyBatis框架通过动态代理来实现。
在 application.yml 添加微信小程序信息:
wx:
app-id: xxxxxx
app-secret: xxxxxx
RuntimeException: 获取OpenId时,微信方面的异常。
EmosException: 自己项目的异常。
上一小节,我们封装了注册用户的持久层代码,下面就应该编写业务层的代码可。比如保存用户记录之前,我们要获得OpenId才行。
一、获取OpenId
获取微信用户的 OpenId ,需要后端程序向微信平台发出请求,并上传若干参数,最终才能得到。URL请求路径:https://api.weixin.qq.com/sns/jscode2session 。
在 com.example.emos.wx.service 中创建 UserService.java 接口
public interface UserService {
}
在 com.example.emos.wx.service.impl 中创建 UserServiceImpl.java 类
package com.example.emos.wx.service.impl;
……
@Service
@Slf4j
@Scope("prototype")
public class UserServiceImpl implements UserService {
@Value("${wx.app-id}")
private String appId;
@Value("${wx.app-secret}")
private String appSecret;
@Autowired
private TbUserDao userDao;
private String getOpenId(String code) {
String url = "https://api.weixin.qq.com/sns/jscode2session";
HashMap map = new HashMap();
map.put("appid", appId);
map.put("secret", appSecret);
map.put("js_code", code);
map.put("grant_type", "authorization_code");
String response = HttpUtil.post(url, map);
JSONObject json = JSONUtil.parseObj(response);
String openId = json.getStr("openid");
if (openId == null || openId.length() == 0) {
throw new RuntimeException("临时登陆凭证错误");
}
return openId;
}
}
二、编写注册新用户的业务代码
在 UserService 接口中添加抽象方法的声明
public int registerUser(String registerCode,String code,String nickname,String photo);
在 UserServiceImpl 类中实现抽象方法
@Override
public int registerUser(String registerCode, String code, String nickname, String photo) {
//如果邀请码是000000,代表是超级管理员
if (registerCode.equals("000000")) {
//查询超级管理员帐户是否已经绑定
boolean bool = userDao.haveRootUser();
if (!bool) {
//把当前用户绑定到ROOT帐户
String openId = getOpenId(code);
HashMap param = new HashMap();
param.put("openId", openId);
param.put("nickname", nickname);
param.put("photo", photo);
param.put("role", "[0]");
param.put("status", 1);
param.put("createTime", new Date());
param.put("root", true);
userDao.insert(param);
int id = userDao.searchIdByOpenId(openId);
return id;
} else {
//如果root已经绑定了,就抛出异常
throw new EmosException("无法绑定超级管理员账号");
}
}
//TODO 此处还有其他判断内容
else{
return 0;
}
}
掌握 RBAC 权限模型
MySQL5.7之后引入JSON数据类型。JSON数据类型可以保存两种数据,一:JSON对象,二:数组。
SELECT JSON_ARRAY(10, 20, 30) # 创建JSON数组
SELECT JSON_CONTAINS(JSON_ARRAY(10, 20, 30), "100") # JSON数组是否包含某元素
DISTINCT 平替 Set<String>
这个小节我们不先不急着写Web层的代码,因为注册成功之后,我们要向客户端返回令牌之外,还要返回用户的权限列表,以后客户端就可以根据权限列表判定用户能看到什么页面内容,以及可以执行什么操作,所以这个小节我们先来学习一下RBAC权限模型。
一、RBAC权限模型
RBAC的基本思想是,对系统操作的各种权限不是直接授予具体的用户,而是在用户集合与权限集合之间建立一个角色集合。每一种角色对应一组相应的权限。一旦用户被分配了适当的角色后,该用户就拥有此角色的所有操作权限。这样做的好处是,不必在每次创建用户时都进行分配权限的操作,只要分配用户相应的角色即可,而且角色的权限变更比用户的权限变更要少得多,这样将简化用户的权限管理,减少系统的开销。
RBAC模型中的权限是由模块和行为合并在一起而产生的,在MySQL中,有 模块表(tb_module) 和 行为表(tb_action) ,这两张表的记录合并在一起就行程了权限记录,保存在 权限表(tb_permission) 中。
现在知道了权限记录是怎么来的,下面我们看看怎么把权限关联到角色中。传统一点的做法是创建一个交叉表,记录角色拥有什么权限。但是现在 MySQL5.7 之后引入了 JSON 数据类型,所以我在 角色表(tb_role) 中设置的permissions字段,类型是JSON格式的。
到目前为止,JSON类型已经支持索引机制,所以我们不用担心存放在JSON字段中的数据检索速度慢了。MySQL为JSON类型配备了很多函数,我们可以很方便的读写JSON字段中的数据。
接下来我们看看角色是怎么关联到用户的,其实我在 用户表(tb_user) 上面设置role字段,类型依旧是JSON的。这样我就可以把多个角色关联到某个用户身上了。
二、前后端权限验证
关于权限验证的工作,前端要做,后端也要做。后端的权限验证还好说,Shiro框架可以做这个事情。但是移动端没有权限验证框架,所以需要我们自己封装函数来验证权限。每个页面在渲染的时候,先判断用户拥有什么权限,然后根据权限控制渲染的内容。比如说普通员工没有添加新员工的权限,所以界面上就不能出现添加按钮。
移动端做权限判断的前提是必须有当前用户的 权限列表 ,这个权限列表是用户 登陆成功 或者 注册成功 ,后端Java项目返回给移动端的,移动端保存到本地 Storage 里面。
三、如何查询用户的权限列表?
SELECT DISTINCT p.permission_name
FROM tb_user u
JOIN tb_role r ON JSON_CONTAINS(u.role, CAST(r.id AS CHAR))
JOIN tb_permission p ON JSON_CONTAINS(r.permissions, CAST(p.id AS CHAR))
WHERE u.id = 用户ID AND u.status = 1;
在 TbUserDao.xml 文件中添加上面的SQL语句,用来查询用户的权限列表
<select id="searchUserPermissions" parameterType="int" resultType="String">
SELECT DISTINCT p.permission_name
FROM tb_user u
JOIN tb_role r ON JSON_CONTAINS(u.role, CAST(r.id AS CHAR))
JOIN tb_permission p ON JSON_CONTAINS(r.permissions, CAST(p.id AS CHAR))
WHERE u.id = #{userId} AND u.status = 1;
</select>
在 TbUserDao.java 接口中声明 searchUserPermissions() 方法
public Set<String> searchUserPermissions(int userId);
在 UserService.java 接口中声明 searchUserPermissions() 方法
public Set<String> searchUserPermissions(int userId);
在 UserServiceImpl.java 接口中实现 searchUserPermissions() 方法
@Override
public Set<String> searchUserPermissions(int userId) {
Set<String> permissions=userDao.searchUserPermissions(userId);
return permissions;
}
实现注册超级管理员功能(Web层)
一、创建表单类
接收移动端提交的注册请求,我们需要用表单类来封装数据,所以创建 RegisterForm.java 类。
package com.example.emos.wx.controller.form;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
@Data
@ApiModel
public class RegisterForm {
@NotBlank(message = "注册码不能为空")
@Pattern(regexp = "^[0-9]{6}$",message = "注册码必须是6位数字")
private String registerCode;
@NotBlank(message = "微信临时授权不能为空")
private String code;
@NotBlank(message = "昵称不能为空")
private String nickname;
@NotBlank(message = "头像不能为空")
private String photo;
}
二、创建Controller类
处理移动端提交的请求,我们需要Controller类,所以创建 UserController.java 类。
问:业务层采用先定义接口,后声明实现类的做法,为什么Web层不这么做?
答:业务层的需求经常变化,所以应该先声明接口,然后再写实现类。Web层这里变化并不大,可以直接定义具体类。
package com.example.emos.wx.controller;
……
@RestController
@RequestMapping("/user")
@Api("用户模块Web接口")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private RedisTemplate redisTemplate;
@Value("${emos.jwt.cache-expire}")
private int cacheExpire;
@PostMapping("/register")
@ApiOperation("注册用户")
public R register(@Valid @RequestBody RegisterForm form) {
int id = userService.registerUser(form.getRegisterCode(), form.getCode(),
form.getNickname(), form.getPhoto());
String token = jwtUtil.createToken(id);
Set<String> permsSet = userService.searchUserPermissions(id);
saveCacheToken(token, id);
return R.ok("用户注册成功").put("token", token).put("permission", permsSet);
}
private void saveCacheToken(String token, int userId) {
redisTemplate.opsForValue().set(token, userId + "", cacheExpire, TimeUnit.DAYS);
}
}
定义全局路径和封装Ajax(移动端)
一、封装全局路径
上节课我们创建好了后端的register方法,那么移动端发出请求,首先要填写好URL地址。为了在移动端项目上集中管理URL路径,我们可以在 main.js 文件中用全局变量的语法,定义全局的URL地址,这样更加便于维护。
let baseUrl = "http://192.168.99.216:8080/emos-wx-api"
Vue.prototype.url = {
register: baseUrl + "/user/register",
}
二、封装Ajax
移动端通过Ajax向服务端提交请求,然后接收到的响应分若干种情况:
1. 如果用户没有登陆系统,就跳转到登陆页面。
2. 如果用户权限不够,就显示提示信息。
3. 如果后端出现异常,就提示异常信息。
4. 如果后端验证令牌不正确,就提示信息。
5. 如果后端正常处理请求,还要判断响应中是否有Token。如果令牌刷新了,还要在本地存储Token。
如果移动端每次发出Ajax,都要做这么多的判断,我们的重复性劳动太多了。所以尽可能的把Ajax封装起来,减少重复性的劳动。
Vue.prototype.ajax = function(url, method, data, fun) {
uni.request({
"url": url,
"method": method,
"header": {
token: uni.getStorageSync('token')
},
"data": data,
success: function(resp) {
if (resp.statusCode == 401) {
uni.redirectTo({
url: '../login/login'
});
} else if (resp.statusCode == 200 && resp.data.code == 200) {
let data = resp.data
if (data.hasOwnProperty("token")) {
console.log(resp.data)
let token = data.token
uni.setStorageSync("token", token)
}
fun(resp)
} else {
uni.showToast({
icon: 'none',
title: resp.data
});
}
}
});
}
完成注册超级管理员功能(移动端)
......
实现用户登录功能(持久层&业务层)
我们完成了超级管理员注册流程之后,用户表中就已经有了超级管理员记录,那么接下来我们可以利用这个用户记录来完成Emos小程序的微信登陆功能。
一、如何判定登陆
用户表中并没有密码字段,我们无法根据username和password来判定用户是否可以登录。因为用户要拿着微信登陆Emos小程序,在用户表中只有 openid 、 nickname 和 photo 跟微信账号相关,我们应该如何判定用户登陆?
我们可以这样设计,用户在Emos登陆页面点击登陆按钮,然后小程序把 临时授权字符串 提交给后端Java系统。后端Java系统拿着临时授权字符串换取到 openid ,我们查询用户表中是否存在这个 openid 。如果存在,意味着该用户是已注册用户,可以登录。如果不存在,说明该用户尚未注册,目前还不是我们的员工,所以禁止登录。
二、编写持久层代码
在 TbUserDao.xml 文件中,编写查询语句
<select id="searchIdByOpenId" parameterType="String" resultType="Integer">
SELECT id FROM tb_user WHERE open_id=#{openId} AND status = 1
</select>
在 TbUserDao.java 中,定义DAO方法
public Integer searchIdByOpenId(String openId);
三、编写业务层代码
在 UserService.java 中定义抽象方法
public Integer login(String code);
在 UserServiceImpl.java 中实现抽象方法
@Override
public Integer login(String code) {
String openId = getOpenId(code);
Integer id = userDao.searchIdByOpenId(openId);
if (id == null) {
throw new EmosException("帐户不存在");
}
//TODO 从消息队列中接收消息,转移到消息表
return id;
}
实现用户登录功能(Web层)
有异常会抛出,不担心出现null,可以直接用 int id。
一、创建表单类
创建 LoginForm.java 类,封装客户端提交的数据。
@ApiModel
@Data
public class LoginForm {
@NotBlank(message = "临时授权不能为空")
private String code;
}
二、创建登陆Web方法
在 UserController.java 中创建 login() 方法。
@PostMapping("/login")
@ApiOperation("登陆系统")
public R login(@Valid @RequestBody LoginForm form) {
int id = userService.login(form.getCode());
String token = jwtUtil.createToken(id);
Set<String> permsSet = userService.searchUserPermissions(id);
saveCacheToken(token, id);
return R.ok("登陆成功").put("token", token).put("permission", permsSet);
}
判定用户登陆成功之后,向客户端返回权限列表和Token令牌。
实现用户登录功能(移动端)
…………
观察Emos后端项目运行细节
之前我们在SpringBoot项目中添加了很多第三方的技术,包括我们自己也写了很多有关的配置程序。其中包括 Servlet过滤器 , Shiro过滤器 ,以及 AOP拦截器 。Emos后端项目运行的时候,这些程序的执行顺序是什么?哪里是入口,我们还不太了解,所以这个小节,我们利用登陆案例来观察Emos后端项目的运行细节。
一、为什么XSSFilter最先执行?
Emos系统接收到 HTTP请求 之后,首先由 XSSFilter 来处理请求。因为 XSSFilter 是标准的Servlet过滤器 ,所以他执行的优先级要高于 ShiroFilter 和 AOP拦截器 的。这也很好理解,还没轮到Controller中的Web方法执行,AOP连接器自然不能运行。另外, XSSFilter 使用@WebFilter 注解定义出来的过滤器,所以他的优先级比 SpringMVC 中注册的 Filter 优先级更高,所以 XSSFilter 早于 SpringMVC 执行。这个也能说得通,我们希望先把请求中的数据先转义,然后再由SpringMVC框架来处理请求。
二、OAuth2Filter的执行
因为OAuth2Filter是在SpringMVC中注册的Filter,所以它晚于Servlet过滤器的执行。但是SpringMVC中注册过滤器有个好处,就是可以规定Filter的优先级别,所以定义普通的Filter,注册在SpringMVC上更加的妥当。
我们在定义OAuth2Filter的时候,声明了很多的方法,但是在注册流程中,我们只能看到doFilterInternal()方法的执行,这又是为什么呢?
@Override
public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
super.doFilterInternal(request, response, chain);
}
我们声明Shiro过滤器拦截路径的时候,为登陆和注册路径下的请求,设置了放行,所以验证与授权并没有生效。等我们将来写具体的业务类型的Web方法,添加相关的Shiro注解,这时候OAuth2Filter中的其他方法就得以运行了。
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("/user/register", "anon");
filterMap.put("/user/login", "anon");
filterMap.put("/test/**", "anon");
filterMap.put("/**", "oauth2");
三、TokenAspect的作用
TokenAspect是切面类,拦截所有Web方法的返回值。TokenAspect先检测ThreadLocalToken中有没有令牌字符串?如果有就把刷新后的令牌写入Web方法返回的R对象里面。因此说,Web方法每次执行的时候,TokenAspect都会随之运行,这在正常不过了。