调用流程
//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 值
他的登录接口的同时会同时调用/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接口的类,用于处理验证码相关的请求。****
//验证码获取 骑士这个请求主要是前端做的处理 这里只涉及到验证码的创建
@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));
}
}
获取用户信息user/getInfo
获取路由**/menu/getRouters**
实现类的解释: **AsyncHandlerInterceptor
是Spring框架中的一个接口,用于拦截异步方法的执行。它提供了三个方法:**
不选用beforeConcurrentHandlingStarted 的原因是 选用他来实现需要维护下面的行为
beforeConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler)
: 在处理请求之前调用,可以在此方法中进行一些预处理操作,例如记录日志、验证权限等。preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
: 在处理请求之前调用,返回一个布尔值。如果返回true
,则继续执行后续拦截器和处理器;如果返回false
,则中断请求处理流程。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;
}
}
基本上看到这个地方就差不多了 ruoyi 这个框架其实只有两个地方可以借鉴 一个是他的框架设计模式 另外一个是做的权限流转情况 下面我会对这个地方进行分析--------
题主没文化 题主读书少 所以最后借用
杂诗 陶渊明
盛年不再来,一日难再晨。
及时当勉励,岁月不待人。
互勉–江湖再见.