最近练习搭建了一个后台管理系统,首先第一步做了关于验证登录的功能.以下项目使用了Nacos作为服务发现和注册中心,将Auth和gateway,system等相关多个微服务注册进Nacos.每次刷新登录页面,就会获取新的验证码(,输入正确的验证码即可成功跳转至首页.
获取验证码url:http://localhost/dev-api/code - dev-api是前端设置的反向代理,实际访问的是网关路径和端口.即在网关gateway模块做了路由转发.返回给前端
/*** 路由转发*/
@Configuration
public class RouterFunctionConfiguration {@Autowiredprivate ValidateCodeHandler validateCodeHandler;@Beanpublic RouterFunction routerFunction() {return RouterFunctions.route(RequestPredicates.GET("/code").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)),validateCodeHandler);}
}/**
*验证码Handler
*/
@Component
public class ValidateCodeHandler implements HandlerFunction<ServerResponse> {@Autowiredprivate ValidateCodeService validateCodeService;@SneakyThrows@Overridepublic Mono<ServerResponse> handle(ServerRequest serverRequest) {AjaxResult result = validateCodeService.createCapcha();//TODO catch returnreturn ServerResponse.status(HttpStatus.OK).body(BodyInserters.fromValue(result));}
}
public interface ValidateCodeService {/*** 生成验证码*/AjaxResult createCapcha() throws IOException;//, CaptchaException/*** 校验验证码*/void checkCapcha(String key, String value) ;//throws CaptchaException
}/*** 验证码服务*/
@Service
@Slf4j
public class ValidateCodeServiceImpl implements ValidateCodeService {@Autowiredprivate RedisUtil redisUtil;// @Autowired
// Producer defaultKaptcha;@Resource(name = "captchaProducerMath")private Producer defaultKaptcha;@Overridepublic AjaxResult createCapcha() {AjaxResult ajax = AjaxResult.success();// 生成验证码String capText = defaultKaptcha.createText();System.out.println("capText....."+ capText);BufferedImage image = defaultKaptcha.createImage(capText);String uuid = IdUtils.simpleUUID();String redis_key = Constants.CAPTCHA_CODE_KEY + uuid;System.out.println("createCapcha redis_key capText"+ redis_key +"-"+ capText);redisUtil.setCacheObject(redis_key, capText, 3, TimeUnit.MINUTES);FastByteArrayOutputStream os = new FastByteArrayOutputStream();try {ImageIO.write(image, "jpg", os);} catch (IOException e) {log.error("ImageIO write error", e);return AjaxResult.error(e.getMessage());}ajax.put("uuid", uuid);ajax.put("img", Base64.encode(os.toByteArray()));return ajax;}@Overridepublic void checkCapcha(String key, String code) {if(StringUtils.isEmpty(code)){//验证码不能为空throw new ObjectNotNullException("验证码不能为空");}//从redis获取 redis_keyString redis_key = Constants.CAPTCHA_CODE_KEY + key;//获取redis_key 对应的 redis_valueString redis_value = redisUtil.getCacheObject(redis_key);if(StringUtils.isEmpty(redis_value)){//验证码失效throw new RuntimeException("验证码失效");}//用了验证码之后从redis删除redisUtil.deleteCacheObject(redis_key);if(!redis_value.equals(code)){//可以修改为忽略大小写//验证码错误throw new RuntimeException("验证码错误");}}
}/**
*验证码样式配置
*/
@Configuration
public class KaptchaConfig {//验证码样式@Bean(name = "captchaProducerMath")public DefaultKaptcha getKaptchaBeanMath(){DefaultKaptcha defaultKaptcha = new DefaultKaptcha();Properties properties = new Properties();// 是否有边框 默认为true 我们可以自己设置yes,noproperties.setProperty(KAPTCHA_BORDER, "yes");// 边框颜色 默认为Color.BLACKproperties.setProperty(KAPTCHA_BORDER_COLOR, "105,179,90");// 验证码文本字符颜色 默认为Color.BLACKproperties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue");// 验证码图片宽度 默认为200properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");// 验证码图片高度 默认为50properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");// 验证码文本字符大小 默认为40properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "35");// KAPTCHA_SESSION_KEYproperties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCodeMath");// 验证码文本生成器
// properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "com.ruoyi.gateway.config.KaptchaTextCreator");// 验证码文本字符间距 默认为2properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, "3");// 验证码文本字符长度 默认为5properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "6");// 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");// 验证码噪点颜色 默认为Color.BLACKproperties.setProperty(KAPTCHA_NOISE_COLOR, "white");// 干扰实现类properties.setProperty(KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");// 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpyproperties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");Config config = new Config(properties);defaultKaptcha.setConfig(config);return defaultKaptcha;}
}
登录验证用户名密码,验证码是否正确
auth模块提供对外验证的接口负责调用system模块验证成功生成并设置token等信息,system模块提供用户信息查询,登录之前会经gateway转发到后台微服务,将验证码校验的功能放到了网关处.
在gateway模块中加入了自定义的验证码过滤器,特定的访问路径,会先执行验证码filter.例如:login
/*** 验证码过滤器*/
public class ValidateFilter extends AbstractGatewayFilterFactory {@Autowiredprivate ValidateCodeService validateCodeService;private static final String CODE = "code";private static final String UUID = "uuid";@Overridepublic GatewayFilter apply(Object config) {return (exchange, chain)->{System.out.println("打印线程id........"+Thread.currentThread());//2System.out.println("ValidateFilter apply ...");//3ServerHttpRequest request = exchange.getRequest();String path = request.getURI().getPath();System.out.println("path..." + path);//4//1.路径不是login和register,不检验验证码,直接放行if(!path.equals("/auth/login")){return chain.filter(exchange);}//2.校验验证码try {Flux<DataBuffer> body = request.getBody();AtomicReference<String> bodyRef = new AtomicReference<>();Consumer<? super DataBuffer> c = (b ->{System.out.println(b.toString()+"lll...");//c...c 之间执行CharBuffer charBuffer = StandardCharsets.UTF_8.decode(b.asByteBuffer());DataBufferUtils.release(b);bodyRef.set(charBuffer.toString());});System.out.println("c..........."+c.toString());//5body.subscribe(c);System.out.println("c..........."+c.toString()+"end");//6String rspStr = bodyRef.get();System.out.println("rspStr");//7JSONObject obj = JSONObject.parseObject(rspStr);System.out.println("obj");//8
// if(obj != null){System.out.println("checkCapcha...");//9String code = obj.getString(CODE);String uuid = obj.getString(UUID);validateCodeService.checkCapcha(uuid, code);
// }System.out.println("ooooooooooooo");//10}catch (Exception e){
// throw new RuntimeException("验证码校验失败!");System.out.println("e.getMessage()"+e.getMessage());//9return ServletUtils.webFluxResponseWriter(exchange.getResponse(), e.getMessage());}System.out.println("apply end........");return chain.filter(exchange);};}
}
如果验证码校验成功,则会转发到后台真正执行登录的接口,完成登录验证,即调用auth微服务
在auth服务中,提供了login接口:
@RestController
public class TokenController {@Autowiredprivate ITokenService tokenService;@Autowiredprivate ILoginService loginService;@PostMapping("/login")public Result<?> login(@RequestBody LoginDTO loginDTO) {System.out.println("查询登录信息开始...");//查询当前用户信息查询接口SysUser sysUser = loginService.login(loginDTO.getUsername(), loginDTO.getPassword());if(sysUser == null){throw new ObjectNotExistException("用户信息不存在");}//创建令牌信息Map<String,Object> tokenInfo = tokenService.createToken(new LoginUser(sysUser.getUserId(), sysUser.getUserName()));System.out.println("查询登录信息结束...");return Result.ok(tokenInfo);}
}@Service("tokenService")
public class TokenService implements ITokenService {/*** 创建令牌*/public Map<String, Object> createToken(LoginUser loginUser) {String token = IdUtils.fastUUID();String userName = loginUser.getUsername();Long userId = loginUser.getUserId();loginUser.setToken(token);//TODO 刷新token生效时间
// refreshToken(loginUser);// Jwt存储信息Map<String, Object> claimsMap = new HashMap<>();claimsMap.put(SecurityConstants.USER_KEY, token);claimsMap.put(SecurityConstants.DETAILS_USERNAME, userName);claimsMap.put(SecurityConstants.DETAILS_USER_ID, userId);//TODO userId deptId等额外信息存储// 接口返回信息Map<String, Object> rspMap = new HashMap<>();rspMap.put("access_token", JwtUtils.createToken(claimsMap));
// rspMap.put("expires_in", expireTime);return rspMap;}
// /**
// * 刷新令牌有效期
// *
// * @param loginUser 登录信息
// */
// public void refreshToken(LoginUser loginUser)
// {
// loginUser.setLoginTime(System.currentTimeMillis());
// loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
// // 根据uuid将loginUser缓存
// String userKey = getTokenKey(loginUser.getToken());
// redisService.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
// }}
登陆成功,会创建token信息返回前端,前端会拿着token访问其他业务接口,会通过网关的校验,验证token信息是否有效.
/*** 网关鉴权 验证token* 1. 用户进入网关开始登陆,网关过滤器进行判断,如果是登录,则路由到后台管理微服务进行登录* 2. 用户登录成功,后台管理微服务签发JWT TOKEN信息返回给用户* 3. 用户再次进入网关开始访问,网关过滤器接收用户携带的TOKEN* 4. 网关过滤器解析TOKEN ,判断是否有权限,如果有,则放行,如果没有则返回未认证错误*/
public class AuthFilter implements GlobalFilter, Ordered {@Autowiredprivate IgnoreWhiteProperties ignoreWhiteProperties;@SneakyThrows@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {String url = exchange.getRequest().getURI().getPath();//url验证 TODO 放行 loginif(matchUrls(url)){//匹配return chain.filter(exchange);}else{String token = exchange.getRequest().getHeaders().getFirst("Authorization");if(StringUtils.isEmpty(token)){exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);System.out.println("token缺失!");return unauthorizedResponse(exchange, "token缺失!");}else{//验证tokenClaims claims = JwtUtils.parseToken(token);if(ObjectUtils.isEmpty(claims)){System.out.println("验证信息无效!");return unauthorizedResponse(exchange, "验证信息无效!");}//TODO 从redis取值,如果不存在则token验证过期 (d)
//TODO boolean login = true;
//TODO if(!login){
//TODO throw new Exception("token过期!");//TODO TokenException
//TODO }String userName = JwtUtils.getUserName(claims);String userId = JwtUtils.getUserId(claims);if(!StringUtils.isEmpty(userName) && !StringUtils.isEmpty(userId)){//身份认证成功,设置请求值Consumer<HttpHeaders> httpHeaders = httpHeader -> {httpHeader.add(SecurityConstants.USER_KEY, token);httpHeader.add(SecurityConstants.DETAILS_USERNAME, userName);httpHeader.add(SecurityConstants.DETAILS_USER_ID, userId);};exchange.getRequest().mutate().headers(httpHeaders);//TODO 内部請求來源參數清除}else{//身份认证失败return unauthorizedResponse(exchange, "验证信息无效!");}}}return chain.filter(exchange);}private boolean matchUrls(String url){System.out.println("url :" +url);List<String> whites = ignoreWhiteProperties.getWhites();if(CollectionUtils.isEmpty(whites)){return false;}for(String u : whites){System.out.println("u : " + u);if(isMatch(url, u)){return true;}}return false;}//比较private boolean isMatch(String url, String match) {
// AntPathMatcher matcher = new AntPathMatcher();
// matcher.match(url, match);return url.contains(match);}@Overridepublic int getOrder() {return Ordered.HIGHEST_PRECEDENCE;}private Mono<Void> unauthorizedResponse(ServerWebExchange exchange, String msg){System.out.println("[鉴权异常处理]请求路径:"+exchange.getRequest().getPath());return ServletUtils.webFluxResponseWriter(exchange.getResponse(), msg, HttpStatus.UNAUTHORIZED.value());}
}
可以再白名单中添加不需要校验token的url
在验证码过滤器中Flux<DataBuffer> body = request.getBody();可能取值为null,原因百度了说是流不能重复读取的问题,添加如下配置即可解决
@Component
public class CacheRequestFilter extends AbstractGatewayFilterFactory<CacheRequestFilter.Config>
{public CacheRequestFilter(){super(Config.class);}@Overridepublic String name(){return "CacheRequestFilter";}@Overridepublic GatewayFilter apply(Config config){System.out.println("CacheRequestFilter apply...");CacheRequestGatewayFilter cacheRequestGatewayFilter = new CacheRequestGatewayFilter();Integer order = config.getOrder();if (order == null){return cacheRequestGatewayFilter;}return new OrderedGatewayFilter(cacheRequestGatewayFilter, order);}public static class CacheRequestGatewayFilter implements GatewayFilter{@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain){System.out.println("CacheRequestFilter filter...");//1// GET DELETE 不过滤HttpMethod method = exchange.getRequest().getMethod();if (method == null || method.matches("GET") || method.matches("DELETE")){return chain.filter(exchange);}return DataBufferUtils.join(exchange.getRequest().getBody()).map(dataBuffer -> {byte[] bytes = new byte[dataBuffer.readableByteCount()];dataBuffer.read(bytes);DataBufferUtils.release(dataBuffer);return bytes;}).defaultIfEmpty(new byte[0]).flatMap(bytes -> {DataBufferFactory dataBufferFactory = exchange.getResponse().bufferFactory();ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(exchange.getRequest()){@Overridepublic Flux<DataBuffer> getBody(){if (bytes.length > 0){return Flux.just(dataBufferFactory.wrap(bytes));}return Flux.empty();}};return chain.filter(exchange.mutate().request(decorator).build());});}}@Overridepublic List<String> shortcutFieldOrder(){return Collections.singletonList("order");}static class Config{private Integer order;public Integer getOrder(){return order;}public void setOrder(Integer order){this.order = order;}}
}
为了获取当前用户的信息,可以通过session获取,我通过吧用户的token解析放到了private static final TransmittableThreadLocal<Map<String, Object>> THREAD_LOCAL中,添加了一个头信息拦截器
/*** 请求头拦截器 : 内部请求身份验证拦截器* AsyncHandlerInterceptor : https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/AsyncHandlerInterceptor.html*/
public class HeaderInterceptor implements AsyncHandlerInterceptor {//1.Thread_local验证//2.session验证@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {System.out.println("HeaderInterceptor preHandle ....开始执行");//↓↓↓↓↓↓↓↓//1.Thread_local验证方式// 如果不是映射到方法直接通过if(!(handler instanceof HandlerMethod)){return true;}//从网关中获取的String userId = request.getHeader(SecurityConstants.DETAILS_USER_ID);String userName = request.getHeader(SecurityConstants.DETAILS_USERNAME);String userkey = request.getHeader(SecurityConstants.USER_KEY);SecurityContextHolder.setThreadLocal(SecurityConstants.DETAILS_USER_ID, userId == null ? "": userId);SecurityContextHolder.setThreadLocal(SecurityConstants.DETAILS_USERNAME, userName == null ? "" : userName);SecurityContextHolder.setThreadLocal(SecurityConstants.USER_KEY, userkey == null ? "" : userkey);//TODO 获取token,获取userid,username,userkeyString token = SecurityUtils.getToken();if (StringUtils.isNotEmpty(token)){TODO LoginUser详情 获取当前登录信息LoginUser loginUser = AuthUtil.getLoginUser(token);if (StringUtils.isNotNull(loginUser)) {//TODO 验证有效期AuthUtil.verifyLoginUserExpire(loginUser);SecurityContextHolder.setThreadLocal(SecurityConstants.LOGIN_USER, loginUser);
// return true;}//TODO 为什么不校验该token解析出来的user是否合法
// else{
// //TODO login无效报错
// System.out.println("login信息,不存在");
// throw new RuntimeException("用户未登录,请登录后操作!");
// }}//TODO 为什么不校验token,直接返回true???
// else{
// //
// //TODO token不能为空
// System.out.println("token为空!");
// throw new RuntimeException("用户未登录,请登录后操作!");
// }//↑↑↑↑↑↑System.out.println("HeaderInterceptor preHandle ....结束执行");return true;}@Overridepublic void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// AsyncHandlerInterceptor.super.afterConcurrentHandlingStarted(request, response, handler);System.out.println("afterConcurrentHandlingStarted");}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {System.out.println("postHandle");}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {System.out.println("afterCompletion");//TODO 放开注释SecurityContextHolder.remove();}
}
就可以通过SecurityContextHolder工具类获取当前登录用户的信息.还可以刷新token的有效期(代码暂无).