2 springsecurity-jwt整合

欢迎关注博主公众号「Java大师」,

专注于分享Java领域干货文章http://www.javaman.cn/sb2/jwt

2.1整合springsecurity

1)

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

2.2认证授权流程

认证管理

流程图解读:

1、用户提交用户名、密码被SecurityFilterChain中的 UsernamePasswordAuthenticationFilter 过滤器获取到, 封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。

2、然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证 。

3、认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息, 身份信息,细节信息,但密码通常会被移除) Authentication 实例。

4、SecurityContextHolder 安全上下文容器将第3步填充了信息的 Authentication ,通过 SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。 可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它 的实现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个 List 列表,存放多种认证方式,最终实际的认证工作是由 AuthenticationProvider完成的。咱们知道web表单的对应的AuthenticationProvider实现类为 DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终 AuthenticationProvider将UserDetails填充至Authentication。

授权管理

访问资源(即授权管理),访问url时,会通过FilterSecurityInterceptor拦截器拦截,其中会调用SecurityMetadataSource的方法来获取被拦截url所需的全部权限,再调用授权管理器AccessDecisionManager,这个授权管理器会通过spring的全局缓存SecurityContextHolder获取用户的权限信息,还会获取被拦截的url和被拦截url所需的全部权限,然后根据所配的投票策略(有:一票决定,一票否定,少数服从多数等),如果权限足够,则决策通过,返回访问资源,请求放行,否则跳转到403页面、自定义页面。

2.3编写自己的UserDetails和UserDetailService

2.3.1UserDetails
package com.ds.book.entity;

import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.util.Collection; import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails; /**
* <p>
*
* </p>
*
* @author java大师
* @since 2023-03-17
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("t_user")
public class User implements Serializable, UserDetails { private static final long serialVersionUID = 1L; private Integer id; /**
* 登录名
*/
private String name; /**
* 用户名
*/
private String username; /**
* 密码
*/
private String password; /**
* 是否有效:1-有效;0-无效
*/
private String status; @Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles
.stream()
.map(role -> new SimpleGrantedAuthority(role.getRoleCode()))
.collect(Collectors.toList());
} @Override
public boolean isAccountNonExpired() {
return true;
} @Override
public boolean isAccountNonLocked() {
return true;
} @Override
public boolean isCredentialsNonExpired() {
return true;
} @Override
public boolean isEnabled() {
return true;
}
}
2.3.2userDetailService

登录成功后,将UserDetails的roles设置到用户中

package com.ds.book.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.ds.book.entity.User;
import com.ds.book.mapper.UserMapper;
import com.ds.book.service.IUserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service; /**
* <p>
* 服务实现类
* </p>
*
* @author java大师
* @since 2023-03-17
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService, UserDetailsService { @Autowired
private UserMapper userMapper; @Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User loginUser = userMapper.selectOne(new QueryWrapper<User>().eq("username", username));
if (loginUser == null){
throw new UsernameNotFoundException("用户名或密码错误");
}
loginUser.setRoles(userMapper.getRolesByUserId(loginUser.getId()));
return loginUser;
}
}
2.3.2加载userDetailService

将我们自己的UserDetailService注入springsecurity

package com.ds.book.config;

import com.ds.book.filter.JwtTokenFilter;
import com.ds.book.service.impl.UserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired
private UserServiceImpl userService; @Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
} //注入我们自己的UserDetailService
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
}

问题:前后端分离项目,通常不会使用springsecurity自带的登录界面,登录界面由前端完成,后台只需要提供响应的服务即可,且目前主流不会采用session去存取用户,后端会返回响应的token,前端访问的时候,会在headers里面带入token.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pW7SxQqz-1679881634010)(D:\个人\公众号\网站\dsblog开发手册\image-20230322114008186.png)]

2.4JwtToken

2.4.1 JWT描述

Jwt token由Header、Payload、Signature三部分组成,这三部分之间以小数点”.”连接,JWT token长这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.keH6T3x1z7mmhKL1T3r9sQdAxxdzB6siemGMr_6ZOwU

token解析后长这样:

header部分,有令牌的类型(JWT)和签名算法名称(HS256):

{

"alg": "HS256",

"typ": "JWT"

}

Payload部分,有效负载,这部分可以放任何你想放的数据:

{

"sub": "1234567890",

"name": "John Doe",

"iat": 1516239022

}

Signature签名部分,由于这部分是使用header和payload部分计算的,所以还可以以此来验证payload部分有没有被篡改:

HMACSHA256(

base64UrlEncode(header) + "." +

base64UrlEncode(payload),

123456 //这里是密钥,只要够复杂,一般不会被破解

)

2.4.2 pom.xml
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
2.4.3 JwtToken工具类
package com.ds.book.tool;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm; import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID; /**
* JWT工具类
*/
public class JwtUtil { //有效期为
public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一个小时
//设置秘钥明文
public static final String JWT_KEY = "dashii"; public static String getUUID(){
String token = UUID.randomUUID().toString().replaceAll("-", "");
return token;
} /**
* 生成jtw
* @param subject token中要存放的数据(json格式)
* @return
*/
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
return builder.compact();
} /**
* 生成jtw
* @param subject token中要存放的数据(json格式)
* @param ttlMillis token超时时间
* @return
*/
public static String createJWT(String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
return builder.compact();
} private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if(ttlMillis==null){
ttlMillis= JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
return Jwts.builder()
.setId(uuid) //唯一的ID
.setSubject(subject) // 主题 可以是JSON数据
.setIssuer("dashi") // 签发者
.setIssuedAt(now) // 签发时间
.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
.setExpiration(expDate);
} /**
* 创建token
* @param id
* @param subject
* @param ttlMillis
* @return
*/
public static String createJWT(String id, String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
return builder.compact();
} /**
* 生成加密后的秘钥 secretKey
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
} /**
* 解析
*
* @param jwt
* @return
* @throws Exception
*/
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}
2.4.4 JwtTokenFilter
package com.ds.book.filter;

