实现注册与登录(企业级)

目录

实现注册超级管理员功能(持久层)

一、判定系统是否已经绑定超级管理员

二、编写保存用户记录的代码

三、编写查询用户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都会随之运行,这在正常不过了。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

chengbo_eva

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值