电脑知识|欧美黑人一区二区三区|软件|欧美黑人一级爽快片淫片高清|系统|欧美黑人狂野猛交老妇|数据库|服务器|编程开发|网络运营|知识问答|技术教程文章 - 好吧啦网

您的位置:首頁技術文章
文章詳情頁

Springboot+SpringSecurity+JWT實現用戶登錄和權限認證示例

瀏覽:5日期:2023-05-14 15:15:06

如今,互聯網項目對于安全的要求越來越嚴格,這就是對后端開發提出了更多的要求,目前比較成熟的幾種大家比較熟悉的模式,像RBAC 基于角色權限的驗證,shiro框架專門用于處理權限方面的,另一個比較流行的后端框架是Spring-Security,該框架提供了一整套比較成熟,也很完整的機制用于處理各類場景下的可以基于權限,資源路徑,以及授權方面的解決方案,部分模塊支持定制化,而且在和oauth2.0進行了很好的無縫連接,在移動互聯網的授權認證方面有很強的優勢,具體的使用大家可以結合自己的業務場景進行選取和使用

下面來說說關于單點登錄中目前比較流行的一種使用方式,就是springsecurity+jwt實現無狀態下用戶登錄;

JWT

在之前的篇章中大致提到過,使用jwt在分布式項目中進行用戶信息的認證很方便,各個模塊只需要知道配置的秘鑰,就可以解密token中用戶的基本信息,完成認證,很方便,關于使用jwt的基本內容可以查閱相關資料,或者參考我之前的一篇;

整理一下思路

1、搭建springboot工程2、導入springSecurity跟jwt的依賴3、用戶的實體類,dao層,service層(真正開發時再寫,這里就直接調用dao層操作數據庫)4、實現UserDetailsService接口5、實現UserDetails接口6、驗證用戶登錄信息的攔截器7、驗證用戶權限的攔截器8、springSecurity配置9、認證的Controller以及測試的controller

項目結構

Springboot+SpringSecurity+JWT實現用戶登錄和權限認證示例

pom文件

<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!-- https://mvnrepository.com/artifact/org.springframework.security.oauth/spring-security-oauth2 --><dependency><groupId>org.springframework.security.oauth</groupId><artifactId>spring-security-oauth2</artifactId><version>2.3.5.RELEASE</version></dependency><!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-jwt --><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-jwt</artifactId><version>1.0.10.RELEASE</version></dependency><!-- https://mvnrepository.com/artifact/org.springframework.security.oauth.boot/spring-security-oauth2-autoconfigure --><dependency><groupId>org.springframework.security.oauth.boot</groupId><artifactId>spring-security-oauth2-autoconfigure</artifactId><version>2.1.4.RELEASE</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><!-- mybatis依賴 --><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>1.3.0</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><!-- redis依賴 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency></dependencies>

application.properties

server.port=8091#數據庫連接spring.datasource.url=jdbc:mysql://localhost:3306/test?useUnicode=truespring.datasource.driver-class-name=com.mysql.jdbc.Driverspring.datasource.username=rootspring.datasource.password=root#mybatis配置mybatis.type-aliases-package=com.congge.entitymybatis.mapper-locations=classpath:mybatis/*.xml#redis配置spring.session.store-type=redisspring.redis.database=0spring,redis.host=127.0.0.1spring.redis.port=6379spring.redis.pool.min-idle=10000spring.redis.timeout=30000

為模擬用戶登錄,這里提前創建了一個測試使用的表,user

Springboot+SpringSecurity+JWT實現用戶登錄和權限認證示例

實體類User

public class User { private Integer id; private String username; private String password; private String role; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getRole() { return role; } public void setRole(String role) { this.role = role; } @Override public String toString() { return 'User{' +'id=' + id +', username=’' + username + ’’’ +', password=’' + password + ’’’ +', role=’' + role + ’’’ +’}’; }}

Jwt工具類,用于管理token相關的操作,可以單測使用