import com.ds.book.entity.User;
import com.ds.book.mapper.UserMapper;
import com.ds.book.service.IMenuService;
import com.ds.book.service.IUserService;
import com.ds.book.tool.JwtUtil;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException; @Component
public class JwtTokenFilter extends OncePerRequestFilter { @Autowired
private IUserService userService;
@Autowired
private UserMapper userMapper; @Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
//1、获取token
String token = httpServletRequest.getHeader("token");
if (StringUtils.isEmpty(token)){
filterChain.doFilter(httpServletRequest,httpServletResponse);
return;
}
String userId;
try {
Claims claims = JwtUtil.parseJWT(token);
userId = claims.getSubject();
} catch (Exception exception) {
exception.printStackTrace();
throw new RuntimeException("token非法");
}
User user = userService.getUserById(Integer.parseInt(userId));
user.setRoles(userMapper.getRolesByUserId(Integer.parseInt(userId)));
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}

在springsecurity中,第一个经过的过滤器是UsernamePasswordAuthenticationFilter,所以前后端分离的项目,我们自己定义的过滤器要放在这个过滤器前面,具体配置如下

@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated();
http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
http.cors();
}
2.4.5授权
2.4.5.1 开启preAuthorize进行收取(Controller路径匹配)

1)主启动类上添加EnableGlobalMethodSecurity注解

@EnableGlobalMethodSecurity(prePostEnabled = true)
@SpringBootApplication
@MapperScan("com.ds.book.mapper")
public class BookSysApplication {
public static void main(String[] args) {
SpringApplication.run(BookSysApplication.class,args);
}
}

2)Controller方法上添加@PreAuthorize注解

@RestController
public class HelloController { @GetMapping("/hello")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public String hello(){
return "hello";
}
}
2.4.5.2 增强方式授权(数据库表配置)

1)创建我们自己的FilterInvocationSecurityMetadataSource,实现getAttributes方法,获取请求url所需要的角色

@Component
public class MySecurtiMetaDataSource implements FilterInvocationSecurityMetadataSource { @Autowired
private IMenuService menuService;
AntPathMatcher antPathMatcher = new AntPathMatcher(); //获取访问url需要的角色,例如:/sys/user需要ROLE_ADMIN角色,访问sys/user时获取到必须要有ROLE_ADMIN角色。返回 Collection<ConfigAttribute>
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
String requestURI = ((FilterInvocation) object).getRequest().getRequestURI();
//获取所有的菜单及角色
List<Menu> menus = menuService.getMenus();
for (Menu menu : menus) {
if (antPathMatcher.match(menu.getUrl(),requestURI)){
String[] roles = menu.getRoles().stream().map(role -> role.getRoleCode()).toArray(String[]::new);
return SecurityConfig.createList(roles);
}
}
return null;
} @Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
} @Override
public boolean supports(Class<?> clazz) {
return false;
}
}

