快速掌握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点个小星星♪(・ω・)ノ

#腾讯##秋招##字节跳动##美团##阿里巴巴#

该专栏存放前后端分离仿b站微服务项目相关教程

全部评论
1
点赞 回复 分享
发布于 07-14 20:09 湖南

相关推荐

多年后的我回首今天,是否会后悔那个选择点下接受阿里意向书的自己。如今的我回首当年,又是否应该坦然接受那个也会迷茫、也会彷徨、也会不知所措的自己。时间节点拨回2021年,那时的我刚刚成年,初入大学,踌躇满志打算拿下保研,虽完全不清楚未来在何方,但就是莫名地充满干劲,现在想想可能是高考的后遗症罢。然而现实是残酷的,我还记得2021.11.7的微积分期中考试,也是我入学以来第一次有关学分绩的大考,我的成绩只有25/30。也可能读到这里的朋友觉得只是扣了5分有什么大不了的,但当时我是我们六人寝中的最低分(当然现在看来是我大一室友都太强了hhhh),给我造成了很大的心理压力。而且就事实而言,这个分数在班...
有担当的灰太狼又在摸鱼:一口气看完了,感慨万千。 本科就业本就是一条少有人走的道路,全靠自己不断摸爬滚打,慢慢探索这条路该怎么走。 我的大一大二大多时间都在迷茫中度过,大二下才准备就业。那时候看到牛客上许多同龄人已经开始收割大厂offer,自己也想跟风投递却不出意料的石沉大海。 “世界上根本没有正确的选择,我们只不过是要努力奋斗,让当初自己做的选择变得正确。” 当时在声哥论坛里面第一次看到这句话时,就深深铭刻在心里。每次回家也总会有亲戚朋友询问以后的规划,当我说本科就业的时候,总会投来异样的眼光。甚至父母一开始也觉得荒谬。“现在这个社会,本科毕业能找到什么好工作?”每次这种时候,我总会想起还有这么优秀的前辈拿到大厂offer,心中也有了些许慰藉。 如今我还在准备寒假日常,希望能找到大厂offer。 尽管楼主一路坎坷,但总算最后结果还算圆满。祝前程似锦~
点赞 评论 收藏
分享
4 3 评论
分享
牛客网
牛客企业服务