@学习若依

记忆点

@auth的认证已经完成 这个应该梳理的比较合适 大概调用就是 auth - system 通过feign 来调用

  1. login 登录过程

调用流程

	//auth   TokenController
    @PostMapping("login")
    public R<?> login(@RequestBody LoginBody form)
    {
        // 用户登录 通过openfeign 拿取详细的信息
        LoginUser userInfo = sysLoginService.login(form.getUsername(), form.getPassword());
        // 这个地方其实是首次登录写入token值的   ???获取登录token???
        return R.ok(tokenService.createToken(userInfo));
    }


//校验

 public LoginUser login(String username, String password)
    {
     //...
     	//重点在这里
     	//在下面 这个作业是做出判断当前账户是否存在在黑名单中
 	    passwordService.validate(user, password);
     	//
        recordLogService.recordLogininfor(username, Constants.LOGIN_SUCCESS, "登录成功");
        recordLoginInfo(user.getUserId());
        return userInfo;
    }


validate代码


    public void validate(SysUser user, String password)
    {
        String username = user.getUserName();

        //获取redis 存取的数据
        Integer retryCount = redisService.getCacheObject(getCacheKey(username));
		//判断redis 中存在值 是否是第一次登录
        if (retryCount == null)
        {
            retryCount = 0;
        }

        //todo 这里证明已经超过%{maxRetryCount}次 放入黑名单了
		//maxRetryCount 是个常量 可以配置
        if (retryCount >= Integer.valueOf(maxRetryCount).intValue())
        {
            String errMsg = String.format("密码输入错误%s次,帐户锁定%s分钟", maxRetryCount, lockTime);
            //这个操作是写到系统日志中 每次的登录都要写这个  但是一般来说我们是使用aop 来对他进行操作
            recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL,errMsg);
            throw new ServiceException(errMsg);
        }

        if (!matches(user, password))
        {
            //写入token次数的地方 一开始我以为他是通过aop innner 结果并不是 inner 注解只是对内部请求进行分辨 
            //这里也可以通过aop 来对他的token 或者次数进行控制
            //次数加一 当大于5次
            retryCount = retryCount + 1;
            //写库
            recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, String.format("密码输入错误%s次", retryCount));
            //todo 写入黑名单 将密码错误次数写入redis
            redisService.setCacheObject(getCacheKey(username), retryCount, lockTime, TimeUnit.MINUTES);
            throw new ServiceException("用户不存在/密码错误");
        }
        else
        {
            //clearLoginRecordCache 已经登录的数据进行清理缓存操作
            clearLoginRecordCache(username);
        }
    }
//BCryptPasswordEncoder   密码加密工具
    
    

/**
 * 权限获取工具类
 * 一个工具类
 * @author ruoyi
 */
public class SecurityUtils
{
    /**
     * 获取用户ID
     */
    public static Long getUserId()
    {
        return SecurityContextHolder.getUserId();
    }

    /**
     * 获取用户名称
     */
    public static String getUsername()
    {
        return SecurityContextHolder.getUserName();
    }

    /**
     * 获取用户key
     */
    public static String getUserKey()
    {
        return SecurityContextHolder.getUserKey();
    }

    /**
     * 获取登录用户信息
     */
    public static LoginUser getLoginUser()
    {
        return SecurityContextHolder.get(SecurityConstants.LOGIN_USER, LoginUser.class);
    }

    /**
     * 获取请求token
     */
    public static String getToken()
    {
        return getToken(ServletUtils.getRequest());
    }

    /**
     * 根据request获取请求token
     */
    public static String getToken(HttpServletRequest request)
    {
        // 从header获取token标识
        String token = request.getHeader(TokenConstants.AUTHENTICATION);
        return replaceTokenPrefix(token);
    }

    /**
     * 裁剪token前缀
     */
    public static String replaceTokenPrefix(String token)
    {
        // 如果前端设置了令牌前缀,则裁剪掉前缀
        if (StringUtils.isNotEmpty(token) && token.startsWith(TokenConstants.PREFIX))
        {
            token = token.replaceFirst(TokenConstants.PREFIX, "");
        }
        return token;
    }

    /**
     * 是否为管理员
     *
     * @param userId 用户ID
     * @return 结果
     */
    public static boolean isAdmin(Long userId)
    {
        return userId != null && 1L == userId;
    }

