快速掌握websocket、权限框架、远程调用、接口文档使用
本篇教程直指实战,10分钟掌握websocket、spring security结合jwt形式token、swagger结合kinfe4j、openfeign与springboot的集成使用
websocket的使用
引入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> 版本号在项目父pom.xml文件中可以看到,后续依赖同理
注册websocket端点
@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Resource WebSocketHandler webSocketHandler; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { 将处理器注册进容器,并指定连接路径为服务器ip:Springboot端口/路径(localhost:1688/test/test)的websocket连接交由处理器处理,并设置允许跨域 registry.addHandler(webSocketHandler, “/test/test”).setAllowedOrigins("*"); } }
创建处理器并注册进容器
@Slf4j @Component public class WebSocketHandler extends TextWebSocketHandler { 重写handleTextMessage方法,有一条websocket消息发送到Springboot服务时可以获取到该消息的websocketsession对象和封装了发送消息的textmessage对象 @Override public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { 用gson将该对象中蕴含的websocket消息相关信息如消息具体内容转换成json格式对象的属性 JsonObject json = JsonParser.parseString(message.getPayload()).getAsJsonObject(); } 重写建立连接后会调用的方法 @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { log.info("连接建立"); } 重写关闭连接后会调用的方法 @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { log.info("连接关闭"); } }
spring security的使用
引入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
创建核心功能配置类,由于gateway是webflux风格,集成security写法和普通mvc集成略有不同,下面是普通mvc集成写法
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Resource JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; 重写核心配置方法自定义权限规则 @Override protected void configure(HttpSecurity http) throws Exception { 链式写法 http .authorizeRequests() .antMatchers("/getAllUser","/doc.html","/index.html","/login","/vue.js","/axios.min.js","/indexs.html","/video/getvideo").permitAll() 像doc.html、index.html这些允许匿名访问 .antMatchers("/path1").hasAuthority("menu:insert")而path1、path2这些路径需要有 .antMatchers("/path2").hasAuthority("menu:delete")menu:insert或menu:delete的权限 .anyRequest().authenticated() 任何请求都需要接受规则验证 .and() .formLogin() 表单登录禁止,前后端分离时需要禁止,否则每次打开页面都会进入security .disable() 自带表单 .formLogin() .loginPage("/index.html") 前后端不分离时定义登录页面所在位置 .loginProcessingUrl("/loginTest") 识别登录的请求 .successHandler(new JwtAuthenticationSuccessHandler()) 登录成功处理器 .defaultSuccessUrl("/page", true) 成功后转向的页面 .failureHandler(new CustomAuthenticationFailureHandler()) 失败的处理器 该方案由security封装实现,自动识别登录,无需编写登录接口,但直接编写登录接口也能起到同样作用且可以少掉重写security授权服务步骤,因此后续都以自己实现登录接口做示例 .and() .httpBasic().disable() 禁用http基本认证 .csrf().disable() 禁用跨域伪造请求保护,因为需要使用jwt .addFilterAt(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); 用自定义鉴权过滤器替换掉security默认鉴权过滤器 } }
自定义登录接口调用的service层核心方法
private String login(LoginRequest loginRequest){ 查询数据库中是否存在和该用户名密码匹配的用户 LambdaQueryWrapper<User> userWrapper=new LambdaQueryWrapper<>(); userWrapper.eq(User::getUsername,loginRequest.getUsername()); User user=userMapper.selectOne(userWrapper); 如果有 if(user.getPassword().equals(loginRequest.getPassword())){ 查询该用户有的权限,以容易理解的形式,权限表中只存用户id和拥有的权限 LambdaQueryWrapper<Authority> authorityWrapper=new LambdaQueryWrapper<>(); authorityWrapper.eq(Authority::getUserId,user.getId()); List<String> authorities=authorityMapper.selectList(authorityWrapper); 将该用户拥有的系列权限和userid一起封装进token返回给controller,再在controller中将token保存到响应头中返回给前端 return JwtUtil.generateToken(user.getId(),authorities); }else{ 若登录失败则返回的token为空 return null; }
鉴权过滤器
前端发送请求时将jwt携带到请求头里 String jwt=request.getHeader("authorization"); if (jwt != null) { 解析JWT令牌并获取权限 Authentication authentication = JwtUtil.getAuthentication(jwt); 设置认证信息到SecurityContextHolder,用来后续验证调接口时是否有该接口的权限 SecurityContextHolder.getContext().setAuthentication(authentication); } chain.doFilter(request, response); }
而gateway集成security如下
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Resource JwtAuthorizationFilter jwtAuthorizationFilter; @Bean public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { http .authorizeExchange() 起始授权、后续填写匹配路径和鉴定权限方法与mvc风格不一致 .pathMatchers( "/webjars/","/register/**","/user-center/**", "/swagger-ui.html", "/webjars/**", "/swagger-resources/**", "/v2/**") .permitAll() .anyExchange().hasAuthority("role:user") .and() .httpBasic().disable() .formLogin().disable() .addFilterAt(new JwtAuthorizationFilter(),SecurityWebFiltersOrder.AUTHORIZATION) .csrf().disable(); return http.build(); } }
而gateway集成security的自定义鉴权过滤器如下
public class JwtAuthorizationFilter implements WebFilter { /** *打印请求路径,用自定义请求头隔绝csrf攻击,取出token认证用户与验证权限 */ @Override public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { String path = exchange.getRequest().getURI().getPath(); if (!path.contains("webjars") && !path.contains("swagger") && !path.contains("api")) { log.info(exchange.getRequest().getURI().getPath()); } 从请求头中获取token String jwt = exchange.getRequest().getHeaders().getFirst(Constant.SHORT_TOKEN); if (jwt != null) { 获取权限 Claims claims = JwtUtil.getClaimsFromToken(jwt); String role = claims.get(JWT_ROLE, String.class); SimpleGrantedAuthority authority = new SimpleGrantedAuthority("role"+role); Authentication authentication = new UsernamePasswordAuthenticationToken( "username", "password", Collections.singletonList(authority) ); SecurityContext context = new SecurityContextImpl(); 将权限封装成authentication对象后放入安全上下文 context.setAuthentication(authentication); return chain.filter(exchange) .contextWrite(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(context)));
jwt的使用
引入依赖
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> </dependency>
创建工具类
package example.util; import io.jsonwebtoken.*; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.Authentication; import java.util.Date; import java.util.List; import java.util.stream.Collectors; public class JwtUtil { private static final String SECRET_KEY = "key"; public static String generateToken(Integer userId, List<SimpleGrantedAuthority> authorities) { List<String> authoritiesList = authorities.stream() .map(SimpleGrantedAuthority::getAuthority) .collect(Collectors.toList()); int expirationTime=10000; 使用建造者模式一步步构建jwt return Jwts.builder() .setSubject(String.valueOf(userId)) 设置主题 .claim("authorities", authoritiesList) 设置声明,也是jwt体中重要信息的存储位置 .setIssuedAt(new Date(System.currentTimeMillis())) 设置jwt创建时间 .setExpiration(new Date(System.currentTimeMillis()+expirationTime)) 设置jwt过期时间 .signWith(SignatureAlgorithm.HS512, SECRET_KEY) 设置签名规则和签名key,需要根据key来解码jwt .compact(); } public static Authentication getAuthentication(String token) { 去掉jwt中的前缀 token = token.replace("Bearer ", ""); 根据预先存储的key解码jwt获取jwt的核心信息如所具备权限 Claims claims = Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody(); List<String> authoritiesList = claims.get("authorities", List.class); List<SimpleGrantedAuthority> authorities = authoritiesList.stream() .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); String username = claims.getSubject(); 将权限设置入上下文 return new UsernamePasswordAuthenticationToken(username, null, authorities); } public static boolean validateToken(String token) { try { 解析Token的同时会检查签名。如果签名错误,会抛出SignatureException 如果Token已过期,会抛出ExpiredJwtException Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token); 如果没有抛出异常,那么Token就是既有效又没有过期的 return true; } catch (ExpiredJwtException | SignatureException e) { Token过期或签名错误 return false; } catch (JwtException e) { 其他可能的错误,例如构造的JWT格式不正确等 return false; } } }
最后是security封装好的加密工具使用
配置类中注册该工具类 public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
加密工具使用(默认使用加盐算法)
@Service public class UserService { @Autowired private PasswordEncoder passwordEncoder; public void registerUser(String rawPassword) { 将密码加密后保存到数据库 String encodedPassword = passwordEncoder.encode(rawPassword); saveUserToDatabase(encodedPassword); } 实现将用户信息保存到数据库 private void saveUserToDatabase(String encodedPassword) { } 验证请求中的密码和数据库中保存的加密密码解密后的值是否一致 public boolean authenticate(String rawPassword, String storedEncodedPassword) { return passwordEncoder.matches(rawPassword, storedEncodedPassword); }
swagger文档结合knife4j的使用
引入依赖
<dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-openapi2-spring-boot-starter</artifactId> </dependency>
核心配置类
@Configuration @EnableSwagger2WebMvc public class Knife4jConfiguration { @Bean(value = "dockerBean") public Docket dockerBean() { Docket docket=new Docket(DocumentationType.SWAGGER_2) .apiInfo(new ApiInfoBuilder() .title("视频模块文档") 接口文档标题以及一些相关描述信息 .description(" 首页、视频详情页、个人主页视频数据的获取以及对视频的操作") .termsOfServiceUrl("https://doc.Amumu.com/") .contact("阿沐木") .version("1.0") .build()) .groupName("视频服务") .select() ↓核心配置,标注接口文档的接口映射路径 .apis(RequestHandlerSelectors.basePackage("ljl.bilibili.video.controller")) .paths(PathSelectors.any()) .build(); return docket; } }
接口文档映射路径在idea中可以找到
找到要放入接口文档的接口所在的包点击,可以在上方看到路径,src-main-java后的路径就是映射路径(tips:点击红色圆圈所在位置可以快速找到一个文件在项目中的具体位置)
在请求类中描述信息
@Data 描述该类 @ApiModel("收藏请求") public class CollectRequest { 描述该属性 @ApiModelProperty("收藏的视频的id") private Integer videoId; @ApiModelProperty("该用户的收藏夹的id") private Integer collectGroupId; @ApiModelProperty("操作类型,true是收藏,false是取消收藏") private Boolean type; }
接口中描述信息
@RestController 给接口文档中的该接口起名,否则默认是接口类的名字CollectController @Api(tags = "收藏和收藏夹的增删改查") @RequestMapping("/collect") public class CollectController { @Resource CollectService collectService; @PostMapping("/collect") 给该接口方法添加描述 @ApiOperation("收藏视频") public Result<Boolean> collect(@RequestBody List<CollectRequest> collectRequest) { return collectService.collect(collectRequest); }
实际效果(浏览器中输入服务ip+端口+/doc.html)
集成knife4j后页面好看很多,且封装了一些参数,操作更便捷。原生swagger是这样
openfeign的使用
引入依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> 在当前版本引入spring-cloud-starter-openfeign-core会报错,需注意不要引错依赖 </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId>当前版本openfeign已经弃用ribbon,因此需额外引入loadbalancer作为负载均衡器 </dependency>
client接口编写
name值是服务注册到注册中心的名称,url值可以省去,若同时写则url值优先级更高,将不走注册中心直接调该路径的方法 @FeignClient(name = "notice",url="http://localhost:30000") @Component public interface SendNoticeClient { 需和实际远程调用接口路径、返回值、参数列表一致 @PostMapping("/notice/sendDynamicNotice") Boolean dynamicNotice(@RequestBody Dynamic dynamic);
使用
@Resource SendNoticeClient client; client.dynamicNotice(new Dynamic());
绝大多数情况传递的都是json数据,但特殊情况需要在服务之间传递文件流,这时需要额外的措施
调用的方法传参中有文件
由于MultipartFile接口往往是前端上传文件后由springboot封装好的流程绑定到后端接口上,无法实例化,因此需要自定义一个类实现MultipartFile接口
public class CustomMultipartFile implements MultipartFile { private final byte[] fileContent; private final String fileName; private final String contentType; public CustomMultipartFile(InputStream inputStream, String fileName, String contentType) throws IOException { this.fileContent = IoUtil.readBytes(inputStream); this.fileName = fileName; this.contentType = contentType; } public CustomMultipartFile(byte[] fileContent, String fileName, String contentType) throws IOException { this.fileContent = fileContent; this.fileName = fileName; this.contentType = contentType; } @Override public String getName() { return StringUtils.cleanPath(fileName); } @Override public String getOriginalFilename() { return StringUtils.cleanPath(fileName); } @Override public String getContentType() { return contentType; } @Override public boolean isEmpty() { return fileContent == null || fileContent.length == 0; } @Override public long getSize() { return fileContent.length; } @Override public byte[] getBytes() { return fileContent; } @Override public InputStream getInputStream() { InputStream inputStream=IoUtil.toStream(getBytes()); return inputStream; } @Override public void transferTo(java.io.File dest) throws IllegalStateException { throw new UnsupportedOperationException("transfer to file not supported"); } }
client和实际远程调用接口都需要额外添加注解中cosumes的值
@PostMapping(value = "/videoEncode/uploadVideo",consumes = MediaType.MULTIPART_FORM_DATA_VALUE) void uploadVideo(@RequestPart("multipartFile") MultipartFile multipartFile);
这样实际调用时可以传入实例化对象
CustomMultipartFile customMultipartFile = new CustomMultipartFile(inputStream, uploadVideo.getUrl().substring(uploadVideo.getUrl().lastIndexOf("/") + 1) , contentType); videoClient.uploadVideo(customMultipartFile);
响应是文件
需要用到InputstreamResource类进行装载
@PostMapping("/getVideoInputStream") public ResponseEntity<Resource> getVideoInputStream(@RequestBody UploadVideo uploadVideo){ String url=uploadVideo.getUrl(); InputStream inputStream = minioService.getObject(url.substring(url.lastIndexOf("/")+1)); 将文件流封装进InputStreamResource对象中 InputStreamResource resource = new InputStreamResource(inputStream); 将InputStreamResource对象封装进远程调用得到的响应的响应体中 return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, HEADERS_VALUES) .body(resource); }
client接口写法同传统json传递方法的写法,只是返回响应类型比较特殊
@PostMapping("/videoEncode/getVideoInputStream") ResponseEntity<Resource> getVideo(@RequestBody UploadVideo uploadVideo);
最后,宣传一下自己的仿b站前后端分离微服务项目,依赖版本号也在该项目的父pom.xml中
实现了以下功能:
视频的上传、查看与上传时获取封面
视频的点赞、评论、可同时新增和删除多个收藏记录的收藏、多功能的弹幕
用户的个人信息查看编辑、用户之间的关注
用户的个人主页权限修改、查看、由个人主页权限动态决定的用户个人主页内容的获取
手机号、邮箱、图形验证码的多种方式登录
支持临时会话的服务器为代理的一对一实时私聊
基于讯飞星火的文生文、文生图、(全网首发)智能PPT
关注up动态视频、评论、点赞、私聊消息的生成与推送
基于es实现的视频和用户的聚合搜索、推荐视频
网关的路由和统一鉴权与授权
基于双token的七天内无感刷新token
防csrf、xss、抓包、恶意上传脚本攻击
统一处理异常和泛型封装响应体、自定义修改响应序列化值
简易的仿redis缓存读取与数据过期剔除实现
xxl-job+ redis+ rocketmq+ es+ 布隆过滤器的自定义es与mysql数据同步
slueth+zipkin的多服务间请求链路追踪
集中多服务日志到一个文件目录下与按需添加特定内容入日志
多服务的详细接口文档
项目地址LABiliBili,github地址GitHub - aigcbilibili/aigcbilibili: 仿bilibili前后端实现,演示地址https://labilibili.com/video/演示.mp4,如果大家觉得有帮助的话可以去github点个小星星♪(・ω・)ノ
#腾讯##秋招##字节跳动##美团##阿里巴巴#