2)创建我们自己的决策管理器AccessDecisionManager,实现decide方法,判断步骤1)中获取到的角色和我们目前登录的角色是否相同,相同则允许访问,不相同则不允许访问,

@Component
public class MyAccessDecisionManager implements AccessDecisionManager { //1、认证通过后,会往authentication中填充用户信息
//2、拿authentication中的权限与上一步获取到的角色信息进行比对,比对成功后,允许访问
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (ConfigAttribute configAttribute : configAttributes) {
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(configAttribute.getAttribute())){
return;
}
}
}
throw new AccessDeniedException("权限不足,请联系管理员");
} @Override
public boolean supports(ConfigAttribute attribute) {
return false;
} @Override
public boolean supports(Class<?> clazz) {
return false;
}
}

3)在SecurityConfig中,添加后置处理器(增强器),让springsecurity使用我们自己的datametasource和decisionMananger

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired
private MySecurtiMetaDataSource mySecurtiMetaDataSource;
@Autowired
private MyAccessDecisionManager myAccessDecisionManager;
@Autowired
private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
@Autowired
private MyAccessDeniedHandler myAccessDeniedHandler; @Autowired
private UserServiceImpl userService; @Autowired
private JwtTokenFilter jwtTokenFilter; @Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
} @Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
} @Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
//后置处理器,使用我们自己的FilterSecurityInterceptor拦截器配置
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor> () {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setSecurityMetadataSource(mySecurtiMetaDataSource);
o.setAccessDecisionManager(myAccessDecisionManager);
return o;
}
})
.and()
.headers().cacheControl();
http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
http.cors();
}
}
2.4.6异常处理

1)前端渲染工具类

public class WebUtils
{
/**
* 将字符串渲染到客户端
*
* @param response 渲染对象
* @param string 待渲染的字符串
* @return null
*/
public static String renderString(HttpServletResponse response, String string) {
try
{
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(string);
}
catch (IOException e)
{
e.printStackTrace();
}
return null;
}
}

2)未登录异常处理,实现commence方法

@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
Result result = new Result(401,"未登录,请先登录",null);
String json = JSON.toJSONString(result);
WebUtils.renderString(httpServletResponse,json); }
}

3)授权失败异常处理,实现Handle方法


@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
Result result = new Result(403,"权限不足请联系管理员",null);
String s = JSON.toJSONString(result);
WebUtils.renderString(httpServletResponse,s);
}
}