    /**
     * 生成BCryptPasswordEncoder密码
     *
     * @param password 密码
     * @return 加密字符串
     */
    public static String encryptPassword(String password)
    {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        return passwordEncoder.encode(password);
    }

    /**
     * 判断密码是否相同
     *
     * @param rawPassword 真实密码
     * @param encodedPassword 加密后字符
     * @return 结果
     */
    public static boolean matchesPassword(String rawPassword, String encodedPassword)
    {
        // 密码加密和解密
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        return passwordEncoder.matches(rawPassword, encodedPassword);
    }
}

/**
 * 获取当前线程变量中的 用户id、用户名称、Token等信息 
 * 注意: 必须在网关通过请求头的方法传入,同时在HeaderInterceptor拦截器设置值。 否则这里无法获取
 * 这个工具类代表的是  从当前线程中拿取 某个变量的值  使用前提在下面
 * @author ruoyi
 */
public class SecurityContextHolder
{
    private static final TransmittableThreadLocal<Map<String, Object>> THREAD_LOCAL = new TransmittableThreadLocal<>();

    public static void set(String key, Object value)
    {
        Map<String, Object> map = getLocalMap();
        map.put(key, value == null ? StringUtils.EMPTY : value);
    }

    public static String get(String key)
    {
        Map<String, Object> map = getLocalMap();
        return Convert.toStr(map.getOrDefault(key, StringUtils.EMPTY));
    }

    public static <T> T get(String key, Class<T> clazz)
    {
        Map<String, Object> map = getLocalMap();
        return StringUtils.cast(map.getOrDefault(key, null));
    }

    public static Map<String, Object> getLocalMap()
    {
        Map<String, Object> map = THREAD_LOCAL.get();
        if (map == null)
        {
            map = new ConcurrentHashMap<String, Object>();
            THREAD_LOCAL.set(map);
        }
        return map;
    }

    public static void setLocalMap(Map<String, Object> threadLocalMap)
    {
        THREAD_LOCAL.set(threadLocalMap);
    }

    public static Long getUserId()
    {
        return Convert.toLong(get(SecurityConstants.DETAILS_USER_ID), 0L);
    }

    public static void setUserId(String account)
    {
        set(SecurityConstants.DETAILS_USER_ID, account);
    }

    public static String getUserName()
    {
        return get(SecurityConstants.DETAILS_USERNAME);
    }

    public static void setUserName(String username)
    {
        set(SecurityConstants.DETAILS_USERNAME, username);
    }

    public static String getUserKey()
    {
        return get(SecurityConstants.USER_KEY);
    }

    public static void setUserKey(String userKey)
    {
        set(SecurityConstants.USER_KEY, userKey);
    }

    public static String getPermission()
    {
        return get(SecurityConstants.ROLE_PERMISSION);
    }

    public static void setPermission(String permissions)
    {
        set(SecurityConstants.ROLE_PERMISSION, permissions);
    }

    public static void remove()
    {
        THREAD_LOCAL.remove();
    }
}

/**
 * 权限获取工具类
 * 这个就是前提
 * @author ruoyi
 */
public class SecurityUtils
{
    /**
     * 获取用户ID
     */
    public static Long getUserId()
    {
        return SecurityContextHolder.getUserId();
    }

    /**
     * 获取用户名称
     */
    public static String getUsername()
    {
        return SecurityContextHolder.getUserName();
    }

    /**
     * 获取用户key
     */
    public static String getUserKey()
    {
        return SecurityContextHolder.getUserKey();
    }

    /**
     * 获取登录用户信息
     */
    public static LoginUser getLoginUser()
    {
        return SecurityContextHolder.get(SecurityConstants.LOGIN_USER, LoginUser.class);
    }

    /**
     * 获取请求token
     */
    public static String getToken()
    {
        return getToken(ServletUtils.getRequest());
    }

    /**
     * 根据request获取请求token
     */
    public static String getToken(HttpServletRequest request)
    {
        // 从header获取token标识
        String token = request.getHeader(TokenConstants.AUTHENTICATION);
        return replaceTokenPrefix(token);
    }

    /**
     * 裁剪token前缀
     */
    public static String replaceTokenPrefix(String token)
    {
        // 如果前端设置了令牌前缀,则裁剪掉前缀
        if (StringUtils.isNotEmpty(token) && token.startsWith(TokenConstants.PREFIX))
        {
            token = token.replaceFirst(TokenConstants.PREFIX, "");
        }
        return token;
    }

