Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

How to use Java Security Framework Shiro

2025-01-18 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >

Share

Shulou(Shulou.com)06/01 Report--

This article mainly explains "how to use the Java security framework Shiro". The content of the article is simple and clear, and it is easy to learn and understand. Please follow the editor's train of thought to study and learn how to use the Java security framework Shiro.

Introduction to Shiro

Apache Shiro is a powerful and easy-to-use Java security framework that performs authentication, authorization, password, and session management

Three core components: Subject, SecurityManager and Realms

Subject represents the security operation of the current user

SecurityManager manages the security operations of all users and is the core of the Shiro framework. Shiro manages internal component instances through SecurityManager and provides various services for security management.

Realm acts as a "bridge" or "connector" between Shiro and application security data. That is, when authentication (login) and authorization (access control) authentication are performed on a user, Shiro looks up the user and their permission information from the Realm configured by the application.

Realm is essentially a security-related DAO: it encapsulates the connection details of the data source and provides the relevant data to the Shiro when needed. When configuring Shiro, you must specify at least one Realm for authentication and / or authorization. It is possible to configure multiple Realm, but at least one is required.

Getting started with Shiro

Import dependency

Org.apache.shiro shiro-core 1.7.1 org.slf4j jcl-over-slf4j 2.0.0-alpha1 org.slf4j slf4j-log4j12 2.0.0-alpha1 log4j Log4j 1.2.17

Configure log4j.properties

Log4j.rootLogger=INFO, stdoutlog4j.appender.stdout=org.apache.log4j.ConsoleAppenderlog4j.appender.stdout.layout=org.apache.log4j.PatternLayoutlog4j.appender.stdout.layout.ConversionPattern=%d% p [% c] -% m% n # General Apache librarieslog4j.logger.org.apache=WARN# Springlog4j.logger.org.springframework=WARN# Default Shiro logginglog4j.logger.org.apache.shiro=INFO# Disable verbose logginglog4j.logger.org.apache.shiro.util.ThreadContext=WARNlog4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN

Configure Shiro.ini (you need to import the ini plug-in in IDEA)

[users] # user 'root' with password' secret' and the 'admin' roleroot = secret, admin# user' guest' with the password 'guest' and the' guest' roleguest = guest, guest# user 'presidentskroob' with password' 12345'("That's the same combination on# my luggage!" ), and role 'president'presidentskroob = 12345, president# user' darkhelmet' with password 'ludicrousspeed' and roles' darklord' and 'schwartz'darkhelmet = ludicrousspeed, darklord, schwartz# user' lonestarr' with password 'vespa' and roles' goodguy' and 'schwartz'lonestarr = vespa, goodguy Schwartz#-# Roles with assigned permissions## Each line conforms to the format defined in the# org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions JavaDoc#- -[roles] # 'admin' role has all permissions Indicated by the wildcard'* 'admin = * # The' schwartz' role can do anything (*) with any lightsaber:schwartz = lightsaber:*# The 'goodguy' role is allowed to' drive' (action) the winnebago (type) with# license plate 'eagle5' (instance specific id) goodguy = winnebago:drive:eagle5

Quick start implementation class quickStart.java