public class TestJwtUtils {public static final String TOKEN_HEADER = 'Authorization'; public static final String TOKEN_PREFIX = 'Bearer ';public static final String SUBJECT = 'congge';public static final long EXPIRITION = 1000 * 24 * 60 * 60 * 7;public static final String APPSECRET_KEY = 'congge_secret'; private static final String ROLE_CLAIMS = 'rol'; public static String generateJsonWebToken(Users user) {if (user.getId() == null || user.getUserName() == null || user.getFaceImage() == null) {return null;}Map<String,Object> map = new HashMap<>();map.put(ROLE_CLAIMS, 'rol');String token = Jwts.builder().setSubject(SUBJECT).setClaims(map).claim('id', user.getId()).claim('name', user.getUserName()).claim('img', user.getFaceImage()).setIssuedAt(new Date()).setExpiration(new Date(System.currentTimeMillis() + EXPIRITION)).signWith(SignatureAlgorithm.HS256, APPSECRET_KEY).compact();return token;}/** * 生成token * @param username * @param role * @return */public static String createToken(String username,String role) {Map<String,Object> map = new HashMap<>();map.put(ROLE_CLAIMS, role);String token = Jwts.builder().setSubject(username).setClaims(map).claim('username',username).setIssuedAt(new Date()).setExpiration(new Date(System.currentTimeMillis() + EXPIRITION)).signWith(SignatureAlgorithm.HS256, APPSECRET_KEY).compact();return token;}public static Claims checkJWT(String token) {try {final Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();return claims;} catch (Exception e) {e.printStackTrace();return null;}}/** * 獲取用戶名 * @param token * @return */public static String getUsername(String token){ Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody(); return claims.get('username').toString(); }/** * 獲取用戶角色 * @param token * @return */ public static String getUserRole(String token){ Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody(); return claims.get('rol').toString(); } /** * 是否過期 * @param token * @return */ public static boolean isExpiration(String token){ Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody(); return claims.getExpiration().before(new Date()); }public static void main(String[] args) {String name = 'acong';String role = 'rol';String token = createToken(name,role);System.out.println(token);Claims claims = checkJWT(token);System.out.println(claims.get('username'));System.out.println(getUsername(token));System.out.println(getUserRole(token));System.out.println(isExpiration(token));}/** * eyJhbGciOiJIUzI1NiJ9. * eyJzdWIiOiJjb25nZ2UiLCJpZCI6IjExMDExIiwibmFtZSI6Im51b3dlaXNpa2kiLCJpbWciOiJ3d3cudW9rby5jb20vMS5wbmciLCJpYXQiOjE1NTQ5OTI1NzksImV4cCI6MTU1NTU5NzM3OX0. * 6DJ9En-UBcTiMRldZeevJq3e1NxJgOWryUyim4_-tEE * * @param args *//*public static void main(String[] args) {Users user = new Users();user.setId('11011');user.setUserName('nuoweisiki');user.setFaceImage('www.uoko.com/1.png');String token = generateJsonWebToken(user);System.out.println(token);Claims claims = checkJWT(token);if (claims != null) {String id = claims.get('id').toString();String name = claims.get('name').toString();String img = claims.get('img').toString();String rol = claims.get('rol').toString();System.out.println('id:' + id);System.out.println('name:' + name);System.out.println('img:' + img);System.out.println('rol:' + rol);}}*/}

操作數據庫的類

,這里主要是提供用戶注冊的一個save用戶的方法,

@Servicepublic class UserService {@Autowiredprivate UserDao userDao;public void save(User user) {user.setId(1);userDao.save(user);}}

JwtUser

該類封裝登錄用戶相關信息,例如用戶名,密碼,權限集合等,需要實現UserDetails 接口,