    /**
     * 是否为管理员
     *
     * @param userId 用户ID
     * @return 结果
     */
    public static boolean isAdmin(Long userId)
    {
        return userId != null && 1L == userId;
    }

    /**
     * 生成BCryptPasswordEncoder密码
     *
     * @param password 密码
     * @return 加密字符串
     */
    public static String encryptPassword(String password)
    {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        return passwordEncoder.encode(password);
    }

    /**
     * 判断密码是否相同
     *
     * @param rawPassword 真实密码
     * @param encodedPassword 加密后字符
     * @return 结果
     */
    public static boolean matchesPassword(String rawPassword, String encodedPassword)
    {
        // 密码加密和解密
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        return passwordEncoder.matches(rawPassword, encodedPassword);
    }
}

使用authutil 可以直接拿取 但是是解析token 值

  1. code 接口 (验证码)
    (js 本身就可以支持多接口同时调用)

他的登录接口的同时会同时调用/code 这个请求 让他可以进行刷新验证码

如果不想要这个地方直接去掉就行 下面分析 ValidateCodeHandler 的代码

请添加图片描述

ai 解释的:: RouterFunctions.route是Spring WebFlux中的一个方法,用于创建一个路由函数。它接收两个参数:一个是请求谓词(RequestPredicate),另一个是处理器(Handler)。请求谓词定义了哪些请求应该被路由到该处理器,而处理器则定义了如何处理这些请求。在这个例子中,请求谓词是RequestPredicates.GET(“/code”).and(RequestPredicates.accept(MediaType.TEXT_PLAIN)),表示只处理GET请求且URL为"/code",并且接受纯文本格式的请求。处理器是validateCodeHandler,用于处理这些符合条件的请求。

//封装的配置类 
@Configuration
public class RouterFunctionConfiguration
{
    @Autowired
    //处理器
    private ValidateCodeHandler validateCodeHandler;

    @SuppressWarnings("rawtypes")
    @Bean
    public RouterFunction routerFunction()
    {
        return RouterFunctions.route(
                RequestPredicates.GET("/code").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)),
                validateCodeHandler);
    }
}

解释: HandlerFunction是Spring WebFlux中的一个接口,用于处理请求并返回响应。它是一个函数式接口,接收一个ServerRequest对象作为参数,并返回一个Mono或者Mono类型的结果。HandlerFunction通常用于定义路由处理器,即当请求匹配某个谓词时,应该调用哪个处理器来处理该请求。在RuoYi框架中,ValidateCodeHandler就是一个实现了HandlerFunction接口的类,用于处理验证码相关的请求。****

使用了WebFlux技术 因为这个行为会占用大量的io
//验证码获取  骑士这个请求主要是前端做的处理  这里只涉及到验证码的创建
@Component

//重点是 实现了 HandlerFunction接口 
public class ValidateCodeHandler implements HandlerFunction<ServerResponse>
{
    @Autowired
    private ValidateCodeService validateCodeService;

    @Override
    public Mono<ServerResponse> handle(ServerRequest serverRequest)
    {
        AjaxResult ajax;
        try
        {
            //创建验证码 这个逻辑没东西 随便写下就行
            ajax = validateCodeService.createCaptcha();
        }
        catch (CaptchaException | IOException e)
        {
            return Mono.error(e);
        }
        return ServerResponse.status(HttpStatus.OK).body(BodyInserters.fromValue(ajax));
    }
}

  1. 获取用户信息user/getInfo

  2. 获取路由**/menu/getRouters**

一个极为重要的拦截器 这个拦截器实现类 用户的封装 和用户权限的刷新

实现类的解释: **AsyncHandlerInterceptor是Spring框架中的一个接口,用于拦截异步方法的执行。它提供了三个方法:**

不选用beforeConcurrentHandlingStarted 的原因是 选用他来实现需要维护下面的行为

  1. beforeConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler): 在处理请求之前调用,可以在此方法中进行一些预处理操作,例如记录日志、验证权限等。
  2. preHandle(HttpServletRequest request, HttpServletResponse response, Object handler): 在处理请求之前调用,返回一个布尔值。如果返回true,则继续执行后续拦截器和处理器;如果返回false,则中断请求处理流程。
  3. afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex): 在请求处理完成后调用,无论请求是否成功或发生异常,都会执行此方法。可以在此处进行资源清理、记录日志等操作。