springsecurity-jwt整合的更多相关文章

  1. 从零玩转SpringSecurity+JWT整合前后端分离

    从零玩转SpringSecurity+JWT整合前后端分离 2021年4月9日 · 预计阅读时间: 50 分钟 一.什么是Jwt? Json web token (JWT), 是为了在网络应用环境间传 ...

  2. SpringBoot+SpringSecurity+jwt整合及初体验

    原来一直使用shiro做安全框架,配置起来相当方便,正好有机会接触下SpringSecurity,学习下这个.顺道结合下jwt,把安全信息管理的问题扔给客户端, 准备 首先用的是SpringBoot, ...

  3. SpringSecurity之整合JWT

    SpringSecurity之整合JWT 目录 SpringSecurity之整合JWT 1. 写在前面的话 2. JWT依赖以及工具类的编写 3. JWT过滤器 4. 登录成功结果处理器 5. Sp ...

  4. 厉害!我带的实习生仅用四步就整合好SpringSecurity+JWT实现登录认证!

    小二是新来的实习生,作为技术 leader,我还是很负责任的,有什么锅都想甩给他,啊,不,一不小心怎么把心里话全说出来了呢?重来! 小二是新来的实习生,作为技术 leader,我还是很负责任的,有什么 ...

  5. SpringBoot2.0+Shiro+JWT 整合

    SpringBoot2.0+Shiro+JWT 整合 JSON Web Token(JWT)是一个非常轻巧的规范.这个规范允许我们使用 JWT 在用户和服务器之间传递安全可靠的信息. 我们利用一定的编 ...

  6. SpringBoot 集成SpringSecurity JWT

    目录 1. 简介 1.1 SpringSecurity 1.2 OAuth2 1.3 JWT 2. SpringBoot 集成 SpringSecurity 2.1 导入Spring Security ...

  7. shiro的使用与JWT整合

    一.shiro入门 两大框架对比:安全框架Shiro和SpringSecurity的比较 了解shiro 什么是Shiro Apache Shiro是一个Java的安全(权限)框架.| Shiro可以 ...

  8. SpringSecurity+Jwt遇到的bug

    最近在使用springsecurity整合Jwt的时候,遇到了一Bug,卡住了很久,记录一下. 编写Jwt工具类JwtUtil,编写Jwt认证的核心过滤器JwtAuthenticationFilter ...

  9. 轻松上手SpringBoot+SpringSecurity+JWT实RESTfulAPI权限控制实战

    前言 我们知道在项目开发中,后台开发权限认证是非常重要的,springboot 中常用熟悉的权限认证框架有,shiro,还有就是springboot 全家桶的 security当然他们各有各的好处,但 ...

  10. springSecurity + jwt + redis 前后端分离用户认证和授权

    记录一下使用springSecurity搭建用户认证和授权的代码... 技术栈使用springSecurity + redis + JWT + mybatisPlus 部分代码来自:https://b ...

随机推荐

  1. iOS源码调试Podspec如何写

    { "name": "XXX", "version": "1.0.0", "summary": &q ...

  2. 【flask】建站经验随笔

    [前端] 1.前端table标签中每行使用template中 {%for i in rows%} {% endfor %}来生成之后,如果想对每行进行一个button处理,此时如果使用jquery的$ ...

  3. 数组扩展(Java)

    Arrays类 基本介绍 数组的工具类java.util.Arrays 由于数组本身中没有什么方法可供我们调用,但API中提供了一个工具类Arrays供我们使用,从而可以对数据对象进行一些基本操作 查 ...

  4. 1、Java程序概述

    1.什么是Java? Java是一个完整的平台,有一个庞大的库,其中包含了很多可重用的代码,以及一个提供诸如安全性.跨操作系统的可移植性以及自动垃圾收集等服务的执行环境. 2.Java白皮书的关键术语 ...

  5. JS变量之间赋值,修改变量值,原变量会随之改变的问题

    现象: 开发vue项目的过程中,需要多次用到一份基础数据,为减少代码量,提高一下复用效果,便用变量A来定义,在项目中需要用到时就用变量A进行赋值. 在项目中调用时,我新定义一个变量B,再将变量A赋值给 ...

  6. tortoiseGit配置和git常用命令

    tortoiseGit配置:https://blog.csdn.net/hjwdz2015/article/details/90487554 常用命令 一.git config --global us ...

  7. sqlite bundle 的含义,和 sqlite.dll, SQLite.Interop.dll, System.Data.SQLite.dll 三者之间的关系

    sqlite bundle 的含义,和 sqlite.dll, SQLite.Interop.dll, System.Data.SQLite.dll 三者之间的关系. bundle 表示不需要配合 S ...

  8. 杨辉三角形实现过程详解-C语言基础

    这一篇要探讨的是"杨辉三角形的实现以及如何人工走循环".涉及的知识点和内容很少,主要是想说明如何看懂循环,如何跟着循环走.属于C语言基础篇. 学习编程的人,在学习的初期,几乎都会接 ...

  9. 10.10 2020 实验 6:OpenDaylight 实验——OpenDaylight 及 Postman 实现流表下发

    一.实验目的 熟悉 Postman 的使用:熟悉如何使用 OpenDaylight 通过 Postman 下发流表.   二.实验任务 推荐阅读:SDNLAB 文章:OpenFlow 协议超时机制简介 ...

  10. protobuf怎么处理java中的Object和Object[],protobuf的bytestring和object[]

    如题,作者一开始也遇到了这个比较棘手的问题. 话不多说,直接说解决方案. 这里使用bytestring,如果是object[]的话则用repeated定义即可. 那么问题又来了,用这个类型怎么做到与j ...