public class JwtUser implements UserDetails { private Integer id; private String username; private String password; private Collection<? extends GrantedAuthority> authorities; public JwtUser() { } // 寫一個能直接使用user創建jwtUser的構造器 public JwtUser(User user) { id = user.getId(); username = user.getUsername(); password = user.getPassword(); authorities = Collections.singleton(new SimpleGrantedAuthority(user.getRole())); } public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } public String getPassword() { return password; } public String getUsername() { return username; } public boolean isAccountNonExpired() { return true; } public boolean isAccountNonLocked() { return true; } public boolean isCredentialsNonExpired() { return true; } public boolean isEnabled() { return true; } @Override public String toString() { return 'JwtUser{' +'id=' + id +', username=’' + username + ’’’ +', password=’' + password + ’’’ +', authorities=' + authorities +’}’; }}

配置攔截器

JWTAuthenticationFilter

JWTAuthenticationFilter繼承于UsernamePasswordAuthenticationFilter該攔截器用于獲取用戶登錄的信息,只需創建一個token并調用authenticationManager.authenticate()讓spring-security去進行驗證就可以了,不用自己查數據庫再對比密碼了,這一步交給spring去操作。 這個操作有點像是shiro的subject.login(new UsernamePasswordToken()),驗證的事情交給框架。

/** * 驗證用戶名密碼正確后,生成一個token,并將token返回給客戶端 * 該類繼承自UsernamePasswordAuthenticationFilter,重寫了其中的2個方法 , * attemptAuthentication:接收并解析用戶憑證。 * successfulAuthentication:用戶成功登錄后,這個方法會被調用,我們在這個方法里生成token并返回。 */public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter { private AuthenticationManager authenticationManager; public JWTAuthenticationFilter(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; super.setFilterProcessesUrl('/auth/login'); } @Override public Authentication attemptAuthentication(HttpServletRequest request,HttpServletResponse response) throws AuthenticationException { // 從輸入流中獲取到登錄的信息 try { LoginUser loginUser = new ObjectMapper().readValue(request.getInputStream(), LoginUser.class); return authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(loginUser.getUsername(), loginUser.getPassword()) ); } catch (IOException e) { e.printStackTrace(); return null; } } // 成功驗證后調用的方法 // 如果驗證成功,就生成token并返回 @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { JwtUser jwtUser = (JwtUser) authResult.getPrincipal(); System.out.println('jwtUser:' + jwtUser.toString()); String role = ''; Collection<? extends GrantedAuthority> authorities = jwtUser.getAuthorities(); for (GrantedAuthority authority : authorities){ role = authority.getAuthority(); } String token = TestJwtUtils.createToken(jwtUser.getUsername(), role); //String token = JwtTokenUtils.createToken(jwtUser.getUsername(), false); // 返回創建成功的token // 但是這里創建的token只是單純的token // 按照jwt的規定,最后請求的時候應該是 `Bearer token` response.setCharacterEncoding('UTF-8'); response.setContentType('application/json; charset=utf-8'); String tokenStr = JwtTokenUtils.TOKEN_PREFIX + token; response.setHeader('token',tokenStr); } @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { response.getWriter().write('authentication failed, reason: ' + failed.getMessage()); }}

JWTAuthorizationFilter

驗證成功當然就是進行鑒權了,每一次需要權限的請求都需要檢查該用戶是否有該權限去操作該資源,當然這也是框架幫我們做的,那么我們需要做什么呢?很簡單,只要告訴spring-security該用戶是否已登錄,是什么角色,擁有什么權限就可以了。JWTAuthenticationFilter繼承于BasicAuthenticationFilter,至于為什么要繼承這個我也不太清楚了,這個我也是網上看到的其中一種實現,實在springSecurity苦手,不過我覺得不繼承這個也沒事呢(實現以下filter接口或者繼承其他filter實現子類也可以吧)只要確保過濾器的順序,JWTAuthorizationFilter在JWTAuthenticationFilter后面就沒問題了。

/** * 驗證成功當然就是進行鑒權了 * 登錄成功之后走此類進行鑒權操作 */public class JWTAuthorizationFilter extends BasicAuthenticationFilter { public JWTAuthorizationFilter(AuthenticationManager authenticationManager) { super(authenticationManager); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String tokenHeader = request.getHeader(TestJwtUtils.TOKEN_HEADER); // 如果請求頭中沒有Authorization信息則直接放行了 if (tokenHeader == null || !tokenHeader.startsWith(TestJwtUtils.TOKEN_PREFIX)) { chain.doFilter(request, response); return; } // 如果請求頭中有token,則進行解析,并且設置認證信息 SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader)); super.doFilterInternal(request, response, chain); } // 這里從token中獲取用戶信息并新建一個token private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) { String token = tokenHeader.replace(TestJwtUtils.TOKEN_PREFIX, ''); String username = TestJwtUtils.getUsername(token); String role = TestJwtUtils.getUserRole(token); if (username != null){ return new UsernamePasswordAuthenticationToken(username, null, Collections.singleton(new SimpleGrantedAuthority(role)) ); } return null; }}

配置SpringSecurity

到這里基本操作都寫好啦,現在就需要我們將這些辛苦寫好的“組件”組合到一起發揮作用了,那就需要配置了。需要開啟一下注解@EnableWebSecurity然后再繼承一下WebSecurityConfigurerAdapter就可以啦,

@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled = true)public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired @Qualifier('userDetailsServiceImpl') private UserDetailsService userDetailsService; @Bean public BCryptPasswordEncoder bCryptPasswordEncoder(){ return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { http.cors().and().csrf().disable().authorizeRequests()// 測試用資源,需要驗證了的用戶才能訪問.antMatchers('/tasks/**').authenticated().antMatchers(HttpMethod.DELETE, '/tasks/**').hasRole('ADMIN')// 其他都放行了.anyRequest().permitAll().and().addFilter(new JWTAuthenticationFilter(authenticationManager())).addFilter(new JWTAuthorizationFilter(authenticationManager()))// 不需要session.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().exceptionHandling().authenticationEntryPoint(new JWTAuthenticationEntryPoint()); } @Bean CorsConfigurationSource corsConfigurationSource() { final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration('/**', new CorsConfiguration().applyPermitDefaultValues()); return source; }}

AuthController

測試類,模擬用戶注冊,

@RestController@RequestMapping('/auth')public class AuthController { @Autowired private UserService userService; @Autowired private BCryptPasswordEncoder bCryptPasswordEncoder; @PostMapping('/register') public String registerUser(@RequestBody Map<String,String> registerUser){ User user = new User(); user.setUsername(registerUser.get('username')); user.setPassword(bCryptPasswordEncoder.encode(registerUser.get('password'))); user.setRole('ROLE_USER'); userService.save(user); return 'success'; }}

注冊是有了,那登錄在哪呢?我們看一下UsernamePasswordAuthenticationFilter的源代碼

public UsernamePasswordAuthenticationFilter() { super(new AntPathRequestMatcher('/login', 'POST')); }

可以看出來默認是/login,所以登錄直接使用這個路徑就可以啦~當然也可以自定義只需要在JWTAuthenticationFilter的構造方法中加入下面那一句話就可以啦

public JWTAuthenticationFilter(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; super.setFilterProcessesUrl('/auth/login'); }

所以現在認證的路徑統一了一下也是挺好的~看起來相當舒服了注冊:/auth/register登錄:/auth/login

TaskController

提供一個外部訪問的API資源接口,即用戶要訪問該類下面的接口必須要先通過認證,后面的測試中也可以看出來,直接貼代碼,

@RequestMapping('/tasks')public class TaskController { @GetMapping('/getTasks') @ResponseBody public String listTasks(){ return '任務列表'; } @PostMapping @PreAuthorize('hasRole(’ADMIN’)') public String newTasks(){ return '創建了一個新的任務'; } }

下面我們來測試一下,為了模擬效果比較直觀點,我們使用postMan進行測試,

1、首先,我們調用注冊的方法注冊一個用戶,

Springboot+SpringSecurity+JWT實現用戶登錄和權限認證示例

注冊成功之后,我們看到數據庫已經有了一個用戶,

Springboot+SpringSecurity+JWT實現用戶登錄和權限認證示例

2、使用該用戶進行登錄,我們希望的是登錄成功之后,后臺生成一個token并返回給前端,這樣后面的接口調用中直接帶上這個token即可,

Springboot+SpringSecurity+JWT實現用戶登錄和權限認證示例

可以看到登錄成功,后臺反返回了token,下面我們使用這個token請求其他的接口,測試一下getTasks這個接口,注意需要在postMan的請求header里面帶上token信息,這里是全部的token,即包含Bearer 的整個字符串,

Springboot+SpringSecurity+JWT實現用戶登錄和權限認證示例

這時候,成功請求到了接口的數據,大家可以測試一下將過期時間調整的短一點,然后再去請求看看會有什么樣的效果,這里就不做演示了。

本篇到這里基本就結束了,關于springsecurity其實內容還是很多的,里面的用法也比較復雜,大家抽空可以做深入的研究,篇幅原因不做過多介紹了。最后感謝觀看。

附上源碼地址:boot-ssoserver_jb51.rar

到此這篇關于Springboot+SpringSecurity+JWT實現用戶登錄和權限認證示例的文章就介紹到這了,更多相關Springboot SpringSecurity JWT內容請搜索好吧啦網以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持好吧啦網!

標簽: Spring
相關文章:
主站蜘蛛池模板: 邢台人才网_邢台招聘网_邢台123招聘【智达人才网】 | 固诺家居-全屋定制十大品牌_整体衣柜木门橱柜招商加盟 | 石英陶瓷,石英坩埚,二氧化硅陶瓷-淄博百特高新材料有限公司 | 杭州火蝠电商_京东代运营_拼多多全托管代运营【天猫代运营】 | 聚天冬氨酸,亚氨基二琥珀酸四钠,PASP,IDS - 远联化工 | 西点培训学校_法式西点培训班_西点师培训_西点蛋糕培训-广州烘趣西点烘焙培训学院 | 电镀标牌_电铸标牌_金属标贴_不锈钢标牌厂家_深圳市宝利丰精密科技有限公司 | 电脑刺绣_绣花厂家_绣花章仔_织唛厂家-[源欣刺绣]潮牌刺绣打版定制绣花加工厂家 | 陕西自考报名_陕西自学考试网 | 彼得逊采泥器-定深式采泥器-电动土壤采样器-土壤样品风干机-常州索奥仪器制造有限公司 | 山东集装箱活动房|济南集装箱活动房-济南利森集装箱有限公司 | 武汉不干胶印刷_标签设计印刷_不干胶标签印刷厂 - 武汉不干胶标签印刷厂家 | 广州云仓代发-昊哥云仓专业电商仓储托管外包代发货服务 | 温州中研白癜风专科_温州治疗白癜风_温州治疗白癜风医院哪家好_温州哪里治疗白癜风 | 冷却塔风机厂家_静音冷却塔风机_冷却塔电机维修更换维修-广东特菱节能空调设备有限公司 | 「钾冰晶石」氟铝酸钾_冰晶石_氟铝酸钠「价格用途」-亚铝氟化物厂家 | 消防泵-XBD单级卧式/立式消防泵-上海塑泉泵阀(集团)有限公司 | 低噪声电流前置放大器-SR570电流前置放大器-深圳市嘉士达精密仪器有限公司 | 合肥注册公司|合肥代办营业执照、2024注册公司流程 | 超声波电磁流量计-液位计-孔板流量计-料位计-江苏信仪自动化仪表有限公司 | 深圳市八百通智能技术有限公司官方网站 | 仓储笼_金属箱租赁_循环包装_铁网箱_蝴蝶笼租赁_酷龙仓储笼租赁 测试治具|过炉治具|过锡炉治具|工装夹具|测试夹具|允睿自动化设备 | 直流电能表-充电桩电能表-导轨式电能表-智能电能表-浙江科为电气有限公司 | 苏州注册公司_苏州代理记账_苏州工商注册_苏州代办公司-恒佳财税 | 地图标注-手机导航电子地图如何标注-房地产商场地图标记【DiTuBiaoZhu.net】 | [官网]叛逆孩子管教_戒网瘾学校_全封闭问题青少年素质教育_新起点青少年特训学校 | 细砂提取机,隔膜板框泥浆污泥压滤机,螺旋洗砂机设备,轮式洗砂机械,机制砂,圆锥颚式反击式破碎机,振动筛,滚筒筛,喂料机- 上海重睿环保设备有限公司 | 伟秀电气有限公司-10kv高低压开关柜-高低压配电柜-中置柜-充气柜-欧式箱变-高压真空断路器厂家 | 微妙网,专业的动画师、特效师、CG模型设计师网站! - wmiao.com 超声波电磁流量计-液位计-孔板流量计-料位计-江苏信仪自动化仪表有限公司 | 沈阳缠绕包装机厂家直销-沈阳海鹞托盘缠绕包装机价格 | 贝朗斯动力商城(BRCPOWER.COM) - 买叉车蓄电池上贝朗斯商城,价格更超值,品质有保障! | 企业管理培训,企业培训公开课,企业内训课程,企业培训师 - 名课堂企业管理培训网 | 吉祥新世纪铝塑板_生产铝塑板厂家_铝塑板生产厂家_临沂市兴达铝塑装饰材料有限公司 | 口臭的治疗方法,口臭怎么办,怎么除口臭,口臭的原因-口臭治疗网 | 工控机-工业平板电脑-研华工控机-研越无风扇嵌入式box工控机 | 铝合金线槽_铝型材加工_空调挡水板厂家-江阴炜福金属制品有限公司 | 刑事律师_深圳著名刑事辩护律师_王平聚【清华博士|刑法教授】 | 绿萝净除甲醛|深圳除甲醛公司|测甲醛怎么收费|培训机构|电影院|办公室|车内|室内除甲醛案例|原理|方法|价格立马咨询 | 电动葫芦|手拉葫芦|环链电动葫芦|微型电动葫芦-北京市凌鹰起重机械有限公司 | 众品地板网-地板品牌招商_地板装修设计_地板门户的首选网络媒体。 | 土壤检测仪器_行星式球磨仪_土壤团粒分析仪厂家_山东莱恩德智能科技有限公司 |