Spring - ruoyi系统鉴权流程

rouyi系统鉴权流程

后端主要逻辑

在验证用户名、密码进行登录之前,在 gateway 模块下的 ValidateCodeFilter 中,拦截需要校验验证码的请求:

    @Override
    public GatewayFilter apply(Object config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();

            // 非登录/注册请求或验证码关闭,不处理
            if (!StringUtils.containsAnyIgnoreCase(request.getURI().getPath(), VALIDATE_URL) || !captchaProperties.getEnabled()) {
                return chain.filter(exchange);
            }

            try {
                String rspStr = resolveBodyFromRequest(request);
                JSONObject obj = JSONObject.parseObject(rspStr);
                validateCodeService.checkCapcha(obj.getString(CODE), obj.getString(UUID));
            } catch (Exception e) {
                return ServletUtils.webFluxResponseWriter(exchange.getResponse(), e.getMessage());
            }
            return chain.filter(exchange);
        };
    }

将需要校验验证码的请求,发送到 gateway 模块下的 ValidateCodeService 中验证:

    /**
     * 校验验证码
     */
    @Override
    public void checkCapcha(String code, String uuid) throws CaptchaException {
        if (StringUtils.isEmpty(code)) {
            throw new CaptchaException("验证码不能为空");
        }
        if (StringUtils.isEmpty(uuid)) {
            throw new CaptchaException("验证码已失效");
        }
        String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid;
        String captcha = redisService.getCacheObject(verifyKey);
        redisService.deleteObject(verifyKey);

        if (!code.equalsIgnoreCase(captcha)) {
            throw new CaptchaException("验证码错误");
        }
    }

验证码校验通过后,进入用户登录流程,请求走到 auth 模块下的 TokenController:

    @PostMapping("login")
    public R<?> login(@RequestBody LoginBody form) {
        // 用户登录
        LoginUser userInfo = sysLoginService.login(form.getUsername(), form.getPassword());
        // 获取登录token
        return R.ok(tokenService.createToken(userInfo));
    }

login 动作分两步,第一步是校验用户名、密码并获取当前用户信息,第二步是基于获取到的用户信息生成 token 缓存。

在 auth 模块下的 SysLoginService 中,进行密码校验和获取详细信息操作:

    /**
     * 登录
     */
    public LoginUser login(String username, String password) {

        ...
// 查询用户信息 R<LoginUser> userResult = remoteUserService.getUserInfo(username, SecurityConstants.INNER); if (R.FAIL == userResult.getCode()) { throw new ServiceException(userResult.getMsg()); } if (StringUtils.isNull(userResult) || StringUtils.isNull(userResult.getData())) { recordLogininfor(username, Constants.LOGIN_FAIL, "登录用户不存在"); throw new ServiceException("登录用户:" + username + " 不存在"); } LoginUser userInfo = userResult.getData(); SysUser user = userResult.getData().getSysUser();
... recordLogininfor(username, Constants.LOGIN_SUCCESS,
"登录成功"); return userInfo; }

然后在 common 模块下的 security 中的 TokenService中,将获取到的 useInfo 和 token 信息存储到 redis:

    /**
     * 创建令牌
     */
    public Map<String, Object> createToken(LoginUser loginUser)
    {
        String token = IdUtils.fastUUID();
        Long userId = loginUser.getSysUser().getUserId();
        String userName = loginUser.getSysUser().getUserName();
        loginUser.setToken(token);
        loginUser.setUserid(userId);
        loginUser.setUsername(userName);
        loginUser.setIpaddr(IpUtils.getIpAddr(ServletUtils.getRequest()));
        // 保存到redis
        refreshToken(loginUser);

        // Jwt存储信息
        Map<String, Object> claimsMap = new HashMap<String, Object>();
        claimsMap.put(SecurityConstants.USER_KEY, token);
        claimsMap.put(SecurityConstants.DETAILS_USER_ID, userId);
        claimsMap.put(SecurityConstants.DETAILS_USERNAME, userName);

        // 接口返回信息
        Map<String, Object> rspMap = new HashMap<String, Object>();
        rspMap.put("access_token", JwtUtils.createToken(claimsMap));
        rspMap.put("expires_in", expireTime);
        return rspMap;
    }