/**
 * 自定义请求头拦截器,将Header数据封装到线程变量中方便获取
 * 注意:此拦截器会同时验证当前用户有效期自动刷新有效期
 *
 * @author ruoyi
 */
public class HeaderInterceptor implements AsyncHandlerInterceptor
{
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
    {
        if (!(handler instanceof HandlerMethod))
        {
            return true;
        }

        //写入封装的数据 来让  SecurityContextHolder--> SecurityUtils ---> getUser 来实现 上面是写入线程数据
        SecurityContextHolder.setUserId(ServletUtils.getHeader(request, SecurityConstants.DETAILS_USER_ID));
        SecurityContextHolder.setUserName(ServletUtils.getHeader(request, SecurityConstants.DETAILS_USERNAME));
        SecurityContextHolder.setUserKey(ServletUtils.getHeader(request, SecurityConstants.USER_KEY));

        //获取token 值 这个一个可以直接获取到 通过这个请求直接拿到
        String token = SecurityUtils.getToken();

        //todo 讨论点: 是否是放行那些白名单 一般情况下 前端都会直接携带token 值 没有token 值

        // 应该就是放行code 和其他情况的 一般来说 token 在请求中 时刻存在 只有登录的时候会有问题
        if (StringUtils.isNotEmpty(token))
        {
            // 获取登录用户信息 没有异议
            //有可能会直接失效这个token 值 所以做了验证
            LoginUser loginUser = AuthUtil.getLoginUser(token);
            //校验认证信息 没有影响 他接下去 在gatway  模块中的AuthFilter 做验证  那上面的讨论点就没有意义  直接在这个地方做了拦截
            if (StringUtils.isNotNull(loginUser))
            {
                AuthUtil.verifyLoginUserExpire(loginUser);
                SecurityContextHolder.set(SecurityConstants.LOGIN_USER, loginUser);
            }
        }
        return true;
    }

    //清除当前的子线程
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception  ex) throws Exception
    {
        SecurityContextHolder.remove();
    }
}

AuthFilter 做验证

@Component
public class AuthFilter implements GlobalFilter, Ordered
{
    private static final Logger log = LoggerFactory.getLogger(AuthFilter.class);

    // 排除过滤的 uri 地址,nacos自行添加
    @Autowired
    private IgnoreWhiteProperties ignoreWhite;

    @Autowired
    private RedisService redisService;


    @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());
    }

    private void addHeader(ServerHttpRequest.Builder mutate, String name, Object value)
    {
        if (value == null)
        {
            return;
        }
        String valueStr = value.toString();
        String valueEncode = ServletUtils.urlEncode(valueStr);
        mutate.header(name, valueEncode);
    }

    private void removeHeader(ServerHttpRequest.Builder mutate, String name)
    {
        mutate.headers(httpHeaders -> httpHeaders.remove(name)).build();
    }

    private Mono<Void> unauthorizedResponse(ServerWebExchange exchange, String msg)
    {
        log.error("[鉴权异常处理]请求路径:{}", exchange.getRequest().getPath());
        return ServletUtils.webFluxResponseWriter(exchange.getResponse(), msg, HttpStatus.UNAUTHORIZED);
    }

    /**
     * 获取缓存key
     */
    private String getTokenKey(String token)
    {
        return CacheConstants.LOGIN_TOKEN_KEY + token;
    }

    /**
     * 获取请求token
     */
    private String getToken(ServerHttpRequest request)
    {
        String token = request.getHeaders().getFirst(TokenConstants.AUTHENTICATION);
        // 如果前端设置了令牌前缀,则裁剪掉前缀
        if (StringUtils.isNotEmpty(token) && token.startsWith(TokenConstants.PREFIX))
        {
            token = token.replaceFirst(TokenConstants.PREFIX, StringUtils.EMPTY);
        }
        return token;
    }

    @Override
    public int getOrder()
    {
        return -200;
    }
}

end

基本上看到这个地方就差不多了 ruoyi 这个框架其实只有两个地方可以借鉴 一个是他的框架设计模式 另外一个是做的权限流转情况 下面我会对这个地方进行分析--------

题主没文化 题主读书少 所以最后借用
杂诗 陶渊明

盛年不再来,一日难再晨。

及时当勉励,岁月不待人。

互勉–江湖再见.

Logo

快速构建 Web 应用程序

更多推荐