Import org.apache.shiro.SecurityUtils;import org.apache.shiro.authc.*;import org.apache.shiro.config.IniSecurityManagerFactory;import org.apache.shiro.mgt.DefaultSecurityManager;import org.apache.shiro.realm.text.IniRealm;import org.apache.shiro.session.Session;import org.apache.shiro.subject.Subject;import org.apache.shiro.util.Factory;import org.slf4j.Logger;import org.slf4j.LoggerFactory;public class quickStart {private static final transient Logger log = LoggerFactory.getLogger (quickStart.class) / * Shiro three major objects: Subject: user SecurityManager: manage all users Realm: connect data * / public static void main (String [] args) {/ / the easiest way to create Shiro SecurityManager with configuration / / realms, users, roles and permissions is to use a simple INI configuration. / / We will use a factory that can extract .ini files to do this, / / return an instance of SecurityManager: / / use the shiro.ini file / / (file: and url: prefix to load from file and url, respectively) at the root of the classpath: / / Factory factory = new IniSecurityManagerFactory ("classpath:shiro.ini"); / / SecurityManager securityManager = factory.getInstance () DefaultSecurityManager securityManager = new DefaultSecurityManager (); IniRealm iniRealm = new IniRealm ("classpath:shiro.ini"); securityManager.setRealm (iniRealm); / / for this simple example to get started, make SecurityManager / / accessible as a JVM singleton. Most applications do not do this / / but rely on their container configuration or web.xml for / / webapps. This is beyond the scope of this simple quick start, so / / We only do minimal work so that you can continue to feel things. SecurityUtils.setSecurityManager (securityManager); / / now that you've established a simple Shiro environment, let's see what you can do: / / get the current user object Subject Subject currentUser = SecurityUtils.getSubject (); / / do something with Session (no Web or EJB container!! Session session = currentUser.getSession (); / / get Session session.setAttribute ("someKey", "aValue") through the current user; String value = (String) session.getAttribute ("someKey"); if (value.equals ("aValue")) {log.info ("Retrieved the correct value! [" + value + "]")) } / / determine whether the current user is authenticated by if (! currentUser.isAuthenticated ()) {/ / token: token, not obtained, random UsernamePasswordToken token = new UsernamePasswordToken ("lonestarr", "vespa"); token.setRememberMe (true); / / set to remember me try {currentUser.login (token) / / perform login operation} catch (UnknownAccountException uae) {/ / print out the user name log.info ("There is no user with username of" + token.getPrincipal ());} catch (IncorrectCredentialsException ice) {/ / print out the password log.info ("Password for account" + token.getPrincipal () + "was incorrect!") } catch (LockedAccountException lae) {log.info ("The account for username" + token.getPrincipal () + "is locked. "+" Please contact your administrator to unlock it. ");} / /. Catch more exceptions here (perhaps a custom exception for your application? Catch (AuthenticationException ae) {/ / unexpected condition? Error?} / / say who they are: / / print their identifying principal (in this case, a username): log.info ("User [" + currentUser.getPrincipal () + "] logged in successfully."); / / test a role: if (currentUser.hasRole ("schwartz")) {log.info ("May the Schwartz be with you!") } else {log.info ("Hello, mere mortal.");} / test a typed permission (not instance-level) if (currentUser.isPermitted ("lightsaber:wield")) {log.info ("You may use a lightsaber ring. Use it wisely. ");} else {log.info (" Sorry, lightsaber rings are for schwartz masters only. ");} / a (very powerful) Instance Level permission: if (currentUser.isPermitted (" winnebago:drive:eagle5 ")) {log.info (" You are permitted to 'drive' the winnebago with license plate (id)' eagle5'. ") "+" Here are the keys-have fun! ");} else {log.info (" Sorry, you aren't allowed to drive the 'eagle5' winnebago! ");} / / all done-log out! CurrentUser.logout (); / / Logout of System.exit (0); / / exit}}

Start the test

SpringBoot-Shiro integration (complete code will be attached at the end)

Preliminary work

Import shiro-spring consolidation package dependencies

Org.apache.shiro shiro-spring 1.7.1

Jump to the page

Index.html

Home page

Add | update

Add.html

Add

Add

Update.html

Update

Update

Write the configuration class ShiroConfig.java of shiro

Package com.example.config;import org.apache.shiro.spring.web.ShiroFilterFactoryBean;import org.apache.shiro.web.mgt.DefaultWebSecurityManager;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import java.util.LinkedHashMap;import java.util.Map;@Configurationpublic class ShiroConfig {/ / 3. ShiroFilterFactoryBean @ Bean public ShiroFilterFactoryBean getshiroFilterFactoryBean (@ Qualifier ("SecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean () / / set the security manager factoryBean.setSecurityManager (defaultWebSecurityManager); return factoryBean;} / / 2. Create DefaultWebSecurityManager @ Bean (name = "SecurityManager") public DefaultWebSecurityManager getDefaultWebSecurityManager (@ Qualifier ("userRealm") UserRealm userRealm) {DefaultWebSecurityManager SecurityManager=new DefaultWebSecurityManager (); / / 3. Associate Realm SecurityManager.setRealm (userRealm); return SecurityManager;} / / 1. Create a Realm object @ Bean (name = "userRealm") public UserRealm userRealm () {return new UserRealm ();}}

Write UserRealm.java

Package com.example.config;import org.apache.shiro.authc.AuthenticationException;import org.apache.shiro.authc.AuthenticationInfo;import org.apache.shiro.authc.AuthenticationToken;import org.apache.shiro.authz.AuthorizationInfo;import org.apache.shiro.realm.AuthorizingRealm;import org.apache.shiro.subject.PrincipalCollection;public class UserRealm extends AuthorizingRealm {@ Override protected AuthorizationInfo doGetAuthorizationInfo (PrincipalCollection principalCollection) {System.out.println ("authorization"); return null } @ Override protected AuthenticationInfo doGetAuthenticationInfo (AuthenticationToken authenticationToken) throws AuthenticationException {System.out.println ("certification"); return null;}}

Write controller to test whether the environment has been set up.

Package com.example.controller;import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.RequestMapping;@Controllerpublic class MyController {@ RequestMapping ({"/", "/ index"}) public String index (Model model) {model.addAttribute ("msg", "hello,shiro"); return "index";} @ RequestMapping ("/ user/add") public String add () {return "user/add" @ RequestMapping ("/ user/update") public String update () {return "user/update";}}

Implement login interception

Add an intercept to the ShiroConfig.java file

Map filterMap = new LinkedHashMap (); / / A pair of files under / user/* can only access filterMap.put ("/ user/*", "authc") if they have authc permission; / / store Map in ShiroFilterFactoryBean factoryBean.setFilterChainDefinitionMap (filterMap)

In this way, the code runs, and you click add or update and there will be a 404 error. At this time, we will continue to add it and let it jump to our custom login page.

Add login block to login page

/ / Jump to toLogin factoryBean.setLoginUrl ("/ toLogin") when permission authentication is required; / / Jump to unauthorized factoryBean.setUnauthorizedUrl ("/ unauthorized") if permission authentication fails

Login.html

Login user name:

Password:

View jump add a login page jump

@ RequestMapping ("/ toLogin") public String login () {return "login";}

Above, we have successfully intercepted, and now let's implement user authentication.

First, we need a login page

Login.html

Log in

User name:

Password:

Second, go to controller to write and jump to the login page

@ RequestMapping ("/ login") public String login (String username,String password,Model model) {/ / get the current user Subject subject = SecurityUtils.getSubject (); / / encapsulate the user data UsernamePasswordToken taken = new UsernamePasswordToken (username,password); try {/ / execute the login operation. If no exception occurs, the login is successful subject.login (taken); return "index" } catch (UnknownAccountException e) {model.addAttribute ("msg", "user name error"); return "login";} catch (IncorrectCredentialsException e) {model.addAttribute ("msg", "password error"); return "login";}}

Finally go to UserRealm.java to configure authentication

/ Certification @ Override protected AuthenticationInfo doGetAuthenticationInfo (AuthenticationToken authenticationToken) throws AuthenticationException {System.out.println ("certification"); String name = "root"; String password = "123456"; UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken; if (! userToken.getUsername () .equals (name)) {return null / / throw exception username error the exception} / / password authentication, shiro does return new SimpleAuthenticationInfo (", password,");}

Run the test, successful!

Attach the final complete code

Dependency introduced by pom.xml

Pom.xml

4.0.0 org.springframework.boot spring-boot-starter-parent 2.4.4 com.example springboot-08-shiro 0.0.1-SNAPSHOT springboot-08-shiro Demo project for Spring Boot 1.8 org.apache.shiro shiro-spring 1.7.1 Org.springframework.boot spring-boot-starter-thymeleaf org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test org .springframework.boot spring-boot-maven-plugin

Static resources

Index.html

Home page

Add | update

Login.html

Log in

User name:

Password:

Add.html

Add

Add

Update.html

Update

Update

Controller layer

MyController.java

Package com.example.controller;import org.apache.shiro.SecurityUtils;import org.apache.shiro.authc.IncorrectCredentialsException;import org.apache.shiro.authc.UnknownAccountException;import org.apache.shiro.authc.UsernamePasswordToken;import org.apache.shiro.subject.Subject;import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.RequestMapping Controllerpublic class MyController {@ RequestMapping ({"/", "/ index"}) public String index (Model model) {model.addAttribute ("msg", "hello,shiro"); return "index";} @ RequestMapping ("/ user/add") public String add () {return "user/add";} @ RequestMapping ("/ user/update") public String update () {return "user/update" } @ RequestMapping ("/ toLogin") public String toLogin () {return "login";} @ RequestMapping ("/ login") public String login (String username,String password,Model model) {/ / get the current user Subject subject = SecurityUtils.getSubject (); / / encapsulate user data UsernamePasswordToken taken = new UsernamePasswordToken (username,password) Try {/ / performs login operation. No exception indicates successful login subject.login (taken); return "index";} catch (UnknownAccountException e) {model.addAttribute ("msg", "user name error"); return "login";} catch (IncorrectCredentialsException e) {model.addAttribute ("msg", "password error") Return "login";}

Config file

ShiroConfig.java

Package com.example.config;import org.apache.shiro.spring.web.ShiroFilterFactoryBean;import org.apache.shiro.web.mgt.DefaultWebSecurityManager;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import java.util.LinkedHashMap;import java.util.Map;@Configurationpublic class ShiroConfig {/ / 4.ShiroFilterFactoryBean @ Bean public ShiroFilterFactoryBean getshiroFilterFactoryBean (@ Qualifier ("SecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean () / / 5. Set the security manager factoryBean.setSecurityManager (defaultWebSecurityManager); / * shiro built-in filter anon can be accessed without authorization and login, and can be accessed by everyone. Authc requires login authorization to access. The authcBasic Basic HTTP authentication interceptor logout exits the interceptor. After the exit is successful, redirect to the set / URI noSessionCreation does not create a session connector perms authorization interceptor. Only with permission to a resource can you access the port port interceptor, rest rest style interceptor, roles role interceptor, and access ssl ssl interceptor only if you have the permission of a role. You can only pass the user user interceptor through the https protocol. You need a remember me function to use * / Map filterMap = new LinkedHashMap (); / / A pair of files under / user/* can only access filterMap.put ("/ user/*", "authc") if they have authc permission; / / store Map in ShiroFilterFactoryBean factoryBean.setFilterChainDefinitionMap (filterMap) / / Jump to toLogin factoryBean.setLoginUrl ("/ toLogin") when permission authentication is required; / / jump to unauthorized factoryBean.setUnauthorizedUrl ("/ unauthorized") when permission authentication fails; return factoryBean;} / / 2. Create DefaultWebSecurityManager @ Bean (name = "SecurityManager") public DefaultWebSecurityManager getDefaultWebSecurityManager (@ Qualifier ("userRealm") UserRealm userRealm) {DefaultWebSecurityManager SecurityManager=new DefaultWebSecurityManager (); / / 3. Associate Realm SecurityManager.setRealm (userRealm); return SecurityManager;} / / 1. Create a Realm object @ Bean (name = "userRealm") public UserRealm userRealm () {return new UserRealm ();}}

UserRealm.java

Package com.example.config;import org.apache.shiro.authc.*;import org.apache.shiro.authz.AuthorizationInfo;import org.apache.shiro.realm.AuthorizingRealm;import org.apache.shiro.subject.PrincipalCollection;public class UserRealm extends AuthorizingRealm {/ / authorization @ Override protected AuthorizationInfo doGetAuthorizationInfo (PrincipalCollection principalCollection) {System.out.println ("authorization"); return null } / / UsernamePasswordToken @ Override protected AuthenticationInfo doGetAuthenticationInfo (AuthenticationToken authenticationToken) throws AuthenticationException {System.out.println ("certification"); String name = "root"; String password = "123456"; UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken; if (! userToken.getUsername (). Equals (name)) {return null / / throw exception username error the exception} / / password authentication, shiro does return new SimpleAuthenticationInfo ("", password, ");}}

However, in the case of user authentication, the real situation is taken from the database, so let's next implement to extract data from the database to achieve user authentication.

Shiro integrates mybatis

Preliminary work

In the previously imported dependencies, continue to add the following dependencies

Mysql mysql-connector-java log4j log4j 1.2.17 com.alibaba druid 1.2.5 org.mybatis.spring.boot mybatis-spring-boot-starter 2.1.4 org.projectlombok lombok

After importing mybatis and Druid, go to application.properties to configure and Druid.

Druid

Spring: datasource: username: root password: 123456 url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8 driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource # Custom data source # Spring Boot does not inject these attribute values by default Need to bind # druid data source proprietary configuration initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true # configure filters for monitoring statistics interception Stat: monitoring statistics, log4j: logging, wall: defense sql injection # if the Times allows java.lang.ClassNotFoundException: org.apache.log4j.Priority #, import log4j dependency, Maven address: https://mvnrepository.com/artifact/log4j/log4j filters: stat,wall,log4j maxPoolPreparedStatementPerConnectionSize: 20 useGlobalDataSourceStat: true connectionProperties: druid.stat.mergeSql=true Druid.stat.slowSqlMillis=500

Mybatis

Mybatis: type-aliases-package: com.example.pojo mapper-locations: classpath:mapper/*.xml

Connect to the database

Write entity classes

Package com.example.pojo;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;@Data@AllArgsConstructor@NoArgsConstructorpublic class User {private Integer id; private String name; private String pwd;}

Write mapper

Package com.example.mapper;import com.example.pojo.User;import org.apache.ibatis.annotations.Mapper;import org.springframework.stereotype.Repository;@Repository@Mapperpublic interface UserMapper {public User getUserByName (String name);}

Write mapper.xml

Select * from mybatis.user where name=# {name}

Write service

Package com.example.service;import com.example.pojo.User;public interface UserService {public User getUserByName (String name);} package com.example.service;import com.example.mapper.UserMapper;import com.example.pojo.User;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Servicepublic class UserServiceImpl implements UserService {@ Autowired UserMapper userMapper; @ Override public User getUserByName (String name) {return userMapper.getUserByName (name);}}

Working with data in the database

Just modify the UserRealm.java.

Package com.example.config;import com.example.pojo.User;import com.example.service.UserService;import org.apache.shiro.authc.*;import org.apache.shiro.authz.AuthorizationInfo;import org.apache.shiro.realm.AuthorizingRealm;import org.apache.shiro.subject.PrincipalCollection;import org.springframework.beans.factory.annotation.Autowired;public class UserRealm extends AuthorizingRealm {@ Autowired UserService userService; / / license @ Override protected AuthorizationInfo doGetAuthorizationInfo (PrincipalCollection principalCollection) {System.out.println ("license") Return null;} / / Authentication @ Override protected AuthenticationInfo doGetAuthenticationInfo (AuthenticationToken authenticationToken) throws AuthenticationException {System.out.println ("authentication"); UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken; / / Connect to the real database User user= userService.getUserByName (userToken.getUsername ()); if (user==null) {return null / / throw exception username error the exception} / / password authentication, shiro does return new SimpleAuthenticationInfo (", user.getPwd (),");}} authentication is done, let's take a look at authorization again

Add authorization to the ShiroConfig.java file and add this line of code: filterMap.put ("/ user/add", "perms [user:add]"); / / only people with user:add permission can access add. Note that the location of authorization is in front of authentication, otherwise authorization will fail authentication.

Run the test: the add page cannot be accessed

Authorization is the same: filterMap.put ("/ user/update", "perms [user:update]"); / / only people with user:update permission can access update

Customize an unauthorized jump page

Jump to the unauthorized page when the ShiroConfig.java file settings are not authorized, and add this line of code:

FactoryBean.setUnauthorizedUrl ("/ unauthorized") 2. Go to Mycontroller to write and jump to unauthorized pages.

@ RequestMapping ("/ unauthorized") @ ResponseBody// does not bother to write the interface and returns a string public String unauthorized () {return "cannot be accessed without authorization";}

Add an attribute perms to the database and modify the corresponding entity class

Modify UserRealm.java

Package com.example.config;import com.example.pojo.User;import com.example.service.UserService;import org.apache.shiro.SecurityUtils;import org.apache.shiro.authc.*;import org.apache.shiro.authz.AuthorizationInfo;import org.apache.shiro.authz.SimpleAuthorizationInfo;import org.apache.shiro.realm.AuthorizingRealm;import org.apache.shiro.subject.PrincipalCollection;import org.apache.shiro.subject.Subject;import org.springframework.beans.factory.annotation.Autowired;public class UserRealm extends AuthorizingRealm {@ Autowired UserService userService / / Override protected AuthorizationInfo doGetAuthorizationInfo (PrincipalCollection principalCollection) {System.out.println ("authorization") {System.out.println ("authorization"); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo (); / / without using the database, the user permissions set directly by yourself are set for everyone. In reality, you need to take / / info.addStringPermission ("user:add") from the database. / / get the permission information from the database / / get the object Subject subject = SecurityUtils.getSubject () of the current login; / / get the User object, and get User currentUser = (User) subject.getPrincipal () through getPrincipal (); / / set the permission of the current user info.addStringPermission (currentUser.getPerms ()); return info } / / Override protected AuthenticationInfo doGetAuthenticationInfo @ Override protected AuthenticationInfo doGetAuthenticationInfo (AuthenticationToken authenticationToken) throws AuthenticationException {System.out.println ("authentication"); UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken; / / Connect to the real database User user= userService.getUserByName (userToken.getUsername ()); if (user==null) {return null / / throw exception username error the exception} / / password authentication, shiro does return new SimpleAuthenticationInfo (user,user.getPwd (), ");}}

With authorization, another question arises: do we want users to lose sight of things that they do not have permission to see? At this point, there is Shiro-thymeleaf integration.

Shiro-thymeleaf integration

Import integrated dependencies

Com.github.theborakompanioni thymeleaf-extras-shiro 2.0.0

Integrate ShiroDialect in ShiroConfig

/ / integrate ShiroDialect: used to integrate shiro thymeleaf @ Bean public ShiroDialect getShiroDialect () {return new ShiroDialect ();}

Modify the index page

Home page

Log in to add update

Determine whether a user is logged in

/ / this is the judgment used by integrating shiro and thymeleaf to make the login button disappear: Subject subject = SecurityUtils.getSubject (); Session session = subject.getSession (); session.setAttribute ("loginUser", user)

test

Thank you for reading, the above is the content of "how to use Java security framework Shiro". After the study of this article, I believe you have a deeper understanding of how to use Java security framework Shiro, and the specific use needs to be verified in practice. Here is, the editor will push for you more related knowledge points of the article, welcome to follow!

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Development

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report