之后在请求资源的时候,gateway 模块中的 AuthFilter 会根据请求头中的 token,从 redis 中查询出 userId 和 userName,重新添加到请求头中,然后执行接下来的调用链:

@Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpRequest.Builder mutate = request.mutate();

        String url = request.getURI().getPath();
        // 跳过不需要验证的路径
        if (StringUtils.matches(url, ignoreWhite.getWhites())) {
            return chain.filter(exchange);
        }
        String token = getToken(request);
        if (StringUtils.isEmpty(token)) {
            return unauthorizedResponse(exchange, "令牌不能为空");
        }
        Claims claims = JwtUtils.parseToken(token);
        if (claims == null) {
            return unauthorizedResponse(exchange, "令牌已过期或验证不正确!");
        }
        String userkey = JwtUtils.getUserKey(claims);
        boolean islogin = redisService.hasKey(getTokenKey(userkey));
        if (!islogin) {
            return unauthorizedResponse(exchange, "登录状态已过期");
        }
        String userid = JwtUtils.getUserId(claims);
        String username = JwtUtils.getUserName(claims);
        if (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username)) {
            return unauthorizedResponse(exchange, "令牌验证失败");
        }

        // 设置用户信息到请求
        addHeader(mutate, SecurityConstants.USER_KEY, userkey);
        addHeader(mutate, SecurityConstants.DETAILS_USER_ID, userid);
        addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username);
        // 内部请求来源参数清除
        removeHeader(mutate, SecurityConstants.FROM_SOURCE);
        return chain.filter(exchange.mutate().request(mutate.build()).build());
    }

当请求到达对应的Controller后,通过 @RequiresPermissions 判断用户是否有权限访问该资源,注解中的字符串就是自定义的权限字符串:

    /**
     * 获取用户列表
     */
    @RequiresPermissions("system:user:list")
    @GetMapping("/list")
    public TableDataInfo list(SysUser user) {
        startPage();
        List<SysUser> list = userService.selectUserList(user);
        return getDataTable(list);
    }

自定义注解 RequiresPermissions 实现在 common 模块下的 security 模块中的  PreAuthorizeAspect 切面定义中。原理就是用当前 permissions 字符串和用户拥有的 permissions 相比较,有重合即为满足权限:

    /**
     * 对一个Method对象进行注解检查
     */
    public void checkMethodAnnotation(Method method) {
        // 校验 @RequiresLogin 注解
        RequiresLogin requiresLogin = method.getAnnotation(RequiresLogin.class);
        if (requiresLogin != null) {
            AuthUtil.checkLogin();
        }

        // 校验 @RequiresRoles 注解
        RequiresRoles requiresRoles = method.getAnnotation(RequiresRoles.class);
        if (requiresRoles != null) {
            AuthUtil.checkRole(requiresRoles);
        }

        // 校验 @RequiresPermissions 注解
        RequiresPermissions requiresPermissions = method.getAnnotation(RequiresPermissions.class);
        if (requiresPermissions != null) {
            AuthUtil.checkPermi(requiresPermissions);
        }
    }

三种鉴权方式

RequireLogin:需要登录才能调用
RequireRoles:需要拥有某些角色才能调用
RequirePermissions:需要拥有某些权限才能调用

关于前端

大家可能注意到前端在设置 Authorization 的 token 值的时候,需要在 token 前面加一个"Bearer "。Bearer 的中文翻译是“持票人”,在这里指的是当前 token 标准,在 RFC 6750 中,它的定义是这样的:

OAuth enables clients to access protected resources by obtaining an access token, 
which is defined in "The OAuth 2.0 Authorization Framework " [RFC6749] " as a string representing an access authorization issued to the client",
rather than using the resource owner's credentials directly.

加上这个单词主要是为了标识当前 token 的标准。

OAuth 2.0 官网给出的定义在这:

https://oauth.net/2/bearer-tokens/

 

 

 

 

posted @ 2022-05-30 23:05  Helios_Fz  阅读(1795)  评论(0编辑  收藏  举报