快速掌握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站微服务项目相关教程与简历话术



